├── __init__.py ├── MD5SUMS ├── SHA256SUMS ├── pytest.ini ├── pyproject.toml ├── tests ├── unit │ ├── _test_template.py │ ├── test_get_utcnow.py │ ├── test_audit_at_is_restricted_to_authorized_users.py │ ├── test_shellexec.py │ ├── test_audit_duplicate_gids.py │ ├── test_audit_duplicate_uids.py │ ├── test_output_json.py │ ├── test_audit_duplicate_user_names.py │ ├── test_audit_nftables_table_exists.py │ ├── test_audit_duplicate_group_names.py │ ├── test_audit_default_group_for_root.py │ ├── test_audit_mta_is_localhost_only.py │ ├── test_audit_no_unconfined_services.py │ ├── test_audit_firewalld_default_zone_is_set.py │ ├── test_audit_iptables_is_flushed.py │ ├── test_audit_root_is_only_uid_0_account.py │ ├── test_audit_sudo_log_exists.py │ ├── test_audit_audit_log_size_is_configured.py │ ├── test_audit_service_enabled.py │ ├── test_audit_audit_config_is_immutable.py │ ├── test_audit_nxdx_support_enabled.py │ ├── test_audit_auth_for_single_user_mode.py │ ├── test_audit_xdmcp_is_not_enabled.py │ ├── test_audit_selinux_policy_is_configured.py │ ├── test_audit_sudo_commands_use_pty.py │ ├── test_audit_audit_logs_not_automatically_deleted.py │ ├── test_audit_etc_shadow_password_fields_are_not_empty.py │ ├── test_audit_package_not_installed.py │ ├── test_audit_partition_is_separate.py │ ├── test_audit_core_dumps_restricted.py │ ├── test_audit_password_reuse_is_limited.py │ ├── test_audit_etc_passwd_accounts_use_shadowed_passwords.py │ ├── test_audit_permissions_on_log_files.py │ ├── test_audit_journald_configured_to_compress_large_logs.py │ ├── test_audit_partition_option_is_set.py │ ├── test_audit_permissions_on_public_host_key_files.py │ ├── test_audit_permissions_on_private_host_key_files.py │ ├── test_audit_journald_configured_to_send_logs_to_rsyslog.py │ ├── test_audit_journald_configured_to_write_logfiles_to_disk.py │ ├── test_audit_rsyslog_default_file_permission_is_configured.py │ ├── test_audit_events_for_login_and_logout_are_collected.py │ ├── test_audit_removable_partition_option_is_set.py │ ├── test_audit_etc_passwd_gids_exist_in_etc_group.py │ ├── test_audit_package_not_installed_or_service_is_masked.py │ ├── test_audit_service_disabled.py │ ├── test_audit_service_masked.py │ ├── test_audit_events_for_session_initiation_are_collected.py │ ├── test_audit_events_for_changes_to_sysadmin_scope_are_collected.py │ ├── test_audit_sticky_bit_set_on_dirs.py │ ├── test_audit_nftables_default_deny_policy.py │ ├── test_audit_nftables_loopback_is_configured.py │ ├── test_audit_shadow_group_is_empty.py │ ├── test_audit_service_is_enabled_and_is_active.py │ ├── test_audit_sysctl_flags_are_set.py │ ├── test_get_homedirs.py │ ├── test_audit_events_that_modify_mandatory_access_controls_are_collected.py │ ├── test_audit_system_is_disabled_when_audit_logs_are_full.py │ ├── test_audit_cron_is_restricted_to_authorized_users.py │ ├── test_audit_events_that_modify_usergroup_info_are_collected.py │ ├── test_audit_package_is_installed.py │ ├── test_audit_password_change_minimum_delay.py │ ├── test_output_text.py │ ├── test_audit_password_expiration_warning_is_configured.py │ ├── test_audit_password_expiration_max_days_is_configured.py │ ├── test_audit_service_active.py │ ├── test_audit_system_accounts_are_secured.py │ ├── test_audit_selinux_mode_is_enforcing.py │ ├── test_audit_selinux_mode_not_disabled.py │ ├── test_audit_chrony_is_configured.py │ ├── test_audit_bootloader_password_is_set.py │ ├── test_audit_updates_installed.py │ ├── test_audit_nftables_connections_are_configured.py │ └── test_audit_gdm_login_banner_configure.py └── integration │ ├── README.md │ ├── __init__.py │ ├── test_integration__get_utcnow.py │ ├── test_integration_audit_service_active.py │ ├── test_integration_audit_service_enabled.py │ ├── test_integration_audit_service_disabled.py │ ├── test_integration_audit_package_is_installed.py │ ├── test_integration_audit_partition_is_separate.py │ ├── test_integration_audit_package_not_installed.py │ ├── test_integration_audit_service_is_enabled_and_is_active.py │ ├── test_integration_audit_partition_option_is_set.py │ ├── test_integration_audit_removable_partition_option_is_set.py │ ├── test_integration__get_homedirs.py │ ├── test_integration_audit_default_group_for_root.py │ ├── test_integration_audit_duplicate_uids.py │ ├── test_integration_audit_selinux_mode_is_enforcing.py │ ├── test_integration_audit_service_masked.py │ ├── test_integration_audit_selinux_mode_not_disabled.py │ ├── test_integration_audit_firewalld_default_zone_is_set.py │ ├── test_integration_audit_sudo_log_exists.py │ ├── test_integration_audit_sudo_commands_use_pty.py │ ├── test_integration_audit_system_accounts_are_secured.py │ ├── test_integration_audit_duplicate_group_names.py │ ├── test_integration_audit_only_one_package_is_installed.py │ ├── test_integration_audit_nftables_table_exists.py │ ├── test_integration_audit_etc_passwd_gids_exist_in_etc_group.py │ ├── test_integration_audit_iptables_is_flushed.py │ ├── test_integration_audit_duplicate_user_names.py │ ├── test_integration_audit_duplicate_gids.py │ ├── test_integration_audit_etc_shadow_password_fields_are_not_empty.py │ ├── test_integration_audit_permissions_on_public_host_key_files.py │ ├── test_integration_audit_etc_passwd_accounts_use_shadowed_passwords.py │ ├── test_integration_audit_sticky_bit_set_on_dirs.py │ ├── test_integration_audit_root_is_only_uid_0_account.py │ ├── test_integration_audit_audit_config_is_immutable.py │ ├── test_integration_audit_rsyslog_default_file_permission_is_configured.py │ ├── test_integration_audit_audit_logs_not_automatically_deleted.py │ ├── test_integration__shellexec.py │ ├── test_integration_audit_homedirs_exist.py │ ├── test_integration_audit_permissions_on_private_host_key_files.py │ ├── test_integration_audit_mta_is_localhost_only.py │ ├── test_integration_audit_permissions_on_log_files.py │ ├── test_integration_audit_journald_configured_to_compress_large_logs.py │ ├── test_integration_audit_xdmcp_is_not_enabled.py │ ├── test_integration_audit_journald_configured_to_send_logs_to_rsyslog.py │ ├── test_integration_audit_journald_configured_to_write_logfiles_to_disk.py │ ├── test_integration_audit_nxdx_support_enabled.py │ ├── test_integration_audit_nftables_loopback_is_configured.py │ ├── test_integration_audit_core_dumps_restricted.py │ ├── test_integration_audit_ntp_is_configured.py │ ├── test_integration_audit_sysctl_flags_are_set.py │ ├── test_integration_audit_at_is_restricted_to_authorized_users.py │ ├── test_integration_audit_homedirs_ownership.py │ ├── test_integration_audit_events_for_changes_to_sysadmin_scope_are_collected.py │ ├── test_integration_audit_password_change_minimum_delay.py │ ├── test_integration_audit_password_hashing_algoritm.py │ ├── test_integration_audit_password_inactive_lock_is_configured.py │ ├── test_integration_audit_updates_installed.py │ ├── test_integration_audit_bootloader_password_is_set.py │ ├── test_integration_audit_password_change_max_days_is_configured.py │ ├── test_integration_audit_password_expiration_warning_is_configured.py │ ├── test_integration_audit_kernel_module_is_disabled.py │ ├── test_integration_audit_events_for_login_and_logout_are_collected.py │ ├── test_integration_audit_selinux_policy_is_configured.py │ ├── test_integration_audit_homedirs_permissions.py │ ├── test_integration_audit_auth_for_single_user_mode.py │ ├── test_integration_audit_log_size_is_configured.py │ ├── test_integration_audit_password_reuse_is_limited.py │ ├── test_integration_audit_events_for_session_initiation_are_collected.py │ ├── test_integration_audit_chrony_is_configured.py │ ├── test_integration_audit_events_that_modify_manditory_access_controls_are_collected.py │ ├── test_integration_audit_package_not_installed_or_service_is_masked.py │ ├── test_integration_audit_no_unconfined_services.py │ ├── test_integration_audit_events_that_modify_usergroup_info_are_collected.py │ ├── test_integration_audit_gpgcheck_is_activated.py │ ├── test_integration_audit_nftables_default_deny_policy.py │ ├── test_integration_audit_nftables_connections_are_configured.py │ ├── test_integration_audit_events_that_modify_network_environment_are_collected.py │ ├── test_integration_audit_iptables_loopback_is_configured.py │ ├── test_integration_audit_access_to_su_command_is_restricted.py │ ├── test_integration_audit_iptables_default_deny_policy.py │ ├── test_integration_audit_rsyslog_sends_logs_to_a_remote_log_host.py │ ├── test_integration_audit_events_for_successful_file_system_mounts_are_collected.py │ ├── test_integration_audit_cron_is_restricted_to_authorized_users.py │ ├── test_integration_audit_shadow_group_is_empty.py │ ├── test_integration_audit_events_for_file_deletion_by_users_are_collected.py │ ├── test_integration_audit_events_for_kernel_module_loading_and_unloading_are_collected.py │ ├── test_integration_audit_events_for_system_administrator_commands_are_collected.py │ ├── test_integration_audit_system_is_disabled_when_audit_logs_are_full.py │ └── test_integration_audit_nftables_base_chain_exists.py ├── .flake8 ├── bin ├── check_audit_module_order.sh └── centos-bootstrap.sh ├── tox.ini ├── Pipfile ├── Vagrantfile └── setup.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MD5SUMS: -------------------------------------------------------------------------------- 1 | 9b4cb9c5cccf92f701df647cd46a0da3 cis_audit.py 2 | -------------------------------------------------------------------------------- /SHA256SUMS: -------------------------------------------------------------------------------- 1 | a56e2330f5502b4c933d99e1ccc7f0ebcaef2ea612d8a5fd0057801a3a2097e8 cis_audit.py 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -q --cov=cis_audit --cov-fail-under=100 --cov-report html --cov-report term-missing --ignore=tests/integration 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 999 3 | skip-string-normalization = true 4 | target-version = ['py36'] 5 | 6 | [tool.isort] 7 | profile = "black" 8 | -------------------------------------------------------------------------------- /tests/unit/_test_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | import cis_audit 6 | 7 | test = cis_audit.CISAudit() 8 | 9 | 10 | if __name__ == '__main__': 11 | pytest.main([__file__]) 12 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 9999 3 | ignore = 4 | E501 E265 E266 5 | W293 E203 6 | 7 | select = C,E,F,W,B,B901 8 | exclude = 9 | .archive, 10 | .git, 11 | __pycache__, 12 | build, 13 | dist, 14 | -------------------------------------------------------------------------------- /bin/check_audit_module_order.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## Helper to make sure the audit modules are sorted alphabetically so they're easier to find 3 | 4 | diff -y <(grep 'def audit_' cis_audit.py) <(grep 'def audit_' cis_audit.py | LC_COLLATE=C sort) 5 | -------------------------------------------------------------------------------- /tests/integration/README.md: -------------------------------------------------------------------------------- 1 | ### Integration Tests 2 | 3 | This directory contains pytest tests which test that the cis-audit.py script works on it's target OS's. This is achieved by making changes to the underlying operating system and as such, do not run these integration tests on a host that you care about! 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | skip_missing_interpreters = True 4 | envlist = py36 5 | [testenv] 6 | # install pytest in the virtualenv where commands will be executed 7 | deps = 8 | pytest 9 | pytest-cov 10 | commands = 11 | # NOTE: you can run any command line tool here – not just tests 12 | pytest 13 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | cis-benchmarks-audit = {editable = true, path = "."} 10 | black = "*" 11 | flake8 = "*" 12 | isort = "*" 13 | mock = "*" 14 | pyfakefs = "<4.6.0" 15 | pytest-cov = "*" 16 | vermin = "*" 17 | 18 | [requires] 19 | python_version = "3.6" 20 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from types import SimpleNamespace 3 | 4 | 5 | def shellexec(command: str): 6 | result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, universal_newlines=True) 7 | output = result.stdout.split('\n') 8 | error = result.stderr.split('\n') 9 | returncode = result.returncode 10 | 11 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 12 | -------------------------------------------------------------------------------- /tests/unit/test_get_utcnow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | test = CISAudit() 10 | 11 | 12 | def test_get_utcnow(): 13 | testtime = test._get_utcnow() 14 | realtime = datetime.utcnow() 15 | timediff = realtime - testtime 16 | 17 | assert timediff.seconds < 1 18 | 19 | 20 | if __name__ == '__main__': 21 | pytest.main([__file__, '--no-cov']) 22 | -------------------------------------------------------------------------------- /tests/integration/test_integration__get_utcnow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | test = CISAudit() 10 | 11 | 12 | def test_integration__get_utcnow(): 13 | testtime = test._get_utcnow() 14 | realtime = datetime.utcnow() 15 | timediff = realtime - testtime 16 | 17 | assert timediff.seconds < 1 18 | 19 | 20 | if __name__ == '__main__': 21 | pytest.main([__file__, '--no-cov']) 22 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_service_active.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_ervice_active_pass(): 9 | state = CISAudit().audit_service_is_active(service='sshd') 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_service_active_fail(): 14 | state = CISAudit().audit_service_is_active(service='rsyncd') 15 | assert state == 1 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_service_enabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_service_enabled_pass(): 9 | state = CISAudit().audit_service_is_enabled(service='sshd') 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_service_enabled_fail(): 14 | state = CISAudit().audit_service_is_enabled(service='rsyncd') 15 | assert state == 1 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_service_disabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_service_disabled_pass(): 9 | state = CISAudit().audit_service_is_disabled(service='rsyncd') 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_service_disabled_fail(): 14 | state = CISAudit().audit_service_is_disabled(service='sshd') 15 | assert state == 1 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | # config.vm.provider "virtualbox" do |v| 6 | # v.cpus = 2 7 | # v.memory = 2048 8 | # v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 9 | # v.customize ["modifyvm", :id, "--natdnsproxy1", "on"] 10 | #end 11 | 12 | config.vm.define "centos7" do |d| 13 | d.vm.box = "bento/centos-7" 14 | d.vm.hostname = "centos7" 15 | d.vm.provision "shell", path: "bin/centos-bootstrap.sh", privileged: "true" 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_package_is_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_package_is_installed_pass(): 9 | state = CISAudit().audit_package_is_installed(package='rsync') 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_package_is_installed_fail(): 14 | state = CISAudit().audit_package_is_installed(package='pytest') 15 | assert state == 1 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_partition_is_separate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_partition_is_separate(): 9 | state = CISAudit().audit_partition_is_separate(partition='/boot') 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_partition_is_not_separate(): 14 | state = CISAudit().audit_partition_is_separate(partition='/var') 15 | assert state == 1 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_package_not_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_package_not_installed_pass(): 9 | state = CISAudit().audit_package_not_installed(package='pytest') 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_package_not_installed_fail(): 14 | state = CISAudit().audit_package_not_installed(package='rsync') 15 | assert state == 1 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_service_is_enabled_and_is_active.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_service_is_enabled_and_is_active_pass(): 9 | state = CISAudit().audit_service_is_enabled_and_is_active(service='sshd') 10 | assert state == 0 11 | 12 | 13 | def test_service_is_enabled_and_is_active_fail(): 14 | state = CISAudit().audit_service_is_enabled_and_is_active(service='rsyncd') 15 | assert state == 3 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_partition_option_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_partition_option_is_set(): 9 | state = CISAudit().audit_partition_option_is_set(partition='/boot', option='relatime') 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_partition_option_is_not_set(): 14 | state = CISAudit().audit_partition_option_is_set(partition='/boot', option='nodev') 15 | assert state == 1 16 | 17 | 18 | if __name__ == '__main__': 19 | pytest.main([__file__, '--no-cov']) 20 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_removable_partition_option_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | test = CISAudit() 8 | 9 | 10 | def test_integration_audit_removable_partition_option_is_set_pass(): 11 | state = test.audit_removable_partition_option_is_set(option='noexec') 12 | assert state == 0 13 | 14 | 15 | # def test_integration_audit_removable_partition_option_is_set_fail(): 16 | # state = test.audit_removable_partition_option_is_set(option='noexec') 17 | # assert state == 1 18 | 19 | 20 | if __name__ == '__main__': 21 | pytest.main([__file__, '--no-cov']) 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import setuptools 4 | 5 | from cis_audit import __version__ 6 | 7 | setuptools.setup( 8 | name="cis-benchmarks-audit", 9 | version=__version__, 10 | author="Andy Dustin", 11 | author_email="andy.dustin@gmail.com", 12 | description="Check systems conformance to CIS Hardening benchmarks", 13 | packages=setuptools.find_packages(), 14 | py_modules=['cis_audit'], 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.6", 18 | "License :: OSI Approved :: Apache Software License", 19 | ], 20 | python_requires='==3.6.*', 21 | ) 22 | -------------------------------------------------------------------------------- /tests/integration/test_integration__get_homedirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import GeneratorType 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | test = CISAudit() 10 | 11 | 12 | def test_integration__get_homedirs_pass(): 13 | homedirs = test._get_homedirs() 14 | homedirs_list = list(homedirs) 15 | 16 | assert isinstance(homedirs, GeneratorType) 17 | assert homedirs_list[0] == ('root', 0, '/root') 18 | assert homedirs_list[1] == ('vagrant', 1000, '/home/vagrant') 19 | 20 | 21 | if __name__ == '__main__': 22 | pytest.main([__file__, '--no-cov', '-W', 'ignore:Module already imported:pytest.PytestWarning']) 23 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_default_group_for_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | shellexec('usermod -g 1 root') 12 | 13 | yield None 14 | 15 | shellexec('usermod -g 0 root') 16 | 17 | 18 | def test_integration_audit_default_group_for_root_pass(): 19 | state = CISAudit().audit_default_group_for_root() 20 | assert state == 0 21 | 22 | 23 | def test_integration_audit_default_group_for_root_fail(setup_to_fail): 24 | state = CISAudit().audit_default_group_for_root() 25 | assert state == 1 26 | 27 | 28 | if __name__ == '__main__': 29 | pytest.main([__file__, '--no-cov']) 30 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_duplicate_uids.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | shellexec('echo "pytest:x:0:0:pytest:/bin/bash" >> /etc/passwd') 12 | 13 | yield None 14 | 15 | shellexec('sed -i "/pytest/d" /etc/passwd') 16 | 17 | 18 | def test_integration_audit_duplicate_uids_pass(): 19 | state = CISAudit().audit_duplicate_uids() 20 | assert state == 0 21 | 22 | 23 | def test_integration_audit_duplicate_uids_fail(setup_to_fail): 24 | state = CISAudit().audit_duplicate_uids() 25 | assert state == 1 26 | 27 | 28 | if __name__ == '__main__': 29 | pytest.main([__file__, '--no-cov']) 30 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_selinux_mode_is_enforcing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_selinux_is_enforcing_pass_enforcing(setup_selinux_enforcing): 9 | state = CISAudit().audit_selinux_mode_is_enforcing() 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_selinux_is_enforcing_fail_permissive(setup_selinux_permissive): 14 | state = CISAudit().audit_selinux_mode_is_enforcing() 15 | assert state == 3 16 | 17 | 18 | def test_integration_audit_selinux_is_enforcing_fail_disabled(setup_selinux_disabled): 19 | state = CISAudit().audit_selinux_mode_is_enforcing() 20 | assert state == 3 21 | 22 | 23 | if __name__ == '__main__': 24 | pytest.main([__file__, '--no-cov']) 25 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_service_masked.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('systemctl mask rsyncd') 13 | 14 | yield None 15 | 16 | ## Tear-down 17 | shellexec('systemctl unmask rsyncd') 18 | 19 | 20 | def test_integration_audit_service_masked_pass(setup_to_pass): 21 | state = CISAudit().audit_service_is_masked(service='rsyncd') 22 | assert state == 0 23 | 24 | 25 | def test_integration_audit_service_masked_fail(): 26 | state = CISAudit().audit_service_is_masked(service='rsyncd') 27 | assert state == 1 28 | 29 | 30 | if __name__ == '__main__': 31 | pytest.main([__file__, '--no-cov']) 32 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_selinux_mode_not_disabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_selinux_mode_not_disabled_pass_enforcing(setup_selinux_enforcing): 9 | state = CISAudit().audit_selinux_mode_not_disabled() 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_selinux_mode_not_disabled_pass_permissive(setup_selinux_permissive): 14 | state = CISAudit().audit_selinux_mode_not_disabled() 15 | assert state == 0 16 | 17 | 18 | def test_integration_audit_selinux_mode_not_disabled_fail_disabled(setup_selinux_disabled): 19 | state = CISAudit().audit_selinux_mode_not_disabled() 20 | assert state == 3 21 | 22 | 23 | if __name__ == '__main__': 24 | pytest.main([__file__, '--no-cov']) 25 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_firewalld_default_zone_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('systemctl start firewalld') 13 | 14 | yield None 15 | 16 | ## Tear-down 17 | shellexec('systemctl stop firewalld') 18 | 19 | 20 | def test_integration_firewalld_defaullt_zone_set_pass(setup_to_pass): 21 | state = CISAudit().audit_firewalld_default_zone_is_set() 22 | assert state == 0 23 | 24 | 25 | def test_integration_firewalld_not_running(): 26 | state = CISAudit().audit_firewalld_default_zone_is_set() 27 | assert state == 1 28 | 29 | 30 | if __name__ == '__main__': 31 | pytest.main([__file__, '--no-cov']) 32 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_sudo_log_exists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | 10 | @pytest.fixture 11 | def setup_to_pass(): 12 | ## Setup 13 | with open('/etc/sudoers.d/pytest', 'w') as f: 14 | f.write('Defaults logfile="/var/log/sudo.log"\n') 15 | 16 | yield None 17 | 18 | ## Tear-down 19 | os.remove('/etc/sudoers.d/pytest') 20 | 21 | 22 | def test_integration_audit_sudo_log_exists_pass(setup_to_pass): 23 | state = CISAudit().audit_sudo_log_exists() 24 | assert state == 0 25 | 26 | 27 | def test_integration_audit_sudo_log_exists_fail(): 28 | state = CISAudit().audit_sudo_log_exists() 29 | assert state == 1 30 | 31 | 32 | if __name__ == '__main__': 33 | pytest.main([__file__, '--no-cov']) 34 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_sudo_commands_use_pty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | 10 | @pytest.fixture 11 | def setup_to_pass(): 12 | ## Setup 13 | with open('/etc/sudoers.d/pytest', 'w') as f: 14 | f.write('Defaults use_pty\n') 15 | 16 | yield None 17 | 18 | ## Tear-down 19 | os.remove('/etc/sudoers.d/pytest') 20 | 21 | 22 | def test_integration_audit_sudo_commands_use_pty_pass(setup_to_pass): 23 | state = CISAudit().audit_sudo_commands_use_pty() 24 | assert state == 0 25 | 26 | 27 | def test_integration_audit_sudo_commands_use_pty_fail(): 28 | state = CISAudit().audit_sudo_commands_use_pty() 29 | assert state == 1 30 | 31 | 32 | if __name__ == '__main__': 33 | pytest.main([__file__, '--no-cov']) 34 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_system_accounts_are_secured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_fail(): 11 | ## Setup 12 | shellexec('useradd -r -s /bin/bash pytest') 13 | 14 | yield None 15 | 16 | ## Tear-down 17 | shellexec('userdel pytest') 18 | 19 | 20 | def test_integration_audit_system_accounts_are_secured_pass(): 21 | state = CISAudit().audit_system_accounts_are_secured() 22 | assert state == 0 23 | 24 | 25 | def test_integration_audit_system_accounts_are_secured_fail(setup_to_fail): 26 | state = CISAudit().audit_system_accounts_are_secured() 27 | assert state == 1 28 | 29 | 30 | if __name__ == '__main__': 31 | pytest.main([__file__, '--no-cov']) 32 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_duplicate_group_names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | shellexec('echo "pytest:x:1001:" >> /etc/group') 12 | shellexec('echo "pytest:x:1002:" >> /etc/group') 13 | 14 | yield None 15 | 16 | shellexec('sed -i "/pytest/d" /etc/group') 17 | 18 | 19 | def test_audit_integration_duplicate_group_names_pass(): 20 | state = CISAudit().audit_duplicate_group_names() 21 | assert state == 0 22 | 23 | 24 | def test_audit_integration_duplicate_group_names_fail(setup_to_fail): 25 | state = CISAudit().audit_duplicate_group_names() 26 | assert state == 1 27 | 28 | 29 | if __name__ == '__main__': 30 | pytest.main([__file__, '--no-cov']) 31 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_only_one_package_is_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | 8 | def test_integration_audit_only_one_package_is_installed_pass(): 9 | state = CISAudit().audit_only_one_package_is_installed(packages="rsync gdm") 10 | assert state == 0 11 | 12 | 13 | def test_integration_audit_only_one_package_is_installed_fail_both_installed(): 14 | state = CISAudit().audit_only_one_package_is_installed(packages='rsync bash') 15 | assert state == 1 16 | 17 | 18 | def test_integration_audit_only_one_package_is_installed_fail_neither_installed(): 19 | state = CISAudit().audit_only_one_package_is_installed(packages="java-1.8.0-openjdk java-11-openjdk") 20 | assert state == 1 21 | 22 | 23 | if __name__ == '__main__': 24 | pytest.main([__file__, '--no-cov']) 25 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_nftables_table_exists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_fail(): 11 | ## Setup 12 | shellexec('nft delete table inet filter') 13 | 14 | yield None 15 | 16 | ## Tear-down 17 | shellexec('nft create table inet filter') 18 | 19 | 20 | def test_integration_audit_nftables_table_exists_pass(setup_install_nftables): 21 | state = CISAudit().audit_nftables_table_exists() 22 | assert state == 0 23 | 24 | 25 | def test_integration_audit_nftables_table_exists_fail(setup_install_nftables, setup_to_fail): 26 | state = CISAudit().audit_nftables_table_exists() 27 | assert state == 1 28 | 29 | 30 | if __name__ == '__main__': 31 | pytest.main([__file__, '--no-cov']) 32 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_etc_passwd_gids_exist_in_etc_group.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | shellexec('echo "pytest:x:1001:1001::/home/pytest:/bin/bash" >> /etc/passwd') 12 | 13 | yield None 14 | 15 | shellexec('sed -i "/pytest/d" /etc/passwd') 16 | 17 | 18 | def test_integration_gids_from_etcpasswd_are_in_etcgroup_pass(): 19 | state = CISAudit().audit_etc_passwd_gids_exist_in_etc_group() 20 | assert state == 0 21 | 22 | 23 | def test_integration_gids_from_etcpasswd_are_in_etcgroup_fail(setup_to_fail): 24 | state = CISAudit().audit_etc_passwd_gids_exist_in_etc_group() 25 | assert state == 1 26 | 27 | 28 | if __name__ == '__main__': 29 | pytest.main([__file__, '--no-cov']) 30 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_iptables_is_flushed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_fail(): 11 | ## Setup 12 | shellexec('iptables -A INPUT -i lo -j ACCEPT') 13 | shellexec('ip6tables -A INPUT -i lo -j ACCEPT') 14 | 15 | yield None 16 | 17 | ## Tear-down 18 | shellexec('iptables -F') 19 | shellexec('ip6tables -F') 20 | 21 | 22 | def test_integration_iptables_is_flushed_pass(): 23 | state = CISAudit().audit_iptables_is_flushed() 24 | assert state == 0 25 | 26 | 27 | def test_integration_iptables_is_flushed_fail(setup_to_fail): 28 | state = CISAudit().audit_iptables_is_flushed() 29 | assert state == 3 30 | 31 | 32 | if __name__ == '__main__': 33 | pytest.main([__file__, '--no-cov']) 34 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_duplicate_user_names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | shellexec('echo "pytest:x:1001:1001:pytest:/bin/bash" >> /etc/passwd') 12 | shellexec('echo "pytest:x:1002:1002:pytest:/bin/bash" >> /etc/passwd') 13 | 14 | yield None 15 | 16 | shellexec('sed -i "/pytest/d" /etc/passwd') 17 | 18 | 19 | def test_integration_audit_duplicate_user_names_pass(): 20 | state = CISAudit().audit_duplicate_user_names() 21 | assert state == 0 22 | 23 | 24 | def test_integration_audit_duplicate_user_names_fail(setup_to_fail): 25 | state = CISAudit().audit_duplicate_user_names() 26 | assert state == 1 27 | 28 | 29 | if __name__ == '__main__': 30 | pytest.main([__file__, '--no-cov']) 31 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_duplicate_gids.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | '''Create a duplicate gid for the purpose of forcing the test to fail''' 12 | 13 | ## Setup 14 | cmd = 'echo "pytest:x:0:" >> /etc/group' 15 | shellexec(cmd) 16 | 17 | yield None 18 | 19 | ## Cleanup 20 | cmd = 'sed -i "/pytest/d" /etc/group' 21 | shellexec(cmd) 22 | 23 | 24 | def test_audit_duplicate_gids_pass(): 25 | state = CISAudit().audit_duplicate_gids() 26 | assert state == 0 27 | 28 | 29 | def test_audit_duplicate_gids_fail(setup_to_fail): 30 | state = CISAudit().audit_duplicate_gids() 31 | assert state == 1 32 | 33 | 34 | if __name__ == '__main__': 35 | pytest.main([__file__, '--no-cov']) 36 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_etc_shadow_password_fields_are_not_empty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | shellexec('echo "pytest::18353:0:99999:7:::" >> /etc/shadow') 12 | 13 | yield None 14 | 15 | shellexec('sed -i "/pytest/d" /etc/shadow') 16 | 17 | 18 | def test_integration_audit_etc_shadow_password_fields_are_not_empty_pass(): 19 | state = CISAudit().audit_etc_shadow_password_fields_are_not_empty() 20 | assert state == 0 21 | 22 | 23 | def test_integration_audit_etc_shadow_password_fields_are_not_empty_fail(setup_to_fail): 24 | state = CISAudit().audit_etc_shadow_password_fields_are_not_empty() 25 | assert state == 1 26 | 27 | 28 | if __name__ == '__main__': 29 | pytest.main([__file__, '--no-cov']) 30 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_permissions_on_public_host_key_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_fail(): 11 | ## Setup 12 | shellexec('chmod -c 664 /etc/ssh/ssh_host_*_key.pub') 13 | 14 | yield None 15 | 16 | shellexec('chmod -c 644 /etc/ssh/ssh_host_*_key.pub') 17 | 18 | 19 | def test_integration_audit_permissions_on_public_host_key_files_pass(): 20 | state = CISAudit().audit_permissions_on_public_host_key_files() 21 | assert state == 0 22 | 23 | 24 | def test_integration_audit_permissions_on_public_host_key_files_fail(setup_to_fail): 25 | state = CISAudit().audit_permissions_on_public_host_key_files() 26 | assert state == 7 27 | 28 | 29 | if __name__ == '__main__': 30 | pytest.main([__file__, '--no-cov']) 31 | -------------------------------------------------------------------------------- /tests/unit/test_audit_at_is_restricted_to_authorized_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | @patch.object(os.path, "exists", return_value=False) 14 | @patch.object(CISAudit, "audit_file_permissions", return_value=0) 15 | def test_audit_at_is_restricted_to_authorized_users_pass(*args): 16 | state = test.audit_at_is_restricted_to_authorized_users() 17 | assert state == 0 18 | 19 | 20 | @patch.object(os.path, "exists", return_value=True) 21 | @patch.object(CISAudit, "audit_file_permissions", return_value=1) 22 | def test_audit_at_is_restricted_to_authorized_users_fail(*args): 23 | state = test.audit_at_is_restricted_to_authorized_users() 24 | assert state == 3 25 | 26 | 27 | if __name__ == '__main__': 28 | pytest.main([__file__, '--no-cov']) 29 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_etc_passwd_accounts_use_shadowed_passwords.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_fail(): 11 | shellexec('echo "pytest:!!:1001:1001::/home/pytest:/bin/bash" >> /etc/passwd') 12 | 13 | yield None 14 | 15 | shellexec('sed -i "/pytest/d" /etc/passwd') 16 | 17 | 18 | def test_integration_audit_etc_passwd_accounts_use_shadowed_passwords_pass(): 19 | state = CISAudit().audit_etc_passwd_accounts_use_shadowed_passwords() 20 | assert state == 0 21 | 22 | 23 | def test_integration_audit_etc_passwd_accounts_use_shadowed_passwords_fail(setup_to_fail): 24 | state = CISAudit().audit_etc_passwd_accounts_use_shadowed_passwords() 25 | assert state == 1 26 | 27 | 28 | if __name__ == '__main__': 29 | pytest.main([__file__, '--no-cov']) 30 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_sticky_bit_set_on_dirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | 10 | @pytest.fixture 11 | def setup_to_fail(): 12 | ## Setup 13 | # We have to update the umask first, otherwise the directory is only created with 755 permissions 14 | os.umask(0o000) 15 | os.mkdir('/tmp/pytest', 0o777) 16 | 17 | yield None 18 | 19 | ## Tear-down 20 | os.rmdir('/tmp/pytest') 21 | 22 | 23 | def test_integration_audit_sticky_bit_on_world_writable_dirs_pass(): 24 | state = CISAudit().audit_sticky_bit_on_world_writable_dirs() 25 | assert state == 0 26 | 27 | 28 | def test_integration_audit_sticky_bit_on_world_writable_dirs_fail(setup_to_fail): 29 | state = CISAudit().audit_sticky_bit_on_world_writable_dirs() 30 | assert state == 1 31 | 32 | 33 | if __name__ == '__main__': 34 | pytest.main([__file__, '--no-cov']) 35 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_root_is_only_uid_0_account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_fail(): 13 | ## Setup 14 | shutil.copy('/etc/passwd', '/etc/passwd.bak') 15 | shellexec('echo "pytest:x:0:0:PyTest:/home/pytest:/bin/bash" >> /etc/passwd') 16 | 17 | yield None 18 | 19 | ## Tear-down 20 | shutil.move('/etc/passwd.bak', '/etc/passwd') 21 | 22 | 23 | def test_integration_audit_root_is_only_uid_0_account_pass(): 24 | state = CISAudit().audit_root_is_only_uid_0_account() 25 | assert state == 0 26 | 27 | 28 | def test_integration_audit_root_is_only_uid_0_account_fail(setup_to_fail): 29 | state = CISAudit().audit_root_is_only_uid_0_account() 30 | assert state == 1 31 | 32 | 33 | if __name__ == '__main__': 34 | pytest.main([__file__, '--no-cov']) 35 | -------------------------------------------------------------------------------- /tests/unit/test_shellexec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | test = CISAudit() 8 | 9 | 10 | def test_shellexec_stdout_pass(): 11 | result = test._shellexec('echo stdout') 12 | assert result.returncode == 0 13 | assert result.stdout[0] == 'stdout' 14 | assert result.stderr[0] == '' 15 | 16 | 17 | def test_shellexec_sterr_pass(): 18 | result = test._shellexec('echo stderr | tee /dev/stderr 1>/dev/null') 19 | assert result.returncode == 0 20 | assert result.stdout[0] == '' 21 | assert result.stderr[0] == 'stderr' 22 | 23 | 24 | def test_shellexec_sterr_error(): 25 | result = test._shellexec('error pytest') 26 | assert result.returncode == 127 27 | assert result.stderr[0] in ['/bin/sh: error: command not found', '/bin/sh: 1: error: not found'] 28 | assert result.stdout[0] == '' 29 | 30 | 31 | if __name__ == '__main__': 32 | pytest.main([__file__, '--no-cov']) 33 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_audit_config_is_immutable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('echo "-e 2" > /etc/audit/rules.d/99-finalize.rules') 13 | 14 | yield None 15 | 16 | ## Cleanup 17 | shellexec('rm /etc/audit/rules.d/99-finalize.rules') 18 | 19 | 20 | @pytest.fixture() 21 | def setup_to_fail(): 22 | shellexec('rm /etc/audit/rules.d/99-finalize.rules') 23 | 24 | 25 | def test_audit_audit_config_is_immutable_pass(setup_to_pass): 26 | state = CISAudit().audit_audit_config_is_immutable() 27 | assert state == 0 28 | 29 | 30 | def test_audit_audit_config_is_immutable_fail(setup_to_fail): 31 | state = CISAudit().audit_audit_config_is_immutable() 32 | assert state == 1 33 | 34 | 35 | if __name__ == '__main__': 36 | pytest.main([__file__, '--no-cov']) 37 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_rsyslog_default_file_permission_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | print(shellexec("echo '$FileCreateMode 0640' >> /etc/rsyslog.d/pytest.conf")) 15 | 16 | yield None 17 | 18 | ## Tear-down 19 | os.remove('/etc/rsyslog.d/pytest.conf') 20 | 21 | 22 | def test_integration_audit_rsyslog_default_file_permission_is_configured_pass(setup_to_pass): 23 | state = CISAudit().audit_rsyslog_default_file_permission_is_configured() 24 | assert state == 0 25 | 26 | 27 | def test_integration_audit_rsyslog_default_file_permission_is_configured_fail(): 28 | state = CISAudit().audit_rsyslog_default_file_permission_is_configured() 29 | assert state == 1 30 | 31 | 32 | if __name__ == '__main__': 33 | pytest.main([__file__, '--no-cov']) 34 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_audit_logs_not_automatically_deleted.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_pass(): 11 | shellexec('sed -i "s/^.*max_log_file_action =.*/max_log_file_action = keep_logs/" /etc/audit/auditd.conf') 12 | 13 | 14 | @pytest.fixture() 15 | def setup_to_fail(): 16 | shellexec('sed -i "s/^.*max_log_file_action =.*/max_log_file_action = ROTATE/" /etc/audit/auditd.conf') 17 | 18 | 19 | def test_audit_audit_logs_not_automatically_deleted_pass(setup_to_pass): 20 | state = CISAudit().audit_audit_logs_not_automatically_deleted() 21 | assert state == 0 22 | 23 | 24 | def test_audit_audit_logs_not_automatically_deleted_fail(setup_to_fail): 25 | state = CISAudit().audit_audit_logs_not_automatically_deleted() 26 | assert state == 1 27 | 28 | 29 | if __name__ == '__main__': 30 | pytest.main([__file__, '--no-cov']) 31 | -------------------------------------------------------------------------------- /tests/integration/test_integration__shellexec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | test = CISAudit() 8 | 9 | 10 | def test_integration__shellexec_pass_stdout(): 11 | result = test._shellexec('echo stdout') 12 | assert result.returncode == 0 13 | assert result.stdout[0] == 'stdout' 14 | assert result.stderr[0] == '' 15 | 16 | 17 | def test_integration__shellexec_pass_sterr(): 18 | result = test._shellexec('echo stderr | tee /dev/stderr 1>/dev/null') 19 | assert result.returncode == 0 20 | assert result.stdout[0] == '' 21 | assert result.stderr[0] == 'stderr' 22 | 23 | 24 | def test_integration__shellexec_error(): 25 | result = test._shellexec('error pytest') 26 | assert result.returncode == 127 27 | assert result.stderr[0] in ['/bin/sh: error: command not found', '/bin/sh: 1: error: not found'] 28 | assert result.stdout[0] == '' 29 | 30 | 31 | if __name__ == '__main__': 32 | pytest.main([__file__, '--no-cov']) 33 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_homedirs_exist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_fail(): 11 | ## Setup 12 | shellexec('useradd --no-create-home pytest') 13 | shellexec('rm -rf /home/pytest') 14 | 15 | yield None 16 | 17 | ## Tear-down 18 | shellexec('userdel pytest') 19 | 20 | 21 | @pytest.fixture 22 | def setup_to_pass(): 23 | ## Setup 24 | shellexec('useradd pytest') 25 | 26 | yield None 27 | 28 | ## Tear-down 29 | shellexec('userdel pytest') 30 | 31 | 32 | def test_integration_audit_homedirs_exist_fail(setup_to_fail): 33 | state = CISAudit().audit_homedirs_exist() 34 | assert state == 1 35 | 36 | 37 | def test_integration_audit_homedirs_exist_pass(): 38 | state = CISAudit().audit_homedirs_exist() 39 | assert state == 0 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_permissions_on_private_host_key_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('chown -c root.root /etc/ssh/ssh_host_*_key') 13 | shellexec('chmod -c 600 /etc/ssh/ssh_host_*_key') 14 | 15 | yield None 16 | 17 | shellexec('chown -c root.ssh_keys /etc/ssh/ssh_host_*_key') 18 | shellexec('chmod -c 640 /etc/ssh/ssh_host_*_key') 19 | 20 | 21 | def test_integration_audit_permissions_on_private_host_key_files_pass(setup_to_pass): 22 | state = CISAudit().audit_permissions_on_private_host_key_files() 23 | assert state == 0 24 | 25 | 26 | def test_integration_audit_permissions_on_private_host_key_files_fail(): 27 | state = CISAudit().audit_permissions_on_private_host_key_files() 28 | assert state == 7 29 | 30 | 31 | if __name__ == '__main__': 32 | pytest.main([__file__, '--no-cov']) 33 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_mta_is_localhost_only.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_fail(): 13 | shutil.copy('/etc/postfix/main.cf', '/etc/postfix/main.cf.bak') 14 | print(shellexec("sed -i 's/^inet_interfaces = .*/inet_interfaces = all/' /etc/postfix/main.cf")) 15 | print(shellexec('systemctl restart postfix')) 16 | 17 | yield None 18 | 19 | shutil.move('/etc/postfix/main.cf.bak', '/etc/postfix/main.cf') 20 | print(shellexec('systemctl restart postfix')) 21 | 22 | 23 | def test_integration_mta_is_localhost_pass(): 24 | state = CISAudit().audit_mta_is_localhost_only() 25 | assert state == 0 26 | 27 | 28 | def test_integration_mta_is_localhost_fail(setup_to_fail): 29 | state = CISAudit().audit_mta_is_localhost_only() 30 | assert state == 1 31 | 32 | 33 | if __name__ == '__main__': 34 | pytest.main([__file__, '--no-cov']) 35 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_permissions_on_log_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | shellexec('find /var/log -type f -exec chmod g-wx,o-rwx "{}" +') 15 | 16 | yield None 17 | 18 | 19 | @pytest.fixture 20 | def setup_to_fail(): 21 | ## Setup 22 | shellexec('touch /var/log/pytest') 23 | 24 | yield None 25 | 26 | ## Tear-down 27 | os.remove('/var/log/pytest') 28 | 29 | 30 | def test_integration_audit_permissions_on_log_files_are_configured_pass(setup_to_pass): 31 | state = CISAudit().audit_permissions_on_log_files() 32 | assert state == 0 33 | 34 | 35 | def test_integration_audit_permissions_on_log_files_are_configured_fail(setup_to_fail): 36 | state = CISAudit().audit_permissions_on_log_files() 37 | assert state == 1 38 | 39 | 40 | if __name__ == '__main__': 41 | pytest.main([__file__, '--no-cov']) 42 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_journald_configured_to_compress_large_logs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | shutil.copy('/etc/systemd/journald.conf', '/etc/systemd/journald.conf.bak') 14 | shellexec("sed -i 's/.*Compress=.*/Compress=yes/' /etc/systemd/journald.conf") 15 | 16 | yield None 17 | 18 | shutil.move('/etc/systemd/journald.conf.bak', '/etc/systemd/journald.conf') 19 | 20 | 21 | def test_integration_audit_journald_configured_to_compress_large_logs_pass(setup_to_pass): 22 | state = CISAudit().audit_journald_configured_to_compress_large_logs() 23 | assert state == 0 24 | 25 | 26 | def test_integration_audit_journald_configured_to_compress_large_logs_fail(): 27 | state = CISAudit().audit_journald_configured_to_compress_large_logs() 28 | assert state == 1 29 | 30 | 31 | if __name__ == '__main__': 32 | pytest.main([__file__, '--no-cov']) 33 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_xdmcp_is_not_enabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | test = CISAudit() 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_fail(): 13 | ## Setup 14 | shellexec(R"sed -i '/\[xdmcp\]/aEnable=true' /etc/gdm/custom.conf") 15 | 16 | yield None 17 | 18 | ## Tear-down 19 | shellexec(R" sed -i '/^Enable=true/d' /etc/gdm/custom.conf") 20 | 21 | 22 | def test_integration_audit_xdmcp_not_enabled_pass_gdm_installed(setup_install_gdm): 23 | state = test.audit_xdmcp_not_enabled() 24 | assert state == 0 25 | 26 | 27 | def test_integration_audit_xdmcp_not_enabled_fail(setup_install_gdm, setup_to_fail): 28 | state = test.audit_xdmcp_not_enabled() 29 | assert state == 1 30 | 31 | 32 | def test_integration_audit_xdmcp_not_enabled_pass_gdm_not_installed(): 33 | state = test.audit_xdmcp_not_enabled() 34 | assert state == 0 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /bin/centos-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat << EOF > /root/.bash_profile 4 | # .bash_profile 5 | 6 | # Get the aliases and functions 7 | if [ -f ~/.bashrc ]; then 8 | . ~/.bashrc 9 | fi 10 | 11 | # User specific environment and startup programs 12 | 13 | PATH=/root/.local/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin 14 | 15 | export PATH 16 | EOF 17 | 18 | #shellcheck disable=SC1091 19 | source /root/.bash_profile 20 | 21 | yum install -y python3-pip 22 | 23 | cd /vagrant || exit 1 24 | 25 | python3 -m pip install --user --upgrade pip 26 | python3 -m pip install --user pipenv 27 | pipenv lock -r --dev > requirements.txt 28 | 29 | if [ -f requirements.txt ]; then 30 | python3 -m pip install --user -r requirements.txt 31 | fi 32 | 33 | ## Preinstall updates to reduce time certain integration tests take to execute 34 | yum update -y 35 | 36 | ## Preinstall gdm dependencies to reduce time integration tests take to execute 37 | ## I know that we could use `yum deplist` for this, but this was quicker/easier 38 | yum install gdm -y 39 | yum remove gdm -y 40 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_journald_configured_to_send_logs_to_rsyslog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | shutil.copy('/etc/systemd/journald.conf', '/etc/systemd/journald.conf.bak') 14 | shellexec("sed -i 's/.*ForwardToSyslog=.*/ForwardToSyslog=yes/' /etc/systemd/journald.conf") 15 | 16 | yield None 17 | 18 | shutil.move('/etc/systemd/journald.conf.bak', '/etc/systemd/journald.conf') 19 | 20 | 21 | def test_integration_audit_journald_configured_to_send_logs_to_rsyslog_pass(setup_to_pass): 22 | state = CISAudit().audit_journald_configured_to_send_logs_to_rsyslog() 23 | assert state == 0 24 | 25 | 26 | def test_integration_audit_journald_configured_to_send_logs_to_rsyslog_fail(): 27 | state = CISAudit().audit_journald_configured_to_send_logs_to_rsyslog() 28 | assert state == 1 29 | 30 | 31 | if __name__ == '__main__': 32 | pytest.main([__file__, '--no-cov']) 33 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_journald_configured_to_write_logfiles_to_disk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | shutil.copy('/etc/systemd/journald.conf', '/etc/systemd/journald.conf.bak') 14 | shellexec("sed -i 's/.*Storage=.*/Storage=persistent/' /etc/systemd/journald.conf") 15 | 16 | yield None 17 | 18 | shutil.move('/etc/systemd/journald.conf.bak', '/etc/systemd/journald.conf') 19 | 20 | 21 | def test_integration_audit_journald_configured_to_write_logfiles_to_disk_pass(setup_to_pass): 22 | state = CISAudit().audit_journald_configured_to_write_logfiles_to_disk() 23 | assert state == 0 24 | 25 | 26 | def test_integration_audit_journald_configured_to_write_logfiles_to_disk_fail(): 27 | state = CISAudit().audit_journald_configured_to_write_logfiles_to_disk() 28 | assert state == 1 29 | 30 | 31 | if __name__ == '__main__': 32 | pytest.main([__file__, '--no-cov']) 33 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_nxdx_support_enabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_fail(): 13 | ## Setup 14 | with open('/usr/local/bin/dmesg', 'w') as f: 15 | f.writelines( 16 | { 17 | '/bin/dmesg | grep -v "Execute Disable"', 18 | } 19 | ) 20 | os.chmod('/usr/local/bin/dmesg', 755) 21 | print(shellexec('echo $PATH')) 22 | print(shellexec('which dmesg')) 23 | 24 | yield None 25 | 26 | ## Tear-down 27 | os.remove('/usr/local/bin/dmesg') 28 | 29 | 30 | def test_integration_audit_nxdx_support_enabled_pass(): 31 | state = CISAudit().audit_nxdx_support_enabled() 32 | assert state == 0 33 | 34 | 35 | def test_integration_audit_nxdx_support_enabled_fail(setup_to_fail): 36 | state = CISAudit().audit_nxdx_support_enabled() 37 | assert state == 1 38 | 39 | 40 | if __name__ == '__main__': 41 | pytest.main([__file__, '--no-cov']) 42 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_nftables_loopback_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('nft add rule inet filter input iif lo accept') 13 | shellexec('nft add rule inet filter input ip saddr 127.0.0.0/8 counter drop') 14 | shellexec('nft add rule inet filter input ip6 saddr ::1/128 counter drop') 15 | 16 | yield None 17 | 18 | ## Tear-down 19 | shellexec('nft flush chain inet filter input') 20 | 21 | 22 | def test_integration_audit_nftables_loopback_is_configured_pass(setup_install_nftables, setup_to_pass): 23 | state = CISAudit().audit_nftables_loopback_is_configured() 24 | assert state == 0 25 | 26 | 27 | def test_integration_audit_nftables_loopback_is_configured_fail(setup_install_nftables): 28 | state = CISAudit().audit_nftables_loopback_is_configured() 29 | assert state == 7 30 | 31 | 32 | if __name__ == '__main__': 33 | pytest.main([__file__, '--no-cov']) 34 | -------------------------------------------------------------------------------- /tests/unit/test_audit_duplicate_gids.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_duplicate_gids_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | stdout = [''] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_duplicate_gids_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['1000'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_duplicate_gids_pass) 30 | def test_audit_duplicate_gids_pass(): 31 | state = test.audit_duplicate_gids() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_duplicate_gids_fail) 36 | def test_audit_duplicate_gids_fail(): 37 | state = test.audit_duplicate_gids() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_duplicate_uids.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_duplicate_uids_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | stdout = [''] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_duplicate_uids_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['1000'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_duplicate_uids_pass) 30 | def test_audit_duplicate_uids_pass(): 31 | state = test.audit_duplicate_uids() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_duplicate_uids_fail) 36 | def test_audit_duplicate_uids_fail(): 37 | state = test.audit_duplicate_uids() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_core_dumps_restricted.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | shellexec('echo -e "*\thard\tcore\t0" > /etc/security/limits.d/pytest.conf') 14 | shellexec('echo 0 > /proc/sys/fs/suid_dumpable') 15 | shellexec('echo -e "fs.suid_dumpable = 0" > /etc/sysctl.d/pytest.conf') 16 | 17 | yield None 18 | 19 | os.remove('/etc/security/limits.d/pytest.conf') 20 | os.remove('/etc/sysctl.d/pytest.conf') 21 | 22 | 23 | @pytest.fixture() 24 | def setup_to_fail(): 25 | shellexec('echo 1 > /proc/sys/fs/suid_dumpable') 26 | 27 | 28 | def test_integration_audit_core_dumps_restricted_pass_with_tabs(setup_to_pass): 29 | state = CISAudit().audit_core_dumps_restricted() 30 | assert state == 0 31 | 32 | 33 | def test_integration_audit_core_dumps_restricted_fail(setup_to_fail): 34 | state = CISAudit().audit_core_dumps_restricted() 35 | assert state == 7 36 | 37 | 38 | if __name__ == '__main__': 39 | pytest.main([__file__, '--no-cov']) 40 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_ntp_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | print(shellexec('yum install -q -y ntp')) 14 | shutil.copy('/etc/ntp.conf', '/etc/ntp.conf.bak') 15 | shellexec("sed -i 's/restrict default.*/restrict default kod nomodify notrap nopeer noquery/' /etc/ntp.conf") 16 | 17 | print(shellexec('systemctl enable ntpd')) 18 | print(shellexec('systemctl start ntpd')) 19 | 20 | yield None 21 | 22 | print(shellexec('yum remove -q -y ntp')) 23 | print(shellexec('systemctl disable ntpd')) 24 | print(shellexec('systemctl start chronyd')) 25 | 26 | 27 | def test_integration_audit_ntp_is_configured_pass(setup_to_pass): 28 | state = CISAudit().audit_ntp_is_configured() 29 | assert state == 0 30 | 31 | 32 | def test_integration_audit_ntp_is_configured_fail(): 33 | state = CISAudit().audit_ntp_is_configured() 34 | assert state == 31 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_sysctl_flags_are_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | sysctl_flags = [ 10 | "net.ipv6.conf.all.disable_ipv6", 11 | "net.ipv6.conf.default.disable_ipv6", 12 | ] 13 | 14 | 15 | @pytest.fixture 16 | def setup_to_pass(): 17 | ## Setup 18 | with open('/etc/sysctl.d/pytest.conf', 'w') as f: 19 | f.writelines( 20 | [ 21 | 'net.ipv6.conf.all.disable_ipv6 = 0\n', 22 | 'net.ipv6.conf.default.disable_ipv6 = 0\n', 23 | ] 24 | ) 25 | 26 | yield None 27 | 28 | ## Tear-down 29 | os.remove('/etc/sysctl.d/pytest.conf') 30 | 31 | 32 | def test_integration_audit_sysctl_flags_are_set_pass(setup_to_pass): 33 | state = CISAudit().audit_sysctl_flags_are_set(flags=sysctl_flags, value=0) 34 | assert state == 0 35 | 36 | 37 | def test_integration_audit_sysctl_flags_are_set_fail(): 38 | state = CISAudit().audit_sysctl_flags_are_set(flags=sysctl_flags, value=1) 39 | assert state == 15 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/unit/test_output_json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | results = [ 8 | ('1', 'section header'), 9 | ('1.1', 'subsection header'), 10 | ('1.1.1', 'test 1.1.1', 1, 'Pass', '1ms'), 11 | ('2', 'section header'), 12 | ('2.1', 'test 2.1', 1, 'Fail', '10ms'), 13 | ('2.2', 'test 2.2', 2, 'Pass', '100ms'), 14 | ('2.3', 'test 2.3', 1, 'Not Implemented'), 15 | ] 16 | 17 | 18 | def test_output_json(capsys): 19 | CISAudit().output_json(data=results) 20 | 21 | output, error = capsys.readouterr() 22 | assert error == '' 23 | assert output == '{"1": {"description": "section header"}, "1.1": {"description": "subsection header"}, "1.1.1": {"description": "test 1.1.1", "level": 1, "result": "Pass", "duration": "1ms"}, "2": {"description": "section header"}, "2.1": {"description": "test 2.1", "level": 1, "result": "Fail", "duration": "10ms"}, "2.2": {"description": "test 2.2", "level": 2, "result": "Pass", "duration": "100ms"}, "2.3": {"description": "test 2.3", "level": 1, "result": "Not Implemented"}}\n' 24 | 25 | 26 | if __name__ == '__main__': 27 | pytest.main([__file__, '--no-cov']) 28 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_at_is_restricted_to_authorized_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('rm /etc/at.deny') 13 | shellexec('install -m 0600 /dev/null /etc/at.allow') 14 | 15 | yield None 16 | 17 | ## Cleanup 18 | shellexec('rm /etc/at.allow') 19 | 20 | 21 | @pytest.fixture() 22 | def setup_to_fail(): 23 | ## Setup 24 | shellexec('touch /etc/at.deny') 25 | shellexec('rm /etc/at.allow') 26 | 27 | yield None 28 | 29 | ## Cleanup 30 | shellexec('/etc/at.deny') 31 | 32 | 33 | def test_integrate_audit_at_is_restricted_to_authorized_users_pass(setup_to_pass): 34 | state = CISAudit().audit_at_is_restricted_to_authorized_users() 35 | assert state == 0 36 | 37 | 38 | def test_integrate_audit_at_is_restricted_to_authorized_users_fail(setup_to_fail): 39 | state = CISAudit().audit_at_is_restricted_to_authorized_users() 40 | assert state == 3 41 | 42 | 43 | if __name__ == '__main__': 44 | pytest.main([__file__, '--no-cov']) 45 | -------------------------------------------------------------------------------- /tests/unit/test_audit_duplicate_user_names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_duplicate_user_names_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | stdout = [''] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_duplicate_user_names_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['pytest'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_duplicate_user_names_pass) 30 | def test_audit_duplicate_user_names_pass(): 31 | state = test.audit_duplicate_user_names() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_duplicate_user_names_fail) 36 | def test_audit_duplicate_user_names_fail(): 37 | state = test.audit_duplicate_user_names() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_nftables_table_exists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_nftables_table_exists_pass(self, cmd): 12 | stdout = ['table inet filter'] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_nftables_table_exists_fail(self, cmd): 20 | stdout = [''] 21 | stderr = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | @patch.object(CISAudit, "_shellexec", mock_nftables_table_exists_pass) 28 | def test_audit_nftables_table_exists_pass(): 29 | state = CISAudit().audit_nftables_table_exists() 30 | assert state == 0 31 | 32 | 33 | @patch.object(CISAudit, "_shellexec", mock_nftables_table_exists_fail) 34 | def test_audit_nftables_table_exists_fail(): 35 | state = CISAudit().audit_nftables_table_exists() 36 | assert state == 1 37 | 38 | 39 | if __name__ == '__main__': 40 | pytest.main([__file__, '--no-cov']) 41 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_homedirs_ownership.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('useradd pytest') 13 | 14 | yield None 15 | 16 | ## Tear-down 17 | shellexec('userdel pytest') 18 | shellexec('rm -rf /home/pytest') 19 | 20 | 21 | @pytest.fixture 22 | def setup_to_fail(): 23 | ## Setup 24 | shellexec('useradd pytest') 25 | shellexec('chown root. /home/pytest') 26 | 27 | yield None 28 | 29 | ## Tear-down 30 | shellexec('userdel pytest') 31 | shellexec('rm -rf /home/pytest') 32 | 33 | 34 | def test_integration_audit_homedirs_ownership_fail(setup_to_fail): 35 | state = CISAudit().audit_homedirs_ownership() 36 | assert state == 1 37 | 38 | 39 | def test_integration_audit_homedirs_ownership_pass(setup_to_pass): 40 | state = CISAudit().audit_homedirs_ownership() 41 | assert state == 0 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '-v', '--no-cov', '-W', 'ignore:Module already imported:pytest.PytestWarning']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_duplicate_group_names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_duplicate_group_names_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | stdout = [''] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_duplicate_group_names_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['pytest'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_duplicate_group_names_pass) 30 | def test_audit_duplicate_group_names_pass(): 31 | state = test.audit_duplicate_group_names() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_duplicate_group_names_fail) 36 | def test_audit_duplicate_group_names_fail(): 37 | state = test.audit_duplicate_group_names() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_default_group_for_root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_default_group_for_root_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | stdout = ['0'] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_default_group_for_root_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['1'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_default_group_for_root_pass) 30 | def test_audit_default_group_for_root_pass(): 31 | state = test.audit_default_group_for_root() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_default_group_for_root_fail) 36 | def test_audit_default_group_for_root_fail(): 37 | state = test.audit_default_group_for_root() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_mta_is_localhost_only.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_mta_pass(self, cmd): 12 | stdout = [''] 13 | stderr = [''] 14 | returncode = 1 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_mta_fail(self, cmd): 20 | stdout = ['0.0.0.0:25'] 21 | stderr = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | class TestMTAIsLocalhost: 28 | test = CISAudit() 29 | test_id = '1.1' 30 | 31 | @patch.object(CISAudit, "_shellexec", mock_mta_pass) 32 | def test_mta_is_localhost_pass(self): 33 | state = self.test.audit_mta_is_localhost_only() 34 | assert state == 0 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_mta_fail) 37 | def test_mta_is_localhost_fail(self): 38 | state = self.test.audit_mta_is_localhost_only() 39 | assert state == 1 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_for_changes_to_sysadmin_scope_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | rules = [ 14 | '-w /etc/sudoers -p wa -k scope', 15 | '-w /etc/sudoers.d -p wa -k scope', 16 | ] 17 | 18 | for rule in rules: 19 | shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules') 20 | shellexec(f'auditctl {rule}') 21 | 22 | yield None 23 | 24 | os.remove('/etc/audit/rules.d/pytest.rules') 25 | shellexec('auditctl -D') 26 | 27 | 28 | def test_integration_audit_events_for_changes_to_sysadmin_scope_are_collected_pass(setup_to_pass): 29 | state = CISAudit().audit_events_for_changes_to_sysadmin_scope_are_collected() 30 | assert state == 0 31 | 32 | 33 | def test_integration_audit_events_for_changes_to_sysadmin_scope_are_collected_fail(): 34 | state = CISAudit().audit_events_for_changes_to_sysadmin_scope_are_collected() 35 | assert state == 3 36 | 37 | 38 | if __name__ == '__main__': 39 | pytest.main([__file__, '--no-cov']) 40 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_password_change_minimum_delay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | shutil.copy('/etc/login.defs', '/etc/login.defs.bak') 15 | shutil.copy('/etc/shadow', '/etc/shadow.bak') 16 | 17 | shellexec(R"sed -i 's/^\s*PASS_MIN_DAYS.*/PASS_MIN_DAYS 7/' /etc/login.defs") 18 | shellexec("sed -i -E '/(root|vagrant):/ s/0:99999/7:99999/' /etc/shadow") 19 | 20 | yield None 21 | 22 | ## Tear-down 23 | shutil.move('/etc/login.defs.bak', '/etc/login.defs') 24 | shutil.move('/etc/shadow.bak', '/etc/shadow') 25 | 26 | 27 | def test_integration_audit_password_expiration_min_days_is_configured_pass(setup_to_pass): 28 | state = CISAudit().audit_password_change_minimum_delay() 29 | assert state == 0 30 | 31 | 32 | def test_integration_audit_password_expiration_min_days_is_configured_fail(): 33 | state = CISAudit().audit_password_change_minimum_delay() 34 | assert state == 3 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_password_hashing_algoritm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_fail(): 13 | ## Setup 14 | shutil.copy('/etc/pam.d/system-auth', '/etc/pam.d/system-auth.bak') 15 | shutil.copy('/etc/pam.d/password-auth', '/etc/pam.d/password-auth.bak') 16 | 17 | shellexec("sed -i 's/sha512/md5/' /etc/pam.d/system-auth") 18 | shellexec("sed -i 's/sha512/md5/' /etc/pam.d/password-auth") 19 | 20 | yield None 21 | 22 | ## Tear-down 23 | shutil.move('/etc/pam.d/system-auth.bak', '/etc/pam.d/system-auth') 24 | shutil.move('/etc/pam.d/password-auth.bak', '/etc/pam.d/password-auth') 25 | 26 | 27 | def test_integration_audit_password_hashing_algorithm_pass(): 28 | state = CISAudit().audit_password_hashing_algorithm() 29 | assert state == 0 30 | 31 | 32 | def test_integration_audit_password_hashing_algorithm_fail(setup_to_fail): 33 | state = CISAudit().audit_password_hashing_algorithm() 34 | assert state == 1 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /tests/unit/test_audit_no_unconfined_services.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_unconfined_services_pass(self, cmd): 12 | stdout = [''] 13 | stderr = [''] 14 | returncode = 1 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_unconfined_services_fail(self, cmd): 20 | stdout = ['system_u:system_r:unconfined_service_t:s0 720 ? 00:03:07 VBoxService'] 21 | stderr = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | @patch.object(CISAudit, "_shellexec", mock_unconfined_services_pass) 28 | def test_no_unconfined_services_pass(): 29 | state = CISAudit().audit_no_unconfined_services() 30 | assert state == 0 31 | 32 | 33 | @patch.object(CISAudit, "_shellexec", mock_unconfined_services_fail) 34 | def test_no_unconfined_services_fail(): 35 | state = CISAudit().audit_no_unconfined_services() 36 | assert state == 1 37 | 38 | 39 | if __name__ == '__main__': 40 | pytest.main([__file__, '--no-cov']) 41 | -------------------------------------------------------------------------------- /tests/unit/test_audit_firewalld_default_zone_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_firewalld_default_zone_is_set(*args): 12 | output = ['public', ''] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_firewalld_not_running(self, cmd): 20 | output = [''] 21 | error = ['FirewallD is not running'] 22 | returncode = 252 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | test = CISAudit() 28 | 29 | 30 | @patch.object(CISAudit, "_shellexec", mock_firewalld_default_zone_is_set) 31 | def test_firewalld_defaullt_zone_set_pass(): 32 | state = test.audit_firewalld_default_zone_is_set() 33 | assert state == 0 34 | 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_firewalld_not_running) 37 | def test_firewalld_not_running(): 38 | state = test.audit_firewalld_default_zone_is_set() 39 | assert state == 1 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/unit/test_audit_iptables_is_flushed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_iptables_is_flushed_pass(self, cmd, **kwargs): 12 | output = [''] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_iptables_is_flushed_fail(self, cmd, **kwargs): 20 | output = [ 21 | '-A INPUT -i lo -j ACCEPT', 22 | ] 23 | error = [''] 24 | returncode = 1 25 | 26 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 27 | 28 | 29 | test = CISAudit() 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_iptables_is_flushed_pass) 33 | def test_iptables_is_flushed_pass(): 34 | state = test.audit_iptables_is_flushed() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_iptables_is_flushed_fail) 39 | def test_iptables_is_flushed_fail(): 40 | state = test.audit_iptables_is_flushed() 41 | assert state == 3 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_password_inactive_lock_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | shutil.copy('/etc/default/useradd', '/etc/default/useradd.bak') 15 | shutil.copy('/etc/shadow', '/etc/shadow.bak') 16 | 17 | shellexec("sed -i '/INACTIVE/ s/=.*/=30/' /etc/default/useradd") 18 | shellexec("sed -i -E '/(root|vagrant):/ s/0:99999:7::/0:99999:7:30:/' /etc/shadow") 19 | 20 | yield None 21 | 22 | ## Tear-down 23 | shutil.move('/etc/default/useradd.bak', '/etc/default/useradd') 24 | shutil.move('/etc/shadow.bak', '/etc/shadow') 25 | 26 | 27 | def test_integration_audit_password_inactive_lock_is_configured_pass(setup_to_pass): 28 | state = CISAudit().audit_password_inactive_lock_is_configured() 29 | assert state == 0 30 | 31 | 32 | def test_integration_audit_password_inactive_lock_is_configured_pass_fail(): 33 | state = CISAudit().audit_password_inactive_lock_is_configured() 34 | assert state == 3 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_updates_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | shellexec('yum update -y') 14 | 15 | yield None 16 | 17 | 18 | @pytest.fixture 19 | def setup_to_fail(): 20 | shellexec('yum downgrade -y linux-firmware') 21 | 22 | yield None 23 | 24 | 25 | @pytest.fixture 26 | def setup_to_error(): 27 | shutil.move('/etc/yum.repos.d', '/etc/yum.repos.d.bak') 28 | 29 | yield None 30 | 31 | shutil.move('/etc/yum.repos.d.bak', '/etc/yum.repos.d') 32 | 33 | 34 | def test_integration_audit_updates_installed_pass(setup_to_pass): 35 | state = CISAudit().audit_updates_installed() 36 | assert state == 0 37 | 38 | 39 | def test_integration_audit_updates_installed_fail(setup_to_fail): 40 | state = CISAudit().audit_updates_installed() 41 | assert state == 1 42 | 43 | 44 | def test_integration_audit_updates_installed_error(setup_to_error): 45 | state = CISAudit().audit_updates_installed() 46 | assert state == -1 47 | 48 | 49 | if __name__ == '__main__': 50 | pytest.main([__file__, '--no-cov']) 51 | -------------------------------------------------------------------------------- /tests/unit/test_audit_root_is_only_uid_0_account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_root_is_only_uid_0_account_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | stdout = ['root'] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_root_is_only_uid_0_account_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['root', 'pytest'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_root_is_only_uid_0_account_pass) 30 | def test_audit_root_is_only_uid_0_account_pass(): 31 | state = test.audit_root_is_only_uid_0_account() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_root_is_only_uid_0_account_fail) 36 | def test_audit_root_is_only_uid_0_account_fail(): 37 | state = test.audit_root_is_only_uid_0_account() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_bootloader_password_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | test = CISAudit() 11 | 12 | 13 | @pytest.fixture 14 | def setup_to_pass(): 15 | ## Setup 16 | ## Get the value by running 'grub2-setpassword', then checking /boot/grub2/user.cfg 17 | shellexec('echo GRUB2_PASSWORD=grub.pbkdf2.sha512.10000.A03A140DBAA0676BF9597209D32653B5A47D0C51C6EA7EDBD6648337E6DA881C70AD1E043AA4A2C3A10EB8D244DD9E346109C5EC732124E165DF59839F8119DB.10AC2C6980F4ABDBDBEDA4FF8C624A0DF1FAB61786B1C87B67219BCD26BAA363CB475D116F2050585CC47AB6CA6C9676F22D8084653D87EB0B4A6A6FC76E393D > /boot/grub2/user.cfg') 18 | 19 | yield None 20 | 21 | ## Tear-down 22 | os.remove('/boot/grub2/user.cfg') 23 | 24 | 25 | def test_integration_audit_bootloader_password_set_pass(setup_to_pass): 26 | state = test.audit_bootloader_password_is_set() 27 | assert state == 0 28 | 29 | 30 | def test_integration_audit_bootloader_password_set_fail(): 31 | state = test.audit_bootloader_password_is_set() 32 | assert state == 1 33 | 34 | 35 | if __name__ == '__main__': 36 | pytest.main([__file__, '--no-cov']) 37 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_password_change_max_days_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | shutil.copy('/etc/login.defs', '/etc/login.defs.bak') 15 | shutil.copy('/etc/shadow', '/etc/shadow.bak') 16 | 17 | shellexec(R"sed -i 's/^\s*PASS_MAX_DAYS.*/PASS_MAX_DAYS 365/' /etc/login.defs") 18 | shellexec("sed -i -E '/(root|vagrant):/ s/0:99999/0:365/' /etc/shadow") 19 | 20 | yield None 21 | 22 | ## Tear-down 23 | shutil.move('/etc/login.defs.bak', '/etc/login.defs') 24 | shutil.move('/etc/shadow.bak', '/etc/shadow') 25 | 26 | 27 | def test_integration_audit_password_expiration_max_days_is_configured_pass(setup_to_pass): 28 | state = CISAudit().audit_password_expiration_max_days_is_configured() 29 | assert state == 0 30 | 31 | 32 | def test_integration_audit_password_expiration_max_days_is_configured_fail(): 33 | state = CISAudit().audit_password_expiration_max_days_is_configured() 34 | assert state == 3 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_password_expiration_warning_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_fail(): 13 | ## Setup 14 | shutil.copy('/etc/login.defs', '/etc/login.defs.bak') 15 | shutil.copy('/etc/shadow', '/etc/shadow.bak') 16 | 17 | shellexec(R"sed -i 's/^\s*PASS_WARN_AGE.*/PASS_WARN_AGE 0/' /etc/login.defs") 18 | shellexec("sed -i -E '/(root|vagrant):/ s/0:99999:7/0:99999:0/' /etc/shadow") 19 | 20 | yield None 21 | 22 | ## Tear-down 23 | shutil.move('/etc/login.defs.bak', '/etc/login.defs') 24 | shutil.move('/etc/shadow.bak', '/etc/shadow') 25 | 26 | 27 | def test_integration_audit_password_expiration_warning_is_configured_pass(): 28 | state = CISAudit().audit_password_expiration_warning_is_configured() 29 | assert state == 0 30 | 31 | 32 | def test_integration_audit_password_expiration_warning_is_configured_fail(setup_to_fail): 33 | state = CISAudit().audit_password_expiration_warning_is_configured() 34 | assert state == 3 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /tests/unit/test_audit_sudo_log_exists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_sudo_log_exists_pass(*args, **kwargs): 12 | output = ['Defaults logfile="/var/log/sudo.log"'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_sudo_log_exists_fail(*args, **kwargs): 20 | output = [''] 21 | error = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | class TestSudoCommandUsePty: 28 | test = CISAudit() 29 | test_id = '1.1' 30 | 31 | @patch.object(CISAudit, "_shellexec", mock_sudo_log_exists_pass) 32 | def test_sudo_log_exists_pass(self): 33 | state = self.test.audit_sudo_log_exists() 34 | assert state == 0 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_sudo_log_exists_fail) 37 | def test_sudo_log_exists_fail(self): 38 | state = self.test.audit_sudo_log_exists() 39 | assert state == 1 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_kernel_module_is_disabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | shellexec('echo "install cramfs /bin/true" > /etc/modprobe.d/cramfs.conf') 14 | 15 | yield None 16 | 17 | os.remove('/etc/modprobe.d/cramfs.conf') 18 | 19 | 20 | @pytest.fixture 21 | def setup_to_fail(): 22 | print(shellexec('modprobe -v cramfs')) 23 | 24 | yield None 25 | 26 | print(shellexec('rmmod cramfs')) 27 | 28 | 29 | def test_integration_audit_kernel_module_is_disabled_pass_disabled(setup_to_pass): 30 | state = CISAudit().audit_kernel_module_is_disabled(module='cramfs') 31 | assert state == 0 32 | 33 | 34 | def test_integration_audit_kernel_module_is_disabled_pass_not_found(): 35 | state = CISAudit().audit_kernel_module_is_disabled(module='pytest') 36 | assert state == 0 37 | 38 | 39 | def test_integration_audit_kernel_module_is_disabled_fail(setup_to_fail): 40 | state = CISAudit().audit_kernel_module_is_disabled(module='cramfs') 41 | assert state == 2 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_audit_log_size_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_audit_log_size_is_configured_pass(self, cmd): 12 | stdout = ['max_log_file = 8', ''] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_audit_log_size_is_configured_fail(self, cmd): 20 | stdout = [''] 21 | stderr = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | test = CISAudit() 28 | 29 | 30 | @patch.object(CISAudit, "_shellexec", mock_audit_log_size_is_configured_pass) 31 | def test_audit_audit_log_size_is_configured_pass(): 32 | state = test.audit_audit_log_size_is_configured() 33 | assert state == 0 34 | 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_audit_log_size_is_configured_fail) 37 | def test_audit_audit_log_size_is_configured_fail(): 38 | state = test.audit_audit_log_size_is_configured() 39 | assert state == 1 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/unit/test_audit_service_enabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_disabled(*args, **kwargs): 12 | output = ['disabled'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_enabled(*args, **kwargs): 20 | output = ['enabled'] 21 | error = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | class TestService: 28 | test = CISAudit() 29 | test_id = '1.1' 30 | test_service = 'pytest' 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_enabled) 33 | def test_service_enabled_pass(self): 34 | state = self.test.audit_service_is_enabled(service=self.test_service) 35 | assert state == 0 36 | 37 | @patch.object(CISAudit, "_shellexec", mock_disabled) 38 | def test_service_enabled_fail(self): 39 | state = self.test.audit_service_is_enabled(service=self.test_service) 40 | assert state == 1 41 | 42 | 43 | if __name__ == '__main__': 44 | pytest.main([__file__, '--no-cov']) 45 | -------------------------------------------------------------------------------- /tests/unit/test_audit_audit_config_is_immutable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_audit_config_is_immutable_pass(self, cmd): 14 | stdout = [ 15 | '-e 2', 16 | '', 17 | ] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_audit_audit_config_is_immutable_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_audit_config_is_immutable_pass) 33 | def test_audit_audit_config_is_immutable_pass(): 34 | state = test.audit_audit_config_is_immutable() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_audit_config_is_immutable_fail) 39 | def test_audit_audit_config_is_immutable_fail(): 40 | state = test.audit_audit_config_is_immutable() 41 | assert state == 1 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_nxdx_support_enabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_nxdx_support_pass(self, cmd): 12 | stdout = ['[ 0.000000] NX (Execute Disable) protection: active'] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_nxdx_support_fail(self, cmd): 20 | stdout = [''] 21 | stderr = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | class TestNXDXSupportEnabled: 28 | test = CISAudit() 29 | test_id = '1.1' 30 | 31 | @patch.object(CISAudit, "_shellexec", mock_nxdx_support_pass) 32 | def test_nxdx_support_enabled_pass(self): 33 | state = self.test.audit_nxdx_support_enabled() 34 | assert state == 0 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_nxdx_support_fail) 37 | def test_nxdx_support_enabled_fail(self): 38 | state = self.test.audit_nxdx_support_enabled() 39 | assert state == 1 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_for_login_and_logout_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | rules = [ 14 | '-w /var/log/lastlog -p wa -k logins', 15 | '-w /var/run/faillock -p wa -k logins', 16 | ] 17 | 18 | for rule in rules: 19 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 20 | print(shellexec(f'auditctl {rule}')) 21 | 22 | yield None 23 | 24 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 25 | print(shellexec('auditctl -l')) 26 | 27 | os.remove('/etc/audit/rules.d/pytest.rules') 28 | shellexec('auditctl -D') 29 | 30 | 31 | def test_integration_audit_events_for_login_and_logout_are_collected_pass(setup_to_pass): 32 | state = CISAudit().audit_events_for_login_and_logout_are_collected() 33 | assert state == 0 34 | 35 | 36 | def test_integration_audit_events_for_login_and_logout_are_collected_fail(): 37 | state = CISAudit().audit_events_for_login_and_logout_are_collected() 38 | assert state == 3 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_selinux_policy_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import shutil 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | from tests.integration import shellexec 10 | 11 | 12 | @pytest.fixture(params=['minimum', 'mls']) 13 | def setup_to_fail(request): 14 | ## Setup 15 | shutil.copy('/etc/selinux/config', '/etc/selinux/config.bak') 16 | shellexec(Rf"sed -i '/SELINUXTYPE=/ s/=.*$/{request.param}/' /etc/selinux/config") 17 | 18 | with open('/usr/local/sbin/sestatus', 'w') as f: 19 | f.write(f'echo Loaded policy name: {request.param}') 20 | shellexec('chmod +x /usr/local/sbin/sestatus') 21 | 22 | yield None 23 | 24 | ## Tear-down 25 | shutil.move('/etc/selinux/config.bak', '/etc/selinux/config') 26 | os.remove('/usr/local/sbin/sestatus') 27 | 28 | 29 | def test_integration_audit_selinux_policy_configured_pass(): 30 | state = CISAudit().audit_selinux_policy_is_configured() 31 | assert state == 0 32 | 33 | 34 | def test_integration_audit_selinux_policy_configured_fail(setup_to_fail): 35 | state = CISAudit().audit_selinux_policy_is_configured() 36 | assert state == 3 37 | 38 | 39 | if __name__ == '__main__': 40 | pytest.main([__file__, '--no-cov']) 41 | -------------------------------------------------------------------------------- /tests/unit/test_audit_auth_for_single_user_mode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_command_pass(*args, **kwargs): 12 | stdout = ['ExecStart=-/bin/sh -c "/sbin/sulogin; /usr/bin/systemctl --fail --no-block default"'] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_command_fail(*args, **kwargs): 20 | stdout = [''] 21 | stderr = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | class TestAuthForSingleUserMode: 28 | test = CISAudit() 29 | 30 | @patch.object(CISAudit, "_shellexec", mock_command_pass) 31 | def test_auth_for_single_user_pass(self): 32 | state = self.test.audit_auth_for_single_user_mode() 33 | assert state == 0 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_command_fail) 36 | def test_auth_for_single_user_fail(self): 37 | state = self.test.audit_auth_for_single_user_mode() 38 | assert state == 3 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_homedirs_permissions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture(params=[750, 700]) 10 | def setup_to_pass(request): 11 | ## Setup 12 | shellexec('useradd pytest') 13 | shellexec(f'chmod {request.param} /home/pytest') 14 | 15 | yield None 16 | 17 | ## Tear-down 18 | shellexec('userdel pytest') 19 | shellexec('rm -rf /home/pytest') 20 | 21 | 22 | @pytest.fixture(params=[755, 770]) 23 | def setup_to_fail(request): 24 | ## Setup 25 | shellexec('useradd pytest') 26 | shellexec(f'chmod {request.param} /home/pytest') 27 | 28 | yield None 29 | 30 | ## Tear-down 31 | shellexec('userdel pytest') 32 | shellexec('rm -rf /home/pytest') 33 | 34 | 35 | def test_integration_audit_homedirs_permissions_pass(setup_to_pass): 36 | state = CISAudit().audit_homedirs_permissions() 37 | assert state == 0 38 | 39 | 40 | def test_integration_audit_homedirs_permissions_fail(setup_to_fail): 41 | state = CISAudit().audit_homedirs_permissions() 42 | assert state == 1 43 | 44 | 45 | if __name__ == '__main__': 46 | pytest.main([__file__, '--no-cov', '-W', 'ignore:Module already imported:pytest.PytestWarning']) 47 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_auth_for_single_user_mode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_fail(): 13 | ## Create copy of original file before modification 14 | shutil.copy('/usr/lib/systemd/system/rescue.service', '/usr/lib/systemd/system/rescue.service.bak') 15 | 16 | ## Update the file 17 | shellexec("sed -i -- '/^ExecStart=/ s|^.*|ExecStart=-/bin/sh -c \"/usr/sbin/sulogin; /usr/bin/systemctl --no-block default\"|' /usr/lib/systemd/system/rescue.service") 18 | print(shellexec('grep ExecStart= /usr/lib/systemd/system/rescue.service')) 19 | 20 | yield None 21 | 22 | ## Replace modified file with original 23 | shutil.move('/usr/lib/systemd/system/rescue.service.bak', '/usr/lib/systemd/system/rescue.service') 24 | 25 | 26 | def test_integrate_auth_for_single_user_mode_pass(): 27 | state = CISAudit().audit_auth_for_single_user_mode() 28 | assert state == 0 29 | 30 | 31 | def test_integrate_auth_for_single_user_mode_fail(setup_to_fail): 32 | state = CISAudit().audit_auth_for_single_user_mode() 33 | assert state == 3 34 | 35 | 36 | if __name__ == '__main__': 37 | pytest.main([__file__, '--no-cov']) 38 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_log_size_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | import shutil 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | from tests.integration import shellexec 10 | 11 | 12 | @pytest.fixture() 13 | def setup_to_pass(): 14 | shutil.copy('/etc/audit/auditd.conf', '/etc/audit/auditd.conf.bak') 15 | shellexec('sed -i "s/^.*max_log_file =/max_log_file =/" /etc/audit/auditd.conf') 16 | 17 | yield None 18 | 19 | shutil.move('/etc/audit/auditd.conf.bak', '/etc/audit/auditd.conf') 20 | 21 | 22 | @pytest.fixture() 23 | def setup_to_fail(): 24 | shutil.copy('/etc/audit/auditd.conf', '/etc/audit/auditd.conf.bak') 25 | shellexec('sed -i "s/^.*max_log_file =/#max_log_file =/" /etc/audit/auditd.conf') 26 | 27 | yield None 28 | 29 | shutil.move('/etc/audit/auditd.conf.bak', '/etc/audit/auditd.conf') 30 | 31 | 32 | def test_integrate_audit_audit_log_size_is_configured_pass(setup_to_pass): 33 | state = CISAudit().audit_audit_log_size_is_configured() 34 | assert state == 0 35 | 36 | 37 | def test_integrate_audit_audit_log_size_is_configured_fail(setup_to_fail): 38 | state = CISAudit().audit_audit_log_size_is_configured() 39 | assert state == 1 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_password_reuse_is_limited.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | shutil.copy('/etc/pam.d/system-auth', '/etc/pam.d/system-auth.bak') 15 | shutil.copy('/etc/pam.d/password-auth', '/etc/pam.d/password-auth.bak') 16 | 17 | shellexec(R"sed -i '/password\s*sufficient\s*pam_unix.so/ s/sha512/sha512 remember=5/' /etc/pam.d/system-auth") 18 | shellexec(R"sed -i '/password\s*sufficient\s*pam_unix.so/ s/sha512/sha512 remember=5/' /etc/pam.d/password-auth") 19 | 20 | yield None 21 | 22 | ## Tear-down 23 | shutil.move('/etc/pam.d/system-auth.bak', '/etc/pam.d/system-auth') 24 | shutil.move('/etc/pam.d/password-auth.bak', '/etc/pam.d/password-auth') 25 | 26 | 27 | def test_integration_audit_password_reuse_is_limited_pass(setup_to_pass): 28 | state = CISAudit().audit_password_reuse_is_limited() 29 | assert state == 0 30 | 31 | 32 | def test_integration_audit_password_reuse_is_limited_pass_fail(): 33 | state = CISAudit().audit_password_reuse_is_limited() 34 | assert state == 1 35 | 36 | 37 | if __name__ == '__main__': 38 | pytest.main([__file__, '--no-cov']) 39 | -------------------------------------------------------------------------------- /tests/unit/test_audit_xdmcp_is_not_enabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from types import SimpleNamespace 5 | from unittest.mock import patch 6 | 7 | import pytest 8 | 9 | from cis_audit import CISAudit 10 | 11 | test = CISAudit() 12 | 13 | 14 | def mock_xdmcp_not_enabled_pass(*args, **kwargs): 15 | stdout = [''] 16 | stderr = [''] 17 | returncode = 1 18 | 19 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 20 | 21 | 22 | def mock_xdmcp_not_enabled_fail(*args, **kwargs): 23 | stdout = ['Enabled=true'] 24 | stderr = [''] 25 | returncode = 0 26 | 27 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 28 | 29 | 30 | def mock_os_path_exists_pass(file): 31 | return True 32 | 33 | 34 | @patch.object(CISAudit, "_shellexec", mock_xdmcp_not_enabled_pass) 35 | def test_audit_xdmcp_not_enabled_pass(): 36 | state = test.audit_xdmcp_not_enabled() 37 | assert state == 0 38 | 39 | 40 | @patch.object(os.path, "exists", mock_os_path_exists_pass) 41 | @patch.object(CISAudit, "_shellexec", mock_xdmcp_not_enabled_fail) 42 | def test_audit_xdmcp_not_enabled_fail(): 43 | state = test.audit_xdmcp_not_enabled() 44 | assert state == 1 45 | 46 | 47 | if __name__ == '__main__': 48 | pytest.main([__file__, '--no-cov']) 49 | -------------------------------------------------------------------------------- /tests/unit/test_audit_selinux_policy_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_selinux_policy_configured_pass(self, cmd): 12 | stdout = ['targeted'] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_selinux_policy_configured_fail(self, cmd): 20 | stdout = [''] 21 | stderr = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | class TestSELinuxPolicyConfigured: 28 | test = CISAudit() 29 | test_id = '1.1' 30 | 31 | @patch.object(CISAudit, "_shellexec", mock_selinux_policy_configured_pass) 32 | def test_selinux_policy_configured_pass(self): 33 | state = self.test.audit_selinux_policy_is_configured() 34 | assert state == 0 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_selinux_policy_configured_fail) 37 | def test_selinux_policy_configured_fail(self): 38 | state = self.test.audit_selinux_policy_is_configured() 39 | assert state == 3 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/unit/test_audit_sudo_commands_use_pty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_sudo_use_pty_pass(*args, **kwargs): 12 | output = ['Defaults use_pty'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_sudo_use_pty_fail(*args, **kwargs): 20 | output = [''] 21 | error = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_sudo_use_pty_error(*args, **kwargs): 28 | raise Exception 29 | 30 | 31 | class TestSudoCommandUsePty: 32 | test = CISAudit() 33 | test_id = '1.1' 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_sudo_use_pty_pass) 36 | def test_sudo_use_pty_pass(self): 37 | state = self.test.audit_sudo_commands_use_pty() 38 | assert state == 0 39 | 40 | @patch.object(CISAudit, "_shellexec", mock_sudo_use_pty_fail) 41 | def test_sudo_use_pty_fail(self): 42 | state = self.test.audit_sudo_commands_use_pty() 43 | assert state == 1 44 | 45 | 46 | if __name__ == '__main__': 47 | pytest.main([__file__, '--no-cov']) 48 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_for_session_initiation_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | rules = [ 14 | '-w /var/run/utmp -p wa -k session', 15 | '-w /var/log/wtmp -p wa -k logins', 16 | '-w /var/log/btmp -p wa -k logins', 17 | ] 18 | 19 | for rule in rules: 20 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 21 | print(shellexec(f'auditctl {rule}')) 22 | 23 | yield None 24 | 25 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 26 | print(shellexec('auditctl -l')) 27 | 28 | os.remove('/etc/audit/rules.d/pytest.rules') 29 | shellexec('auditctl -D') 30 | 31 | 32 | def test_integration_audit_events_for_session_initiation_are_collected_pass(setup_to_pass): 33 | state = CISAudit().audit_events_for_session_initiation_are_collected() 34 | assert state == 0 35 | 36 | 37 | def test_integration_audit_events_for_session_initiation_are_collected_fail(): 38 | state = CISAudit().audit_events_for_session_initiation_are_collected() 39 | assert state == 3 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/unit/test_audit_audit_logs_not_automatically_deleted.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_audit_logs_not_automatically_deleted_pass(self, cmd): 12 | stdout = ['max_log_file = keep_logs', ''] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_audit_logs_not_automatically_deleted_fail(self, cmd): 20 | stdout = [''] 21 | stderr = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | test = CISAudit() 28 | 29 | 30 | @patch.object(CISAudit, "_shellexec", mock_audit_logs_not_automatically_deleted_pass) 31 | def test_audit_audit_logs_not_automatically_deleted_pass(): 32 | state = test.audit_audit_logs_not_automatically_deleted() 33 | assert state == 0 34 | 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_audit_logs_not_automatically_deleted_fail) 37 | def test_audit_audit_logs_not_automatically_deleted_fail(): 38 | state = test.audit_audit_logs_not_automatically_deleted() 39 | assert state == 1 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov']) 44 | -------------------------------------------------------------------------------- /tests/unit/test_audit_etc_shadow_password_fields_are_not_empty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_etc_shadow_password_fields_are_not_empty_pass(self, cmd): 14 | returncode = 1 15 | stderr = [''] 16 | stdout = [''] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_etc_shadow_password_fields_are_not_empty_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['pytest::18925::::::'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_etc_shadow_password_fields_are_not_empty_pass) 30 | def test_audit_etc_shadow_password_fields_are_not_empty_pass(): 31 | state = test.audit_etc_shadow_password_fields_are_not_empty() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_etc_shadow_password_fields_are_not_empty_fail) 36 | def test_audit_etc_shadow_password_fields_are_not_empty_fail(): 37 | state = test.audit_etc_shadow_password_fields_are_not_empty() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_package_not_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_package_installed(*args): 12 | output = ['pytest-0.0.0', ''] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_package_not_installed(self, cmd): 20 | output = ['package pytest is not installed', ''] 21 | error = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | class TestPackageNotInstalled: 28 | test = CISAudit() 29 | test_id = '1.1' 30 | test_package = 'pytest' 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_package_not_installed) 33 | def test_package_not_installed_pass(self): 34 | state = self.test.audit_package_not_installed(package=self.test_package) 35 | assert state == 0 36 | 37 | @patch.object(CISAudit, "_shellexec", mock_package_installed) 38 | def test_package_not_installed_fail(self): 39 | state = self.test.audit_package_not_installed(package=self.test_package) 40 | assert state == 1 41 | 42 | 43 | if __name__ == '__main__': 44 | pytest.main([__file__, '--no-cov']) 45 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_chrony_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_fail(): 13 | ## Original State 14 | is_active = shellexec('systemctl is-active chronyd').stdout[0] 15 | is_enabled = shellexec('systemctl is-enabled chronyd').stdout[0] 16 | shutil.copy('/etc/chrony.conf', '/etc/chrony.conf.bak') 17 | 18 | ## Setup 19 | shellexec('systemctl stop chronyd') 20 | shellexec('systemctl disable chronyd') 21 | shellexec('sed -i "/^server/d" /etc/chrony.conf') 22 | 23 | yield None 24 | 25 | ## Cleanup 26 | if is_active == 'active': 27 | shellexec('systemctl start chronyd') 28 | 29 | if is_enabled == 'enabled': 30 | shellexec('systemctl enable chronyd') 31 | 32 | shutil.move('/etc/chrony.conf.bak', '/etc/chrony.conf') 33 | 34 | 35 | def test_integration_audit_chrony_is_configured_pass(): 36 | state = CISAudit().audit_chrony_is_configured() 37 | assert state == 0 38 | 39 | 40 | def test_integration_audit_chrony_is_configured_fail(setup_to_fail): 41 | state = CISAudit().audit_chrony_is_configured() 42 | assert state == 15 43 | 44 | 45 | if __name__ == '__main__': 46 | pytest.main([__file__, '--no-cov']) 47 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_that_modify_manditory_access_controls_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | rules = [ 14 | '-w /etc/selinux -p wa -k MAC-policy', 15 | '-w /usr/share/selinux -p wa -k MAC-policy', 16 | ] 17 | 18 | for rule in rules: 19 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 20 | print(shellexec(f'auditctl {rule}')) 21 | 22 | yield None 23 | 24 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 25 | print(shellexec('auditctl -l')) 26 | 27 | os.remove('/etc/audit/rules.d/pytest.rules') 28 | shellexec('auditctl -D') 29 | 30 | 31 | def test_integration_audit_events_that_modify_mandatory_access_controls_are_collected_pass(setup_to_pass): 32 | state = CISAudit().audit_events_that_modify_mandatory_access_controls_are_collected() 33 | assert state == 0 34 | 35 | 36 | def test_integration_audit_events_that_modify_mandatory_access_controls_are_collected_fail(): 37 | state = CISAudit().audit_events_that_modify_mandatory_access_controls_are_collected() 38 | assert state == 3 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_partition_is_separate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_parition_exists(self, cmd): 12 | output = ['/dev/sda1 1014M 125M 890M 13% /boot'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_parititon_not_exists(self, cmd): 20 | output = [''] 21 | error = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | class TestPartitionSeparate: 28 | test_id = '1.1' 29 | test_level = 1 30 | partition = '/dev/sda1' 31 | test = CISAudit() 32 | 33 | @patch.object(CISAudit, "_shellexec", mock_parition_exists) 34 | def test_partition_is_separate(self): 35 | state = self.test.audit_partition_is_separate(partition=self.partition) 36 | assert state == 0 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_parititon_not_exists) 39 | def test_partition_is_not_separate(self): 40 | state = self.test.audit_partition_is_separate(partition=self.partition) 41 | assert state == 1 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_package_not_installed_or_service_is_masked.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | import pytest 5 | 6 | from cis_audit import CISAudit 7 | from tests.integration import shellexec 8 | 9 | 10 | @pytest.fixture 11 | def setup_to_pass_masked(): 12 | shellexec('systemctl mask rsyncd') 13 | 14 | yield None 15 | 16 | shellexec('systemctl unmask rsyncd') 17 | 18 | 19 | @pytest.fixture 20 | def setup_to_pass_not_installed(): 21 | shellexec('yum remove -y rsync') 22 | 23 | yield None 24 | 25 | shellexec('yum install -y rsync') 26 | 27 | 28 | def test_audit_package_not_installed_or_service_is_masked_pass_not_installed(setup_to_pass_not_installed): 29 | state = CISAudit().audit_package_not_installed_or_service_is_masked(package='rsync', service='rsyncd') 30 | assert state == 0 31 | 32 | 33 | def test_audit_package_not_installed_or_service_is_masked_pass_masked(setup_to_pass_masked): 34 | state = CISAudit().audit_package_not_installed_or_service_is_masked(package='rsync', service='rsyncd') 35 | assert state == 0 36 | 37 | 38 | def test_audit_package_not_installed_or_service_is_masked_fail(): 39 | state = CISAudit().audit_package_not_installed_or_service_is_masked(package='rsync', service='rsyncd') 40 | assert state == 1 41 | 42 | 43 | if __name__ == '__main__': 44 | pytest.main([__file__, '--no-cov']) 45 | -------------------------------------------------------------------------------- /tests/unit/test_audit_core_dumps_restricted.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_core_dumps_pass(self, cmd): 12 | if 'limits.conf' in cmd: 13 | stdout = ['* hard core 0'] 14 | stderr = [''] 15 | returncode = 0 16 | elif 'sysctl' in cmd: 17 | stdout = ['fs.suid_dumpable = 0'] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_core_dumps_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | class TestCoreDumpsRestricted: 33 | test = CISAudit() 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_core_dumps_pass) 36 | def test_mock_core_dumps_pass(self): 37 | state = self.test.audit_core_dumps_restricted() 38 | assert state == 0 39 | 40 | @patch.object(CISAudit, "_shellexec", mock_core_dumps_fail) 41 | def test_mock_core_dumps_fail(self): 42 | state = self.test.audit_core_dumps_restricted() 43 | assert state == 7 44 | 45 | 46 | if __name__ == '__main__': 47 | pytest.main([__file__, '--no-cov']) 48 | -------------------------------------------------------------------------------- /tests/unit/test_audit_password_reuse_is_limited.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_password_reuse_is_limited_pass(*args): 14 | returncode = 0 15 | stderr = [''] 16 | stdout = [ 17 | '/etc/pam.d/system-auth:password required pam_pwhistory.so remember=5', 18 | '/etc/pam.d/password-auth:password required pam_pwhistory.so remember=5', 19 | '', 20 | ] 21 | 22 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 23 | 24 | 25 | def mock_password_reuse_is_limited_fail(*args): 26 | returncode = 1 27 | stderr = [''] 28 | stdout = [''] 29 | 30 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 31 | 32 | 33 | @patch.object(CISAudit, "_shellexec", mock_password_reuse_is_limited_pass) 34 | def test_audit_password_reuse_is_limited_pass(): 35 | state = test.audit_password_reuse_is_limited() 36 | assert state == 0 37 | 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_password_reuse_is_limited_fail) 40 | def test_audit_password_reuse_is_limited_pass_fail(): 41 | state = test.audit_password_reuse_is_limited() 42 | assert state == 1 43 | 44 | 45 | if __name__ == '__main__': 46 | pytest.main([__file__, '--no-cov']) 47 | -------------------------------------------------------------------------------- /tests/unit/test_audit_etc_passwd_accounts_use_shadowed_passwords.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_etc_passwd_accounts_use_shadowed_passwords_pass(self, cmd): 14 | returncode = 1 15 | stderr = [''] 16 | stdout = [''] 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_etc_passwd_accounts_use_shadowed_passwords_fail(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = ['pytest:!!:1000:1000::/home/pytest:/bin/bash'] 25 | 26 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 27 | 28 | 29 | @patch.object(CISAudit, "_shellexec", mock_etc_passwd_accounts_use_shadowed_passwords_pass) 30 | def test_audit_etc_passwd_accounts_use_shadowed_passwords_pass(): 31 | state = test.audit_etc_passwd_accounts_use_shadowed_passwords() 32 | assert state == 0 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_etc_passwd_accounts_use_shadowed_passwords_fail) 36 | def test_audit_etc_passwd_accounts_use_shadowed_passwords_fail(): 37 | state = test.audit_etc_passwd_accounts_use_shadowed_passwords() 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_permissions_on_log_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_permissions_on_log_files_are_configured_pass(self, cmd): 14 | stdout = [''] 15 | stderr = [''] 16 | returncode = 0 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_audit_permissions_on_log_files_are_configured_fail(self, cmd): 22 | stdout = [ 23 | '-rw-r--r--. 1 root root 0 Jan 1 0:00 /var/log/pytest', 24 | '', 25 | ] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_permissions_on_log_files_are_configured_pass) 33 | def test_audit_permissions_on_log_files_are_configured_pass(): 34 | state = test.audit_permissions_on_log_files() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_permissions_on_log_files_are_configured_fail) 39 | def test_audit_permissions_on_log_files_are_configured_fail(): 40 | state = test.audit_permissions_on_log_files() 41 | assert state == 1 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_journald_configured_to_compress_large_logs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_journald_configured_to_compress_large_logs_pass(self, cmd): 14 | stdout = [ 15 | 'Compress=yes', 16 | '', 17 | ] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_audit_journald_configured_to_compress_large_logs_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_journald_configured_to_compress_large_logs_pass) 33 | def test_audit_journald_configured_to_compress_large_logs_pass(): 34 | state = test.audit_journald_configured_to_compress_large_logs() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_journald_configured_to_compress_large_logs_fail) 39 | def test_audit_journald_configured_to_compress_large_logs_fail(): 40 | state = test.audit_journald_configured_to_compress_large_logs() 41 | assert state == 1 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_partition_option_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_option_set(self, cmd): 12 | output = ['xfs on /pytest type proc (rw,nosuid,nodev,noexec,relatime)'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_option_not_set(self, cmd): 20 | output = [''] 21 | error = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | class TestPartitionOptions: 28 | test = CISAudit() 29 | test_id = '1.1' 30 | test_level = 1 31 | partition = '/pytest' 32 | option = 'noexec' 33 | 34 | @patch.object(CISAudit, "_shellexec", mock_option_set) 35 | def test_partition_option_is_set(self): 36 | state = self.test.audit_partition_option_is_set(partition=self.partition, option=self.option) 37 | assert state == 0 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_option_not_set) 40 | def test_partition_option_is_not_set(self): 41 | state = self.test.audit_partition_option_is_set(partition=self.partition, option=self.option) 42 | assert state == 1 43 | 44 | 45 | if __name__ == '__main__': 46 | pytest.main([__file__, '--no-cov']) 47 | -------------------------------------------------------------------------------- /tests/unit/test_audit_permissions_on_public_host_key_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | import cis_audit 9 | 10 | test = cis_audit.CISAudit() 11 | 12 | 13 | def mock_audit_file_permissions_pass(*args, **kwargs): 14 | return 0 15 | 16 | 17 | def mock_audit_file_permissions_fail(*args, **kwargs): 18 | return 1 19 | 20 | 21 | def mock_shellexec(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = [ 25 | 'hostkey /pytest1', 26 | 'hostkey /pytest2', 27 | ] 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(cis_audit.CISAudit, "_shellexec", mock_shellexec) 33 | @patch.object(cis_audit.CISAudit, "audit_file_permissions", mock_audit_file_permissions_pass) 34 | def test_audit_permissions_on_public_host_key_files_pass(): 35 | state = test.audit_permissions_on_public_host_key_files() 36 | assert state == 0 37 | 38 | 39 | @patch.object(cis_audit.CISAudit, "_shellexec", mock_shellexec) 40 | @patch.object(cis_audit.CISAudit, "audit_file_permissions", mock_audit_file_permissions_fail) 41 | def test_audit_permissions_on_public_host_key_files_fail(): 42 | state = test.audit_permissions_on_public_host_key_files() 43 | assert state == 3 44 | 45 | 46 | if __name__ == '__main__': 47 | pytest.main([__file__, '--no-cov']) 48 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_no_unconfined_services.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from time import sleep 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | print(shellexec('pkill -e VBoxService')) 15 | # Wait before proceeding to allow VBoxService to finish exiting 16 | sleep(0.5) 17 | 18 | yield None 19 | 20 | print('systemctl restart vboxadd-service') 21 | 22 | 23 | @pytest.fixture 24 | def setup_to_fail(): 25 | ## Setup 26 | ## https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/selinux_users_and_administrators_guide/sect-security-enhanced_linux-targeted_policy-unconfined_processes 27 | shellexec('chcon -t bin_t /usr/bin/rsync') 28 | shellexec('systemctl start rsyncd') 29 | 30 | yield None 31 | 32 | ## Tear-down 33 | shellexec('systemctl stop rsyncd') 34 | shellexec('restorecon -v /usr/bin/rsync') 35 | 36 | 37 | def test_integration_audit_no_unconfined_services_pass(setup_to_pass): 38 | state = CISAudit().audit_no_unconfined_services() 39 | assert state == 0 40 | 41 | 42 | def test_integration_audit_no_unconfined_services_fail(setup_to_fail): 43 | state = CISAudit().audit_no_unconfined_services() 44 | assert state == 1 45 | 46 | 47 | if __name__ == '__main__': 48 | pytest.main([__file__, '--no-cov']) 49 | -------------------------------------------------------------------------------- /tests/unit/test_audit_permissions_on_private_host_key_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | import cis_audit 9 | 10 | test = cis_audit.CISAudit() 11 | 12 | 13 | def mock_audit_file_permissions_pass(*args, **kwargs): 14 | return 0 15 | 16 | 17 | def mock_audit_file_permissions_fail(*args, **kwargs): 18 | return 1 19 | 20 | 21 | def mock_shellexec(self, cmd): 22 | returncode = 0 23 | stderr = [''] 24 | stdout = [ 25 | 'hostkey /pytest1', 26 | 'hostkey /pytest2', 27 | ] 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(cis_audit.CISAudit, "_shellexec", mock_shellexec) 33 | @patch.object(cis_audit.CISAudit, "audit_file_permissions", mock_audit_file_permissions_pass) 34 | def test_audit_permissions_on_private_host_key_files_pass(): 35 | state = test.audit_permissions_on_private_host_key_files() 36 | assert state == 0 37 | 38 | 39 | @patch.object(cis_audit.CISAudit, "_shellexec", mock_shellexec) 40 | @patch.object(cis_audit.CISAudit, "audit_file_permissions", mock_audit_file_permissions_fail) 41 | def test_audit_permissions_on_private_host_key_files_fail(): 42 | state = test.audit_permissions_on_private_host_key_files() 43 | assert state == 3 44 | 45 | 46 | if __name__ == '__main__': 47 | pytest.main([__file__, '--no-cov']) 48 | -------------------------------------------------------------------------------- /tests/unit/test_audit_journald_configured_to_send_logs_to_rsyslog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_journald_configured_to_send_logs_to_rsyslog_pass(self, cmd): 14 | stdout = [ 15 | 'ForwardToSyslog=yes', 16 | '', 17 | ] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_audit_journald_configured_to_send_logs_to_rsyslog_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_journald_configured_to_send_logs_to_rsyslog_pass) 33 | def test_audit_journald_configured_to_send_logs_to_rsyslog_pass(): 34 | state = test.audit_journald_configured_to_send_logs_to_rsyslog() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_journald_configured_to_send_logs_to_rsyslog_fail) 39 | def test_audit_journald_configured_to_send_logs_to_rsyslog_fail(): 40 | state = test.audit_journald_configured_to_send_logs_to_rsyslog() 41 | assert state == 1 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_that_modify_usergroup_info_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | rules = [ 14 | '-w /etc/group -p wa -k identity', 15 | '-w /etc/passwd -p wa -k identity', 16 | '-w /etc/gshadow -p wa -k identity', 17 | '-w /etc/shadow -p wa -k identity', 18 | '-w /etc/security/opasswd -p wa -k identity', 19 | ] 20 | 21 | for rule in rules: 22 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 23 | print(shellexec(f'auditctl {rule}')) 24 | 25 | yield None 26 | 27 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 28 | print(shellexec('auditctl -l')) 29 | 30 | os.remove('/etc/audit/rules.d/pytest.rules') 31 | shellexec('auditctl -D') 32 | 33 | 34 | def test_integration_audit_events_that_modify_usergroup_info_are_collected_pass(setup_to_pass): 35 | state = CISAudit().audit_events_that_modify_usergroup_info_are_collected() 36 | assert state == 0 37 | 38 | 39 | def test_integration_audit_events_that_modify_usergroup_info_are_collected_fail(): 40 | state = CISAudit().audit_events_that_modify_usergroup_info_are_collected() 41 | assert state == 3 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_gpgcheck_is_activated.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_fail_yum_conf(): 13 | shellexec("sed -i '/gpgcheck=/ s/1/0/' /etc/yum.conf") 14 | 15 | yield None 16 | 17 | shellexec("sed -i '/gpgcheck=/ s/0/1/' /etc/yum.conf") 18 | 19 | 20 | @pytest.fixture 21 | def setup_to_fail_repo_file(): 22 | with open('/etc/yum.repos.d/pytest.repo', 'w') as f: 23 | f.writelines( 24 | [ 25 | '[pytest]\n', 26 | 'name=Pytest\n', 27 | 'enabled=1\n', 28 | 'gpgcheck=0\n', 29 | ] 30 | ) 31 | 32 | yield None 33 | 34 | os.remove('/etc/yum.repos.d/pytest.repo') 35 | 36 | 37 | def test_integration_audit_gpgcheck_is_activated_pass(): 38 | state = CISAudit().audit_gpgcheck_is_activated() 39 | assert state == 0 40 | 41 | 42 | def test_integration_audit_gpgcheck_is_activated_fail_state_1(setup_to_fail_yum_conf): 43 | state = CISAudit().audit_gpgcheck_is_activated() 44 | assert state == 1 45 | 46 | 47 | def test_integration_audit_gpgcheck_is_activated_fail_state_2(setup_to_fail_repo_file): 48 | state = CISAudit().audit_gpgcheck_is_activated() 49 | assert state == 2 50 | 51 | 52 | if __name__ == '__main__': 53 | pytest.main([__file__, '--no-cov']) 54 | -------------------------------------------------------------------------------- /tests/unit/test_audit_journald_configured_to_write_logfiles_to_disk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_journald_configured_to_write_logfiles_to_disk_pass(self, cmd): 14 | stdout = [ 15 | 'Storage=persistent', 16 | '', 17 | ] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_audit_journald_configured_to_write_logfiles_to_disk_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_journald_configured_to_write_logfiles_to_disk_pass) 33 | def test_audit_journald_configured_to_write_logfiles_to_disk_pass(): 34 | state = test.audit_journald_configured_to_write_logfiles_to_disk() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_journald_configured_to_write_logfiles_to_disk_fail) 39 | def test_audit_journald_configured_to_write_logfiles_to_disk_fail(): 40 | state = test.audit_journald_configured_to_write_logfiles_to_disk() 41 | assert state == 1 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_rsyslog_default_file_permission_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_rsyslog_default_file_permission_is_configured_pass(self, cmd): 14 | stdout = [ 15 | '$FileCreateMode 0640', 16 | '', 17 | ] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_audit_rsyslog_default_file_permission_is_configured_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_rsyslog_default_file_permission_is_configured_pass) 33 | def test_audit_rsyslog_default_file_permission_is_configured_pass(): 34 | state = test.audit_rsyslog_default_file_permission_is_configured() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_rsyslog_default_file_permission_is_configured_fail) 39 | def test_audit_rsyslog_default_file_permission_is_configured_fail(): 40 | state = test.audit_rsyslog_default_file_permission_is_configured() 41 | assert state == 1 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_events_for_login_and_logout_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_events_for_login_and_logout_are_collected_pass(self, cmd): 14 | stdout = [ 15 | '-w /var/log/lastlog -p wa -k logins', 16 | '-w /var/run/faillock -p wa -k logins', 17 | ] 18 | 19 | stderr = [''] 20 | returncode = 0 21 | 22 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 23 | 24 | 25 | def mock_audit_events_for_login_and_logout_are_collected_fail(self, cmd): 26 | stdout = [''] 27 | stderr = [''] 28 | returncode = 1 29 | 30 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 31 | 32 | 33 | @patch.object(CISAudit, "_shellexec", mock_audit_events_for_login_and_logout_are_collected_pass) 34 | def test_audit_events_for_login_and_logout_are_collected_pass(): 35 | state = test.audit_events_for_login_and_logout_are_collected() 36 | assert state == 0 37 | 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_audit_events_for_login_and_logout_are_collected_fail) 40 | def test_audit_events_for_login_and_logout_are_collected_fail(): 41 | state = test.audit_events_for_login_and_logout_are_collected() 42 | assert state == 3 43 | 44 | 45 | if __name__ == '__main__': 46 | pytest.main([__file__, '--no-cov']) 47 | -------------------------------------------------------------------------------- /tests/unit/test_audit_removable_partition_option_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_option_set(self, cmd): 12 | if 'lsblk' in cmd: 13 | output = ['/mnt'] 14 | else: 15 | output = [''] 16 | 17 | error = [''] 18 | returncode = 0 19 | 20 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 21 | 22 | 23 | def mock_option_not_set(self, cmd): 24 | if 'lsblk' in cmd: 25 | output = ['/mnt'] 26 | else: 27 | output = ['/mnt /dev/sdb1 vfat ro,relatime'] 28 | 29 | error = [''] 30 | returncode = 1 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | class TestPartitionOptions: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | test_level = 1 39 | option = 'noexec' 40 | 41 | @patch.object(CISAudit, "_shellexec", mock_option_set) 42 | def test_partition_option_is_set(self): 43 | state = self.test.audit_removable_partition_option_is_set(option=self.option) 44 | assert state == 0 45 | 46 | @patch.object(CISAudit, "_shellexec", mock_option_not_set) 47 | def test_partition_option_is_not_set(self): 48 | state = self.test.audit_removable_partition_option_is_set(option=self.option) 49 | assert state == 1 50 | 51 | 52 | if __name__ == '__main__': 53 | pytest.main([__file__, '--no-cov']) 54 | -------------------------------------------------------------------------------- /tests/unit/test_audit_etc_passwd_gids_exist_in_etc_group.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_gids_in_passwd_pass(self, cmd): 12 | if '/etc/group' in cmd: 13 | output = ['1000', '1001', ''] 14 | elif '/etc/passwd' in cmd: 15 | output = ['1000', '1001', ''] 16 | else: 17 | output = [''] 18 | error = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 22 | 23 | 24 | def mock_gids_in_passwd_fail(self, cmd): 25 | if '/etc/group' in cmd: 26 | output = ['1000', ''] 27 | elif '/etc/passwd' in cmd: 28 | output = ['1000', '1001', ''] 29 | else: 30 | output = [''] 31 | error = [''] 32 | returncode = 0 33 | 34 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 35 | 36 | 37 | test = CISAudit() 38 | 39 | 40 | @patch.object(CISAudit, "_shellexec", mock_gids_in_passwd_pass) 41 | def test_gids_from_etcpasswd_are_in_etcgroup_pass(): 42 | state = test.audit_etc_passwd_gids_exist_in_etc_group() 43 | assert state == 0 44 | 45 | 46 | @patch.object(CISAudit, "_shellexec", mock_gids_in_passwd_fail) 47 | def test_gids_from_etcpasswd_are_in_etcgroup_fail(): 48 | state = test.audit_etc_passwd_gids_exist_in_etc_group() 49 | assert state == 1 50 | 51 | 52 | if __name__ == '__main__': 53 | pytest.main([__file__, '--no-cov']) 54 | -------------------------------------------------------------------------------- /tests/unit/test_audit_package_not_installed_or_service_is_masked.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | 9 | test = CISAudit() 10 | 11 | 12 | def mock_result_pass(package, service): 13 | return 0 14 | 15 | 16 | def mock_result_fail(package, service): 17 | return 1 18 | 19 | 20 | @patch.object(CISAudit, "audit_package_is_installed", mock_result_fail) 21 | @patch.object(CISAudit, "audit_service_is_masked", mock_result_fail) 22 | def test_audit_package_not_installed_or_service_is_masked_pass_not_installed(): 23 | state = test.audit_package_not_installed_or_service_is_masked(package='pytest', service='pytestd') 24 | assert state == 0 25 | 26 | 27 | @patch.object(CISAudit, "audit_package_is_installed", mock_result_pass) 28 | @patch.object(CISAudit, "audit_service_is_masked", mock_result_pass) 29 | def test_audit_package_not_installed_or_service_is_masked_pass_masked(): 30 | state = test.audit_package_not_installed_or_service_is_masked(package='pytest', service='pytestd') 31 | assert state == 0 32 | 33 | 34 | @patch.object(CISAudit, "audit_package_is_installed", mock_result_pass) 35 | @patch.object(CISAudit, "audit_service_is_masked", mock_result_fail) 36 | def test_audit_package_not_installed_or_service_is_masked_fail(): 37 | state = test.audit_package_not_installed_or_service_is_masked(package='pytest', service='pytestd') 38 | assert state == 1 39 | 40 | 41 | if __name__ == '__main__': 42 | pytest.main([__file__, '--no-cov']) 43 | -------------------------------------------------------------------------------- /tests/unit/test_audit_service_disabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_disabled(*args, **kwargs): 12 | output = ['disabled'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_enabled(*args, **kwargs): 20 | output = ['enabled'] 21 | error = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_error(*args, **kwargs): 28 | output = [''] 29 | error = ['Failed to get unit file state for pytest.service: No such file or directory'] 30 | returncode = 1 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | class TestServiceDisabled: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | test_service = 'pytest' 39 | 40 | @patch.object(CISAudit, "_shellexec", mock_disabled) 41 | def test_service_disabled_pass(self): 42 | state = self.test.audit_service_is_disabled(self.test_service) 43 | assert state == 0 44 | 45 | @patch.object(CISAudit, "_shellexec", mock_enabled) 46 | def test_service_disabled_fail(self): 47 | state = self.test.audit_service_is_disabled(self.test_service) 48 | assert state == 1 49 | 50 | 51 | if __name__ == '__main__': 52 | pytest.main([__file__, '--no-cov']) 53 | -------------------------------------------------------------------------------- /tests/unit/test_audit_service_masked.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_masked(*args, **kwargs): 12 | output = ['masked'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_unmasked(*args, **kwargs): 20 | output = ['enabled'] 21 | error = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_error(*args, **kwargs): 28 | output = [''] 29 | error = ['Failed to get unit file state for pytest.service: No such file or directory'] 30 | returncode = 1 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | class TestServiceMasked: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | test_service = 'pytest' 39 | 40 | @patch.object(CISAudit, "_shellexec", mock_masked) 41 | def test_service_masked_pass(self): 42 | state = self.test.audit_service_is_masked(service=self.test_service) 43 | assert state == 0 44 | 45 | @patch.object(CISAudit, "_shellexec", mock_unmasked) 46 | def test_service_masked_fail(self): 47 | state = self.test.audit_service_is_masked(service=self.test_service) 48 | assert state == 1 49 | 50 | 51 | if __name__ == '__main__': 52 | pytest.main([__file__, '--no-cov']) 53 | -------------------------------------------------------------------------------- /tests/unit/test_audit_events_for_session_initiation_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_events_for_session_initiation_are_collected_pass(self, cmd): 14 | stdout = [ 15 | '-w /var/run/utmp -p wa -k session', 16 | '-w /var/log/wtmp -p wa -k logins', 17 | '-w /var/log/btmp -p wa -k logins', 18 | ] 19 | stderr = [''] 20 | returncode = 0 21 | 22 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 23 | 24 | 25 | def mock_audit_events_for_session_initiation_are_collected_fail(self, cmd): 26 | stdout = [''] 27 | stderr = [''] 28 | returncode = 1 29 | 30 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 31 | 32 | 33 | @patch.object(CISAudit, "_shellexec", mock_audit_events_for_session_initiation_are_collected_pass) 34 | def test_audit_events_for_session_initiation_are_collected_pass(): 35 | state = test.audit_events_for_session_initiation_are_collected() 36 | assert state == 0 37 | 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_audit_events_for_session_initiation_are_collected_fail) 40 | def test_audit_events_for_session_initiation_are_collected_fail(): 41 | state = test.audit_events_for_session_initiation_are_collected() 42 | assert state == 3 43 | 44 | 45 | if __name__ == '__main__': 46 | pytest.main([__file__, '--no-cov']) 47 | -------------------------------------------------------------------------------- /tests/unit/test_audit_events_for_changes_to_sysadmin_scope_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_events_for_changes_to_sysadmin_scope_are_collected_pass(self, cmd): 14 | stdout = [ 15 | '-w /etc/sudoers -p wa -k scope', 16 | '-w /etc/sudoers.d -p wa -k scope', 17 | ] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_audit_events_for_changes_to_sysadmin_scope_are_collected_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_events_for_changes_to_sysadmin_scope_are_collected_pass) 33 | def test_audit_events_for_changes_to_sysadmin_scope_are_collected_pass(): 34 | state = test.audit_events_for_changes_to_sysadmin_scope_are_collected() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_events_for_changes_to_sysadmin_scope_are_collected_fail) 39 | def test_audit_events_for_changes_to_sysadmin_scope_are_collected_fail(): 40 | state = test.audit_events_for_changes_to_sysadmin_scope_are_collected() 41 | assert state == 3 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/unit/test_audit_sticky_bit_set_on_dirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_sticky_bit_set(self, cmd): 12 | output = [''] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_sticky_bit_not_set(self, cmd): 20 | output = ['/pytest'] 21 | error = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_sticky_bit_error(self, cmd): 28 | output = [''] 29 | error = ['find: invalid expression; I was expecting to find a \')\' somewhere but did not see one.'] 30 | returncode = 123 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | class TestPartitionOptions: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_sticky_bit_set) 40 | def test_directory_sticky_bit_is_set(self): 41 | state = self.test.audit_sticky_bit_on_world_writable_dirs() 42 | assert state == 0 43 | 44 | @patch.object(CISAudit, "_shellexec", mock_sticky_bit_not_set) 45 | def test_directory_sticky_bit_is_not_set(self): 46 | state = self.test.audit_sticky_bit_on_world_writable_dirs() 47 | assert state == 1 48 | 49 | 50 | if __name__ == '__main__': 51 | pytest.main([__file__, '--no-cov']) 52 | -------------------------------------------------------------------------------- /tests/unit/test_audit_nftables_default_deny_policy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_nftables_default_deny_policy_pass(self, cmd): 12 | returncode = 0 13 | stderr = [''] 14 | 15 | if 'input' in cmd: 16 | stdout = ['type filter hook input priority 0; policy drop;'] 17 | elif 'forward' in cmd: 18 | stdout = ['type filter hook forward priority 0; policy drop;'] 19 | elif 'output' in cmd: 20 | stdout = ['type filter hook output priority 0; policy drop;'] 21 | else: 22 | stdout = [''] 23 | returncode = 1 24 | 25 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 26 | 27 | 28 | def mock_nftables_default_deny_policy_fail(self, cmd): 29 | stdout = [''] 30 | stderr = [''] 31 | returncode = 1 32 | 33 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 34 | 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_nftables_default_deny_policy_pass) 37 | def test_audit_nftables_default_deny_policy_pass(): 38 | state = CISAudit().audit_nftables_default_deny_policy() 39 | assert state == 0 40 | 41 | 42 | @patch.object(CISAudit, "_shellexec", mock_nftables_default_deny_policy_fail) 43 | def test_audit_nftables_default_deny_policy_fail(): 44 | state = CISAudit().audit_nftables_default_deny_policy() 45 | assert state == 7 46 | 47 | 48 | if __name__ == '__main__': 49 | pytest.main([__file__, '--no-cov']) 50 | -------------------------------------------------------------------------------- /tests/unit/test_audit_nftables_loopback_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_nftables_loopback_is_configured_pass(self, cmd): 12 | returncode = 0 13 | stderr = [''] 14 | 15 | if 'accept' in cmd: 16 | stdout = ['iif "lo" accept'] 17 | elif 'ip saddr' in cmd: 18 | stdout = ['ip saddr 127.0.0.0/8 counter packets 99 bytes 99 drop'] 19 | elif 'ip6 saddr' in cmd: 20 | stdout = ['ip6 saddr ::1 counter packets 0 bytes 0 drop'] 21 | else: 22 | stdout = [''] 23 | returncode = 1 24 | 25 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 26 | 27 | 28 | def mock_nftables_loopback_is_configured_fail(self, cmd): 29 | stdout = [''] 30 | stderr = [''] 31 | returncode = 1 32 | 33 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 34 | 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_nftables_loopback_is_configured_pass) 37 | def test_audit_nftables_loopback_is_configured_pass(): 38 | state = CISAudit().audit_nftables_loopback_is_configured() 39 | assert state == 0 40 | 41 | 42 | @patch.object(CISAudit, "_shellexec", mock_nftables_loopback_is_configured_fail) 43 | def test_audit_nftables_loopback_is_configured_fail_all(): 44 | state = CISAudit().audit_nftables_loopback_is_configured() 45 | assert state == 7 46 | 47 | 48 | if __name__ == '__main__': 49 | pytest.main([__file__, '--no-cov']) 50 | -------------------------------------------------------------------------------- /tests/unit/test_audit_shadow_group_is_empty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_shadow_group_is_empty(self, cmd): 12 | output = [''] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_shadow_group_is_not_empty(self, cmd): 20 | output = ['user'] 21 | error = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_shadow_group_is_absent(self, cmd): 28 | output = [''] 29 | error = [''] 30 | returncode = 0 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | test = CISAudit() 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_shadow_group_is_empty) 39 | def test_audit_shadow_group_is_empty_pass(): 40 | state = test.audit_shadow_group_is_empty() 41 | assert state == 0 42 | 43 | 44 | @patch.object(CISAudit, "_shellexec", mock_shadow_group_is_absent) 45 | def test_audit_shadow_group_is_absent_pass(): 46 | state = test.audit_shadow_group_is_empty() 47 | assert state == 0 48 | 49 | 50 | @patch.object(CISAudit, "_shellexec", mock_shadow_group_is_not_empty) 51 | def test_audit_shadow_group_is_empty_fail(): 52 | state = test.audit_shadow_group_is_empty() 53 | assert state == 3 54 | 55 | 56 | if __name__ == '__main__': 57 | pytest.main([__file__, '--no-cov']) 58 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_nftables_default_deny_policy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | shellexec('nft add rule inet filter input dport tcp ssh accept') 12 | shellexec('nft add rule inet filter input ct state established accept') 13 | shellexec('nft add rule inet filter output ct state new,related,established accept') 14 | 15 | shellexec(R'nft chain inet filter input { policy drop \; }') 16 | shellexec(R'nft chain inet filter forward { policy drop \; }') 17 | shellexec(R'nft chain inet filter output { policy drop \; }') 18 | 19 | yield None 20 | 21 | shellexec(R'nft chain inet filter input { policy accept \; }') 22 | shellexec(R'nft chain inet filter forward { policy accept \; }') 23 | shellexec(R'nft chain inet filter output { policy accept \; }') 24 | 25 | shellexec('nft flush chain inet filter input') 26 | shellexec('nft flush chain inet filter forward') 27 | shellexec('nft flush chain inet filter output') 28 | 29 | 30 | def test_integration_audit_nftables_default_deny_policy_pass(setup_install_nftables, setup_to_pass): 31 | state = CISAudit().audit_nftables_default_deny_policy() 32 | assert state == 0 33 | 34 | 35 | def test_integration_audit_nftables_default_deny_policy_fail(setup_install_nftables): 36 | state = CISAudit().audit_nftables_default_deny_policy() 37 | assert state == 7 38 | 39 | 40 | if __name__ == '__main__': 41 | pytest.main([__file__, '--no-cov']) 42 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_nftables_connections_are_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | shellexec('nft add rule inet filter input ip protocol tcp ct state established accept') 12 | shellexec('nft add rule inet filter input ip protocol udp ct state established accept') 13 | shellexec('nft add rule inet filter input ip protocol icmp ct state established accept') 14 | shellexec('nft add rule inet filter output ip protocol tcp ct state new,related,established accept') 15 | shellexec('nft add rule inet filter output ip protocol udp ct state new,related,established accept') 16 | shellexec('nft add rule inet filter output ip protocol icmp ct state new,related,established accept') 17 | 18 | yield None 19 | 20 | shellexec('nft flush chain inet filter input') 21 | shellexec('nft flush chain inet filter forward') 22 | shellexec('nft flush chain inet filter output') 23 | 24 | 25 | def test_integration_audit_nftables_outbound_and_established_connections_pass(setup_install_nftables, setup_to_pass): 26 | state = CISAudit().audit_nftables_outbound_and_established_connections() 27 | assert state == 0 28 | 29 | 30 | def test_integration_audit_nftables_outbound_and_established_connections_fail(setup_install_nftables): 31 | state = CISAudit().audit_nftables_outbound_and_established_connections() 32 | assert state == 3 33 | 34 | 35 | if __name__ == '__main__': 36 | pytest.main([__file__, '--no-cov']) 37 | -------------------------------------------------------------------------------- /tests/unit/test_audit_service_is_enabled_and_is_active.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_disabled_and_inactive(self, cmd, **kwargs): 12 | if 'is-active' in cmd: 13 | output = ['inactive'] 14 | elif 'is-enabled' in cmd: 15 | output = ['disabled'] 16 | 17 | error = [''] 18 | returncode = 0 19 | 20 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 21 | 22 | 23 | def mock_enabled_and_active(self, cmd, **kwargs): 24 | if 'is-active' in cmd: 25 | output = ['active'] 26 | elif 'is-enabled' in cmd: 27 | output = ['enabled'] 28 | 29 | error = [''] 30 | returncode = 0 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | class TestService: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | test_service = 'pytest' 39 | 40 | @patch.object(CISAudit, "_shellexec", mock_enabled_and_active) 41 | def test_service_is_enabled_and_is_active_pass(self): 42 | state = self.test.audit_service_is_enabled_and_is_active(service=self.test_service) 43 | assert state == 0 44 | 45 | @patch.object(CISAudit, "_shellexec", mock_disabled_and_inactive) 46 | def test_service_is_enabled_and_is_active_fail(self): 47 | state = self.test.audit_service_is_enabled_and_is_active(service=self.test_service) 48 | assert state == 3 49 | 50 | 51 | if __name__ == '__main__': 52 | pytest.main([__file__, '--no-cov']) 53 | -------------------------------------------------------------------------------- /tests/unit/test_audit_sysctl_flags_are_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_sysctl_flags_are_set_pass(self, cmd): 12 | if 'net.ipv6.conf.all.disable_ipv6' in cmd: 13 | stdout = ['net.ipv6.conf.all.disable_ipv6 = 1'] 14 | elif 'net.ipv6.conf.default.disable_ipv6' in cmd: 15 | stdout = ['net.ipv6.conf.default.disable_ipv6 = 1'] 16 | 17 | stderr = [''] 18 | returncode = 0 19 | 20 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 21 | 22 | 23 | def mock_sysctl_flags_are_set_fail(self, cmd): 24 | if 'grub' in cmd: 25 | stdout = ['pytest'] 26 | else: 27 | stdout = [''] 28 | 29 | stderr = [''] 30 | returncode = 1 31 | 32 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 33 | 34 | 35 | test = CISAudit() 36 | flags = ["net.ipv6.conf.all.disable_ipv6", "net.ipv6.conf.default.disable_ipv6"] 37 | 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_sysctl_flags_are_set_pass) 40 | def test_audit_sysctl_flags_are_set_pass(): 41 | value = 1 42 | state = test.audit_sysctl_flags_are_set(flags, value) 43 | assert state == 0 44 | 45 | 46 | @patch.object(CISAudit, "_shellexec", mock_sysctl_flags_are_set_fail) 47 | def test_audit_sysctl_flags_are_set_fail(): 48 | value = 0 49 | state = test.audit_sysctl_flags_are_set(flags, value) 50 | assert state == 15 51 | 52 | 53 | if __name__ == '__main__': 54 | pytest.main([__file__, '--no-cov']) 55 | -------------------------------------------------------------------------------- /tests/unit/test_get_homedirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Tests in this file use pyfakefs to fake elements of the filesystem in order to perform the tests. 4 | ## pyfakefs provides the 'fs' fixture automatically, but this is redefined to make it easier to understand 5 | ## for people not familiar with it. 6 | ## Refer to https://jmcgeheeiv.github.io/pyfakefs/release/usage.html#patch-using-the-pytest-plugin 7 | ## https://jmcgeheeiv.github.io/pyfakefs/release/modules.html#pyfakefs.fake_filesystem.FakeFilesystem.create_dir 8 | ## https://jmcgeheeiv.github.io/pyfakefs/release/modules.html#pyfakefs.fake_filesystem.set_uid 9 | 10 | from types import GeneratorType, SimpleNamespace 11 | from unittest.mock import patch 12 | 13 | import pytest 14 | 15 | from cis_audit import CISAudit 16 | 17 | 18 | def mock_homedirs_data(self, cmd): 19 | stderr = [] 20 | stdout = [ 21 | 'root 0 /root', 22 | 'pytest 1000 /home/pytest', 23 | ] 24 | returncode = 0 25 | 26 | return SimpleNamespace(stdout=stdout, stderr=stderr, returncode=returncode) 27 | 28 | 29 | test = CISAudit() 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_homedirs_data) 33 | def test_get_homedirs_pass(): 34 | homedirs = test._get_homedirs() 35 | homedirs_list = list(homedirs) 36 | 37 | assert isinstance(homedirs, GeneratorType) 38 | assert homedirs_list[0] == ('root', 0, '/root') 39 | assert homedirs_list[1] == ('pytest', 1000, '/home/pytest') 40 | 41 | 42 | if __name__ == '__main__': 43 | pytest.main([__file__, '--no-cov', '-W', 'ignore:Module already imported:pytest.PytestWarning']) 44 | -------------------------------------------------------------------------------- /tests/unit/test_audit_events_that_modify_mandatory_access_controls_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_events_that_modify_mandatory_access_controls_are_collected_pass(self, cmd): 14 | stdout = [ 15 | '-w /etc/selinux -p wa -k MAC-policy', 16 | '-w /usr/share/selinux -p wa -k MAC-policy', 17 | ] 18 | stderr = [''] 19 | returncode = 0 20 | 21 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 22 | 23 | 24 | def mock_audit_events_that_modify_mandatory_access_controls_are_collected_fail(self, cmd): 25 | stdout = [''] 26 | stderr = [''] 27 | returncode = 1 28 | 29 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 30 | 31 | 32 | @patch.object(CISAudit, "_shellexec", mock_audit_events_that_modify_mandatory_access_controls_are_collected_pass) 33 | def test_audit_events_that_modify_mandatory_access_controls_are_collected_pass(): 34 | state = test.audit_events_that_modify_mandatory_access_controls_are_collected() 35 | assert state == 0 36 | 37 | 38 | @patch.object(CISAudit, "_shellexec", mock_audit_events_that_modify_mandatory_access_controls_are_collected_fail) 39 | def test_audit_events_that_modify_mandatory_access_controls_are_collected_fail(): 40 | state = test.audit_events_that_modify_mandatory_access_controls_are_collected() 41 | assert state == 3 42 | 43 | 44 | if __name__ == '__main__': 45 | pytest.main([__file__, '--no-cov']) 46 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_that_modify_network_environment_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | rules = [ 14 | '-a always,exit -F arch=b64 -S sethostname -S setdomainname -k system-locale', 15 | '-a always,exit -F arch=b32 -S sethostname -S setdomainname -k system-locale', 16 | '-w /etc/issue -p wa -k system-locale', 17 | '-w /etc/issue.net -p wa -k system-locale', 18 | '-w /etc/hosts -p wa -k system-locale', 19 | '-w /etc/sysconfig/network -p wa -k system-locale', 20 | ] 21 | 22 | for rule in rules: 23 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 24 | print(shellexec(f'auditctl {rule}')) 25 | 26 | yield None 27 | 28 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 29 | print(shellexec('auditctl -l')) 30 | 31 | os.remove('/etc/audit/rules.d/pytest.rules') 32 | shellexec('auditctl -D') 33 | 34 | 35 | def test_integration_audit_events_that_modify_network_environment_are_collected_pass(setup_to_pass): 36 | state = CISAudit().audit_events_that_modify_network_environment_are_collected() 37 | assert state == 0 38 | 39 | 40 | def test_integration_audit_events_that_modify_network_environment_are_collected_fail(): 41 | state = CISAudit().audit_events_that_modify_network_environment_are_collected() 42 | assert state == 3 43 | 44 | 45 | if __name__ == '__main__': 46 | pytest.main([__file__, '--no-cov']) 47 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_iptables_loopback_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('iptables -A INPUT -i lo -j ACCEPT') 13 | shellexec('iptables -A INPUT -s 127.0.0.1/8 -j DROP') 14 | shellexec('iptables -A OUTPUT -o lo -j ACCEPT') 15 | 16 | shellexec('ip6tables -A INPUT -i lo -j ACCEPT') 17 | shellexec('ip6tables -A INPUT -s ::1 -j DROP') 18 | shellexec('ip6tables -A OUTPUT -o lo -j ACCEPT') 19 | 20 | yield None 21 | 22 | ## Tear-down 23 | shellexec('iptables -F') 24 | shellexec('ip6tables -F') 25 | 26 | 27 | ## IPv4 28 | def test_integration_audit_iptables_loopback_is_configured_pass_ipv4(setup_to_pass): 29 | state = CISAudit().audit_iptables_loopback_is_configured(ip_version='ipv4') 30 | assert state == 0 31 | 32 | 33 | def test_integration_audit_iptables_loopback_is_configured_fail_ipv4(): 34 | state = CISAudit().audit_iptables_loopback_is_configured(ip_version='ipv4') 35 | assert state == 7 36 | 37 | 38 | ## IPv6 39 | def test_integration_audit_iptables_loopback_is_configured_pass_ipv6(setup_to_pass): 40 | state = CISAudit().audit_iptables_loopback_is_configured(ip_version='ipv6') 41 | assert state == 0 42 | 43 | 44 | def test_integration_audit_iptables_loopback_is_configured_fail_ipv6(): 45 | state = CISAudit().audit_iptables_loopback_is_configured(ip_version='ipv6') 46 | assert state == 7 47 | 48 | 49 | if __name__ == '__main__': 50 | pytest.main([__file__, '--no-cov']) 51 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_access_to_su_command_is_restricted.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture() 10 | def setup_access_to_su_command_is_restricted_pass(): 11 | ## Setup 12 | shellexec('echo "auth required pam_wheel.so use_uid group=pytest" >> /etc/pam.d/su') 13 | shellexec('echo "pytest:x:1000:" >> /etc/group') 14 | 15 | yield None 16 | 17 | ## Cleanup 18 | shellexec('sed -i "/pytest/d" /etc/pam.d/su') 19 | shellexec('sed -i "/pytest/d" /etc/group') 20 | 21 | 22 | @pytest.fixture() 23 | def setup_access_to_su_command_is_restricted_fail_with_no_users_in_group(): 24 | ## Setup 25 | shellexec('echo "auth required pam_wheel.so use_uid group=pytest" >> /etc/pam.d/su') 26 | 27 | yield None 28 | 29 | ## Cleanup 30 | shellexec('sed -i "/pytest/d" /etc/pam.d/su') 31 | 32 | 33 | def test_audit_access_to_su_command_is_restricted_pass(setup_access_to_su_command_is_restricted_pass): 34 | state = CISAudit().audit_access_to_su_command_is_restricted() 35 | assert state == 0 36 | 37 | 38 | def test_audit_access_to_su_command_is_restricted_fail(): 39 | state = CISAudit().audit_access_to_su_command_is_restricted() 40 | assert state == 1 41 | 42 | 43 | def test_audit_access_to_su_command_is_restricted_fail_with_no_users_in_group(setup_access_to_su_command_is_restricted_fail_with_no_users_in_group): 44 | state = CISAudit().audit_access_to_su_command_is_restricted() 45 | assert state == 2 46 | 47 | 48 | if __name__ == '__main__': 49 | pytest.main([__file__, '--no-cov']) 50 | -------------------------------------------------------------------------------- /tests/unit/test_audit_system_is_disabled_when_audit_logs_are_full.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_audit_system_is_disabled_when_audit_logs_are_full_pass(self, cmd): 12 | if '^space_left_action' in cmd: 13 | stdout = ['space_left_action = email'] 14 | elif '^action_mail_acct' in cmd: 15 | stdout = ['action_mail_acct = root'] 16 | elif '^admin_space_left_action' in cmd: 17 | stdout = ['admin_space_left_action = halt'] 18 | 19 | stderr = [''] 20 | returncode = 0 21 | 22 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 23 | 24 | 25 | def mock_audit_system_is_disabled_when_audit_logs_are_full_fail(self, cmd): 26 | stdout = [''] 27 | stderr = [''] 28 | returncode = 1 29 | 30 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 31 | 32 | 33 | test = CISAudit() 34 | 35 | 36 | @patch.object(CISAudit, "_shellexec", mock_audit_system_is_disabled_when_audit_logs_are_full_pass) 37 | def test_audit_system_is_disabled_when_audit_logs_are_full_pass(): 38 | state = test.audit_system_is_disabled_when_audit_logs_are_full() 39 | assert state == 0 40 | 41 | 42 | @patch.object(CISAudit, "_shellexec", mock_audit_system_is_disabled_when_audit_logs_are_full_fail) 43 | def test_audit_system_is_disabled_when_audit_logs_are_full_fail(): 44 | state = test.audit_system_is_disabled_when_audit_logs_are_full() 45 | assert state == 7 46 | 47 | 48 | if __name__ == '__main__': 49 | pytest.main([__file__, '--no-cov']) 50 | -------------------------------------------------------------------------------- /tests/unit/test_audit_cron_is_restricted_to_authorized_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_os_path_exists_pass(file): 12 | if file == '/etc/cron.deny': 13 | return False 14 | elif file == '/etc/cron.allow': 15 | return True 16 | else: 17 | raise Exception 18 | 19 | 20 | def mock_os_path_exists_fail(file): 21 | if file == '/etc/cron.deny': 22 | return True 23 | elif file == '/etc/cron.allow': 24 | return False 25 | else: 26 | raise Exception 27 | 28 | 29 | @patch.object(os.path, "exists", mock_os_path_exists_pass) 30 | @patch.object(CISAudit, "audit_file_permissions", return_value=0) 31 | def test_audit_cron_is_restricted_to_authorized_users_pass(*args): 32 | state = CISAudit().audit_cron_is_restricted_to_authorized_users() 33 | assert state == 0 34 | 35 | 36 | @patch.object(os.path, "exists", mock_os_path_exists_fail) 37 | @patch.object(CISAudit, "audit_file_permissions", return_value=1) 38 | def test_audit_cron_is_restricted_to_authorized_users_fail_exists(*args): 39 | state = CISAudit().audit_cron_is_restricted_to_authorized_users() 40 | assert state == 3 41 | 42 | 43 | @patch.object(os.path, "exists", return_value=True) 44 | @patch.object(CISAudit, "audit_file_permissions", return_value=1) 45 | def test_audit_cron_is_restricted_to_authorized_users_fail_permissions(*args): 46 | state = CISAudit().audit_cron_is_restricted_to_authorized_users() 47 | assert state == 5 48 | 49 | 50 | if __name__ == '__main__': 51 | pytest.main([__file__, '--no-cov']) 52 | -------------------------------------------------------------------------------- /tests/unit/test_audit_events_that_modify_usergroup_info_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_audit_events_that_modify_usergroup_info_are_collected_pass(self, cmd): 14 | stdout = [ 15 | '-w /etc/group -p wa -k identity', 16 | '-w /etc/passwd -p wa -k identity', 17 | '-w /etc/gshadow -p wa -k identity', 18 | '-w /etc/shadow -p wa -k identity', 19 | '-w /etc/security/opasswd -p wa -k identity', 20 | ] 21 | stderr = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | def mock_audit_events_that_modify_usergroup_info_are_collected_fail(self, cmd): 28 | stdout = [''] 29 | stderr = [''] 30 | returncode = 1 31 | 32 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 33 | 34 | 35 | @patch.object(CISAudit, "_shellexec", mock_audit_events_that_modify_usergroup_info_are_collected_pass) 36 | def test_audit_events_that_modify_usergroup_info_are_collected_pass(): 37 | state = test.audit_events_that_modify_usergroup_info_are_collected() 38 | assert state == 0 39 | 40 | 41 | @patch.object(CISAudit, "_shellexec", mock_audit_events_that_modify_usergroup_info_are_collected_fail) 42 | def test_audit_events_that_modify_usergroup_info_are_collected_fail(): 43 | state = test.audit_events_that_modify_usergroup_info_are_collected() 44 | assert state == 3 45 | 46 | 47 | if __name__ == '__main__': 48 | pytest.main([__file__, '--no-cov']) 49 | -------------------------------------------------------------------------------- /tests/unit/test_audit_package_is_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_package_installed(*args): 12 | output = ['pytest-0.0.0\n'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_package_not_installed(self, cmd): 20 | output = ['package pytest is not installed\n'] 21 | error = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_package_error(self, cmd): 28 | output = [''] 29 | error = {'rpm: no arguments given for query\n'} 30 | returncode = 1 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | test = CISAudit() 36 | package = 'pytest' 37 | 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_package_installed) 40 | def test_packages_are_installed_pass(): 41 | state = test.audit_package_is_installed(package='pytest') 42 | assert state == 0 43 | 44 | 45 | @patch.object(CISAudit, "_shellexec", mock_package_not_installed) 46 | def test_packages_are_installed_fail(): 47 | state = test.audit_package_is_installed(package='pytest') 48 | assert state == 1 49 | 50 | 51 | @patch.object(CISAudit, "_shellexec", mock_package_error) 52 | def test_packages_are_installed_error(): 53 | state = test.audit_package_is_installed(package='pytest') 54 | assert state == 1 55 | 56 | 57 | if __name__ == '__main__': 58 | pytest.main([__file__, '--no-cov']) 59 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_iptables_default_deny_policy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | ## Setup 12 | shellexec('iptables -P INPUT DROP') 13 | shellexec('iptables -P FORWARD DROP') 14 | shellexec('iptables -P OUTPUT DROP') 15 | shellexec('ip6tables -P INPUT DROP') 16 | shellexec('ip6tables -P FORWARD DROP') 17 | shellexec('ip6tables -P OUTPUT DROP') 18 | 19 | yield None 20 | 21 | shellexec('iptables -P INPUT ACCEPT') 22 | shellexec('iptables -P FORWARD ACCEPT') 23 | shellexec('iptables -P OUTPUT ACCEPT') 24 | shellexec('ip6tables -P INPUT ACCEPT') 25 | shellexec('ip6tables -P FORWARD ACCEPT') 26 | shellexec('ip6tables -P OUTPUT ACCEPT') 27 | 28 | 29 | def test_integration_audit_iptables_default_deny_pass_ipv4(setup_to_pass): 30 | state = CISAudit().audit_iptables_default_deny_policy(ip_version='ipv4') 31 | assert state == 0 32 | 33 | 34 | def test_integration_audit_iptables_default_deny_fail_ipv4(): 35 | state = CISAudit().audit_iptables_default_deny_policy(ip_version='ipv4') 36 | assert state == 7 37 | 38 | 39 | def test_integration_audit_iptables_default_deny_pass_ipv6(setup_to_pass): 40 | state = CISAudit().audit_iptables_default_deny_policy(ip_version='ipv6') 41 | assert state == 0 42 | 43 | 44 | def test_integration_audit_iptables_default_deny_fail_ipv6(): 45 | state = CISAudit().audit_iptables_default_deny_policy(ip_version='ipv6') 46 | assert state == 7 47 | 48 | 49 | if __name__ == '__main__': 50 | pytest.main([__file__, '--no-cov']) 51 | -------------------------------------------------------------------------------- /tests/unit/test_audit_password_change_minimum_delay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_password_expiration_min_days_is_configured_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | 17 | if 'PASS_MIN_DAYS' in cmd: 18 | stdout = ['PASS_MIN_DAYS 1'] 19 | elif 'shadow' in cmd: 20 | stdout = [ 21 | 'root:1', 22 | 'vagrant:1', 23 | ] 24 | 25 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 26 | 27 | 28 | def mock_password_expiration_min_days_is_configured_fail(self, cmd): 29 | returncode = 0 30 | stderr = [''] 31 | 32 | if 'PASS_MIN_DAYS' in cmd: 33 | stdout = ['PASS_MIN_DAYS 0'] 34 | elif 'shadow' in cmd: 35 | stdout = [ 36 | 'root:0', 37 | 'vagrant:0', 38 | ] 39 | 40 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 41 | 42 | 43 | @patch.object(CISAudit, "_shellexec", mock_password_expiration_min_days_is_configured_pass) 44 | def test_audit_password_expiration_min_days_is_configured_pass(): 45 | state = test.audit_password_change_minimum_delay() 46 | assert state == 0 47 | 48 | 49 | @patch.object(CISAudit, "_shellexec", mock_password_expiration_min_days_is_configured_fail) 50 | def test_audit_password_expiration_min_days_is_configured_pass_fail(): 51 | state = test.audit_password_change_minimum_delay() 52 | assert state == 3 53 | 54 | 55 | if __name__ == '__main__': 56 | pytest.main([__file__, '--no-cov']) 57 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_rsyslog_sends_logs_to_a_remote_log_host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass_1(): 13 | ## Setup 14 | shellexec("echo '*.* @@192.168.2.100' >> /etc/rsyslog.d/pytest.conf") 15 | 16 | yield None 17 | 18 | ## Tear-down 19 | os.remove('/etc/rsyslog.d/pytest.conf') 20 | 21 | 22 | @pytest.fixture 23 | def setup_to_pass_2(): 24 | ## Setup 25 | with open('/etc/rsyslog.d/pytest.conf', 'w') as f: 26 | f.writelines( 27 | [ 28 | '*.* action(type="omfwd" target="192.168.2.100" port="514" protocol="tcp"', 29 | ' action.resumeRetryCount="100"', 30 | ' queue.type="LinkedList" queue.size="1000")', 31 | '', 32 | ] 33 | ) 34 | 35 | yield None 36 | 37 | ## Tear-down 38 | os.remove('/etc/rsyslog.d/pytest.conf') 39 | 40 | 41 | def test_audit_rsyslog_sends_logs_to_a_remote_log_host_pass_type1(setup_to_pass_1): 42 | state = CISAudit().audit_rsyslog_sends_logs_to_a_remote_log_host() 43 | assert state == 0 44 | 45 | 46 | def test_audit_rsyslog_sends_logs_to_a_remote_log_host_pass_type2(setup_to_pass_2): 47 | state = CISAudit().audit_rsyslog_sends_logs_to_a_remote_log_host() 48 | assert state == 0 49 | 50 | 51 | def test_audit_rsyslog_sends_logs_to_a_remote_log_host_fail(): 52 | state = CISAudit().audit_rsyslog_sends_logs_to_a_remote_log_host() 53 | assert state == 1 54 | 55 | 56 | if __name__ == '__main__': 57 | pytest.main([__file__, '--no-cov']) 58 | -------------------------------------------------------------------------------- /tests/unit/test_output_text.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | 7 | results = [ 8 | ('1', 'section header'), 9 | ('1.1', 'subsection header'), 10 | ('1.1.1', 'test 1.1.1', 1, 'Pass', '1ms'), 11 | ('2', 'section header'), 12 | ('2.1', 'test 2.1', 1, 'Fail', '10ms'), 13 | ('2.2', 'test 2.2', 2, 'Pass', '100ms'), 14 | ('2.3', 'test 2.3', 1, 'Not Implemented'), 15 | ] 16 | 17 | 18 | def test_output_text(capsys): 19 | CISAudit().output_text(data=results) 20 | 21 | output, error = capsys.readouterr() 22 | print(output) 23 | 24 | assert error == '' 25 | assert output.split('\n')[0] == "ID Description Level Result Duration" 26 | assert output.split('\n')[1] == "----- ----------------- ----- --------------- --------" 27 | assert output.split('\n')[2] == "" 28 | assert output.split('\n')[3] == "1 section header " 29 | assert output.split('\n')[4] == "1.1 subsection header " 30 | assert output.split('\n')[5] == "1.1.1 test 1.1.1 1 Pass 1ms" 31 | assert output.split('\n')[6] == "" 32 | assert output.split('\n')[7] == "2 section header " 33 | assert output.split('\n')[8] == "2.1 test 2.1 1 Fail 10ms" 34 | assert output.split('\n')[9] == "2.2 test 2.2 2 Pass 100ms" 35 | assert output.split('\n')[10] == "2.3 test 2.3 1 Not Implemented " 36 | 37 | 38 | if __name__ == '__main__': 39 | pytest.main([__file__, '--no-cov', '-v']) 40 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_for_successful_file_system_mounts_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | file_rules = [ 14 | '-a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts', 15 | '-a always,exit -F arch=b32 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts', 16 | ] 17 | auditctl_rules = [ 18 | "-a always,exit -F arch=b64 -S mount -F 'auid>=1000' -F 'auid!=4294967295' -F key=mounts", 19 | "-a always,exit -F arch=b32 -S mount -F 'auid>=1000' -F 'auid!=4294967295' -F key=mounts", 20 | ] 21 | 22 | for rule in file_rules: 23 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 24 | 25 | for rule in auditctl_rules: 26 | print(shellexec(f'auditctl {rule}')) 27 | 28 | yield None 29 | 30 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 31 | print(shellexec('auditctl -l')) 32 | 33 | os.remove('/etc/audit/rules.d/pytest.rules') 34 | shellexec('auditctl -D') 35 | 36 | 37 | def test_integration_audit_events_for_successful_file_system_mounts_are_collected_pass(setup_to_pass): 38 | state = CISAudit().audit_events_for_successful_file_system_mounts_are_collected() 39 | assert state == 0 40 | 41 | 42 | def test_integration_audit_events_for_successful_file_system_mounts_are_collected_fail(): 43 | state = CISAudit().audit_events_for_successful_file_system_mounts_are_collected() 44 | assert state == 3 45 | 46 | 47 | if __name__ == '__main__': 48 | pytest.main([__file__, '--no-cov']) 49 | -------------------------------------------------------------------------------- /tests/unit/test_audit_password_expiration_warning_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_password_expiration_warning_is_configured_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | 17 | if 'PASS_WARN_AGE' in cmd: 18 | stdout = ['PASS_WARN_AGE 7'] 19 | elif 'shadow' in cmd: 20 | stdout = [ 21 | 'root:7', 22 | 'vagrant:7', 23 | ] 24 | 25 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 26 | 27 | 28 | def mock_password_expiration_warning_is_configured_fail(self, cmd): 29 | returncode = 0 30 | stderr = [''] 31 | 32 | if 'PASS_WARN_AGE' in cmd: 33 | stdout = ['PASS_WARN_AGE 0'] 34 | elif 'shadow' in cmd: 35 | stdout = [ 36 | 'root:0', 37 | 'vagrant:0', 38 | ] 39 | 40 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 41 | 42 | 43 | @patch.object(CISAudit, "_shellexec", mock_password_expiration_warning_is_configured_pass) 44 | def test_audit_password_expiration_warning_is_configured_pass(): 45 | state = test.audit_password_expiration_warning_is_configured() 46 | assert state == 0 47 | 48 | 49 | @patch.object(CISAudit, "_shellexec", mock_password_expiration_warning_is_configured_fail) 50 | def test_audit_password_expiration_warning_is_configured_pass_fail(): 51 | state = test.audit_password_expiration_warning_is_configured() 52 | assert state == 3 53 | 54 | 55 | if __name__ == '__main__': 56 | pytest.main([__file__, '--no-cov']) 57 | -------------------------------------------------------------------------------- /tests/unit/test_audit_password_expiration_max_days_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_password_expiration_max_days_is_configured_pass(self, cmd): 14 | returncode = 0 15 | stderr = [''] 16 | 17 | if 'PASS_MAX_DAYS' in cmd: 18 | stdout = ['PASS_MAX_DAYS 365'] 19 | elif 'shadow' in cmd: 20 | stdout = [ 21 | 'root:365', 22 | 'vagrant:365', 23 | ] 24 | 25 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 26 | 27 | 28 | def mock_password_expiration_max_days_is_configured_fail(self, cmd): 29 | returncode = 0 30 | stderr = [''] 31 | 32 | if 'PASS_MAX_DAYS' in cmd: 33 | stdout = ['PASS_MAX_DAYS 99999'] 34 | elif 'shadow' in cmd: 35 | stdout = [ 36 | 'root:99999', 37 | 'vagrant:99999', 38 | ] 39 | 40 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 41 | 42 | 43 | @patch.object(CISAudit, "_shellexec", mock_password_expiration_max_days_is_configured_pass) 44 | def test_audit_password_expiration_max_days_is_configured_pass(): 45 | state = test.audit_password_expiration_max_days_is_configured() 46 | assert state == 0 47 | 48 | 49 | @patch.object(CISAudit, "_shellexec", mock_password_expiration_max_days_is_configured_fail) 50 | def test_audit_password_expiration_max_days_is_configured_pass_fail(): 51 | state = test.audit_password_expiration_max_days_is_configured() 52 | assert state == 3 53 | 54 | 55 | if __name__ == '__main__': 56 | pytest.main([__file__, '--no-cov']) 57 | -------------------------------------------------------------------------------- /tests/unit/test_audit_service_active.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_active(*args, **kwargs): 12 | output = ['active'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_stopped(*args, **kwargs): 20 | output = ['inactive'] 21 | error = [''] 22 | returncode = 3 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_error(*args, **kwargs): 28 | output = [''] 29 | error = ['Failed to get unit file state for pytest.service: No such file or directory'] 30 | returncode = 1 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | class TestService: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | test_service = 'pytest' 39 | 40 | @patch.object(CISAudit, "_shellexec", mock_active) 41 | def test_service_active_pass(self): 42 | state = self.test.audit_service_is_active(service=self.test_service) 43 | assert state == 0 44 | 45 | @patch.object(CISAudit, "_shellexec", mock_stopped) 46 | def test_service_active_fail(self): 47 | state = self.test.audit_service_is_active(service=self.test_service) 48 | assert state == 1 49 | 50 | 51 | # @patch.object(CISAudit, "_shellexec", mock_error) 52 | # def test_service_active_error(self): 53 | # state = self.test.audit_service_is_active(service=self.test_service) 54 | # assert state == -1 55 | 56 | if __name__ == '__main__': 57 | pytest.main([__file__, '--no-cov']) 58 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_cron_is_restricted_to_authorized_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | shellexec('install -o root -g root -m 0600 /dev/null /etc/cron.allow') 14 | if os.path.exists('/etc/cron.deny'): 15 | os.remove('/etc/cron.deny') 16 | 17 | yield None 18 | 19 | os.remove('/etc/cron.allow') 20 | 21 | 22 | @pytest.fixture() 23 | def setup_to_fail_exists(): 24 | shellexec('touch /etc/cron.deny') 25 | if os.path.exists('/etc/cron.allow'): 26 | os.remove('/etc/cron.allow') 27 | 28 | yield None 29 | 30 | os.remove('/etc/cron.deny') 31 | 32 | 33 | @pytest.fixture() 34 | def setup_to_fail_permissions(): 35 | shellexec('touch /etc/cron.allow') 36 | if os.path.exists('/etc/cron.deny'): 37 | os.remove('/etc/cron.deny') 38 | 39 | yield None 40 | 41 | os.remove('/etc/cron.allow') 42 | 43 | 44 | def test_integration_audit_cron_is_restricted_to_authorized_users_pass(setup_to_pass): 45 | state = CISAudit().audit_cron_is_restricted_to_authorized_users() 46 | assert state == 0 47 | 48 | 49 | def test_integration_audit_cron_is_restricted_to_authorized_users_fail_exists(setup_to_fail_exists): 50 | state = CISAudit().audit_cron_is_restricted_to_authorized_users() 51 | assert state == 3 52 | 53 | 54 | def test_integration_audit_cron_is_restricted_to_authorized_users_fail_permissions(setup_to_fail_permissions): 55 | state = CISAudit().audit_cron_is_restricted_to_authorized_users() 56 | assert state == 4 57 | 58 | 59 | if __name__ == '__main__': 60 | pytest.main([__file__, '--no-cov']) 61 | -------------------------------------------------------------------------------- /tests/unit/test_audit_system_accounts_are_secured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_system_accounts_are_secured(self, cmd): 12 | if 'UID_MIN' in cmd: 13 | output = ['1000', ''] 14 | else: 15 | output = [ 16 | 'root:x:0:0:root:/root:/bin/bash', 17 | 'sync:x:5:0:sync:/sbin:/bin/sync', 18 | 'shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown', 19 | 'halt:x:7:0:halt:/sbin:/sbin/halt', 20 | 'nobody:x:99:99:Nobody:/:/sbin/nologin', 21 | 'vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash', 22 | ] 23 | error = [''] 24 | returncode = 0 25 | 26 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 27 | 28 | 29 | def mock_system_accounts_are_not_secured(self, cmd): 30 | if 'UID_MIN' in cmd: 31 | output = ['1000', ''] 32 | else: 33 | output = [ 34 | 'nobody:x:99:99:Nobody:/:/bin/bash', 35 | ] 36 | error = [''] 37 | returncode = 0 38 | 39 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 40 | 41 | 42 | test = CISAudit() 43 | 44 | 45 | @patch.object(CISAudit, "_shellexec", mock_system_accounts_are_secured) 46 | def test_system_accounts_are_secured(): 47 | state = test.audit_system_accounts_are_secured() 48 | assert state == 0 49 | 50 | 51 | @patch.object(CISAudit, "_shellexec", mock_system_accounts_are_not_secured) 52 | def test_system_accounts_are_not_secured(): 53 | state = test.audit_system_accounts_are_secured() 54 | assert state == 1 55 | 56 | 57 | if __name__ == '__main__': 58 | pytest.main([__file__, '--no-cov']) 59 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_shadow_group_is_empty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass_empty(): 11 | ## Setup 12 | shellexec('groupadd -r shadow') 13 | 14 | yield None 15 | 16 | ## Tear-down 17 | shellexec('groupdel shadow') 18 | 19 | 20 | @pytest.fixture 21 | def setup_to_fail_primary(): 22 | ## Setup 23 | shellexec('groupadd -r shadow') 24 | shellexec('useradd -g shadow pytest') 25 | 26 | yield None 27 | 28 | ## Tear-down 29 | shellexec('userdel pytest') 30 | shellexec('groupdel shadow') 31 | 32 | 33 | @pytest.fixture 34 | def setup_to_fail_supplementary(): 35 | ## Setup 36 | shellexec('groupadd -r shadow') 37 | shellexec('useradd -G shadow pytest') 38 | 39 | yield None 40 | 41 | ## Tear-down 42 | shellexec('userdel pytest') 43 | shellexec('groupdel shadow') 44 | 45 | 46 | def test_integration_audit_shadow_group_is_empty_pass_absent(): 47 | state = CISAudit().audit_shadow_group_is_empty() 48 | assert state == 0 49 | 50 | 51 | def test_integration_audit_shadow_group_is_empty_pass_empty(setup_to_pass_empty): 52 | state = CISAudit().audit_shadow_group_is_empty() 53 | assert state == 0 54 | 55 | 56 | def test_integration_audit_shadow_group_is_empty_fail_primary(setup_to_fail_primary): 57 | state = CISAudit().audit_shadow_group_is_empty() 58 | assert state == 2 59 | 60 | 61 | def test_integration_audit_shadow_group_is_empty_fail_supplementary(setup_to_fail_supplementary): 62 | state = CISAudit().audit_shadow_group_is_empty() 63 | assert state == 1 64 | 65 | 66 | if __name__ == '__main__': 67 | pytest.main([__file__, '--no-cov']) 68 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_for_file_deletion_by_users_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | file_rules = [ 14 | "-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=4294967295 -k delete", 15 | "-a always,exit -F arch=b32 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=4294967295 -k delete", 16 | ] 17 | auditctl_rules = [ 18 | "-a always,exit -F arch=b64 -S rename,unlink,unlinkat,renameat -F 'auid>=1000' -F 'auid!=4294967295' -F key=delete", 19 | "-a always,exit -F arch=b32 -S unlink,rename,unlinkat,renameat -F 'auid>=1000' -F 'auid!=4294967295' -F key=delete", 20 | ] 21 | 22 | for rule in file_rules: 23 | shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules') 24 | 25 | for rule in auditctl_rules: 26 | shellexec(f'auditctl {rule}') 27 | 28 | yield None 29 | 30 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 31 | print(shellexec('auditctl -l')) 32 | 33 | os.remove('/etc/audit/rules.d/pytest.rules') 34 | shellexec('auditctl -D') 35 | 36 | 37 | def test_integration_audit_events_for_file_deletion_by_users_are_collected_pass(setup_to_pass): 38 | state = CISAudit().audit_events_for_file_deletion_by_users_are_collected() 39 | assert state == 0 40 | 41 | 42 | def test_integration_audit_events_for_file_deletion_by_users_are_collected_fail(): 43 | state = CISAudit().audit_events_for_file_deletion_by_users_are_collected() 44 | assert state == 3 45 | 46 | 47 | if __name__ == '__main__': 48 | pytest.main([__file__, '--no-cov']) 49 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_for_kernel_module_loading_and_unloading_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | file_rules = [ 14 | '-w /sbin/insmod -p x -k modules', 15 | '-w /sbin/rmmod -p x -k modules', 16 | '-w /sbin/modprobe -p x -k modules', 17 | '-a always,exit -F arch=b64 -S init_module -S delete_module -k modules', 18 | ] 19 | auditctl_rules = [ 20 | '-w /sbin/insmod -p x -k modules', 21 | '-w /sbin/rmmod -p x -k modules', 22 | '-w /sbin/modprobe -p x -k modules', 23 | '-a always,exit -F arch=b64 -S init_module,delete_module -F key=modules', 24 | ] 25 | 26 | for rule in file_rules: 27 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 28 | 29 | for rule in auditctl_rules: 30 | print(shellexec(f'auditctl {rule}')) 31 | 32 | yield None 33 | 34 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 35 | print(shellexec('auditctl -l')) 36 | 37 | os.remove('/etc/audit/rules.d/pytest.rules') 38 | shellexec('auditctl -D') 39 | 40 | 41 | def test_integration_audit_events_for_kernel_module_loading_and_unloading_are_collected_pass(setup_to_pass): 42 | state = CISAudit().audit_events_for_kernel_module_loading_and_unloading_are_collected() 43 | assert state == 0 44 | 45 | 46 | def test_integration_audit_events_for_kernel_module_loading_and_unloading_are_collected_fail(): 47 | state = CISAudit().audit_events_for_kernel_module_loading_and_unloading_are_collected() 48 | assert state == 3 49 | 50 | 51 | if __name__ == '__main__': 52 | pytest.main([__file__, '--no-cov']) 53 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_events_for_system_administrator_commands_are_collected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture() 12 | def setup_to_pass(): 13 | file_rules = [ 14 | '-a exit,always -F arch=b64 -C euid!=uid -F euid=0 -F auid>=1000 -F auid!=4294967295 -S execve -k actions', 15 | '-a exit,always -F arch=b32 -C euid!=uid -F euid=0 -F auid>=1000 -F auid!=4294967295 -S execve -k actions', 16 | ] 17 | auditctl_rules = [ 18 | "-a exit,always -F arch=b64 -C euid!=uid -F euid=0 -F 'auid>=1000' -F 'auid!=4294967295' -S execve -F key=actions", 19 | "-a exit,always -F arch=b32 -C euid!=uid -F euid=0 -F 'auid>=1000' -F 'auid!=4294967295' -S execve -F key=actions", 20 | ] 21 | 22 | for rule in file_rules: 23 | print(shellexec(f'echo "{rule}" >> /etc/audit/rules.d/pytest.rules')) 24 | 25 | for rule in auditctl_rules: 26 | print(shellexec(f'auditctl {rule}')) 27 | 28 | yield None 29 | 30 | print(shellexec('cat /etc/audit/rules.d/pytest.rules')) 31 | print(shellexec('auditctl -l')) 32 | 33 | os.remove('/etc/audit/rules.d/pytest.rules') 34 | shellexec('auditctl -D') 35 | 36 | 37 | def test_integration_audit_events_for_system_administrator_commands_are_collected_pass(setup_to_pass): 38 | state = CISAudit().audit_events_for_system_administrator_commands_are_collected() 39 | assert state == 0 40 | 41 | 42 | def test_integration_audit_events_for_system_administrator_commands_are_collected_fail(): 43 | state = CISAudit().audit_events_for_system_administrator_commands_are_collected() 44 | assert state == 3 45 | 46 | 47 | if __name__ == '__main__': 48 | pytest.main([__file__, '--no-cov']) 49 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_system_is_disabled_when_audit_logs_are_full.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | 5 | import pytest 6 | 7 | from cis_audit import CISAudit 8 | from tests.integration import shellexec 9 | 10 | 11 | @pytest.fixture 12 | def setup_to_pass(): 13 | ## Setup 14 | shutil.copy('/etc/audit/auditd.conf', '/etc/audit/auditd.conf.bak') 15 | shellexec("sed -i '/^space_left_action/ s/=.*/= email/' /etc/audit/auditd.conf") 16 | # shellexec("sed -i '/^action_mail_acct/ s/=.*/= root/' /etc/audit/auditd.conf") 17 | shellexec("sed -i '/^admin_space_left_action/ s/=.*/= halt/' /etc/audit/auditd.conf") 18 | 19 | yield None 20 | 21 | ## Tear-down 22 | shutil.move('/etc/audit/auditd.conf.bak', '/etc/audit/auditd.conf') 23 | 24 | 25 | @pytest.fixture 26 | def setup_to_fail(): 27 | ## Setup 28 | shutil.copy('/etc/audit/auditd.conf', '/etc/audit/auditd.conf.bak') 29 | # shellexec("sed -i '/^space_left_action/ s/=.*/= email/' /etc/audit/auditd.conf") 30 | shellexec("sed -i '/^action_mail_acct/ s/=.*/= pytest/' /etc/audit/auditd.conf") 31 | # shellexec("sed -i '/^admin_space_left_action/ s/=.*/= halt/' /etc/audit/auditd.conf") 32 | 33 | yield None 34 | 35 | ## Tear-down 36 | shutil.move('/etc/audit/auditd.conf.bak', '/etc/audit/auditd.conf') 37 | 38 | 39 | def test_integration_audit_system_is_disabled_when_audit_logs_are_full_pass(setup_to_pass): 40 | state = CISAudit().audit_system_is_disabled_when_audit_logs_are_full() 41 | assert state == 0 42 | 43 | 44 | def test_integration_audit_system_is_disabled_when_audit_logs_are_full_fail(setup_to_fail): 45 | state = CISAudit().audit_system_is_disabled_when_audit_logs_are_full() 46 | assert state == 7 47 | 48 | 49 | if __name__ == '__main__': 50 | pytest.main([__file__, '--no-cov']) 51 | -------------------------------------------------------------------------------- /tests/unit/test_audit_selinux_mode_is_enforcing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_selinux_mode_is_enforcing_enforcing(self, cmd): 12 | stdout = ['enforcing'] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_selinux_mode_is_enforcing_permissive(self, cmd): 20 | stdout = ['permissive'] 21 | stderr = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | def mock_selinux_mode_is_enforcing_disabled(self, cmd): 28 | stdout = ['disabled'] 29 | stderr = [''] 30 | returncode = 0 31 | 32 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 33 | 34 | 35 | class TestSELinuxIsEnforcing: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_selinux_mode_is_enforcing_enforcing) 40 | def test_selinux_is_enforcing_enforcing_pass(self): 41 | state = self.test.audit_selinux_mode_is_enforcing() 42 | assert state == 0 43 | 44 | @patch.object(CISAudit, "_shellexec", mock_selinux_mode_is_enforcing_permissive) 45 | def test_selinux_is_enforcing_permissive_pass(self): 46 | state = self.test.audit_selinux_mode_is_enforcing() 47 | assert state == 3 48 | 49 | @patch.object(CISAudit, "_shellexec", mock_selinux_mode_is_enforcing_disabled) 50 | def test_selinux_is_enforcing_disabled_fail(self): 51 | state = self.test.audit_selinux_mode_is_enforcing() 52 | assert state == 3 53 | 54 | 55 | if __name__ == '__main__': 56 | pytest.main([__file__, '--no-cov']) 57 | -------------------------------------------------------------------------------- /tests/unit/test_audit_selinux_mode_not_disabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_selinux_mode_not_disabled_enforcing(self, cmd): 12 | stdout = ['enforcing'] 13 | stderr = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 17 | 18 | 19 | def mock_selinux_mode_not_disabled_permissive(self, cmd): 20 | stdout = ['permissive'] 21 | stderr = [''] 22 | returncode = 0 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | def mock_selinux_mode_not_disabled_disabled(self, cmd): 28 | stdout = ['disabled'] 29 | stderr = [''] 30 | returncode = 0 31 | 32 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 33 | 34 | 35 | class TestSELinuxNotDisabled: 36 | test = CISAudit() 37 | test_id = '1.1' 38 | 39 | @patch.object(CISAudit, "_shellexec", mock_selinux_mode_not_disabled_enforcing) 40 | def test_selinux_not_disabled_enforcing_pass(self): 41 | state = self.test.audit_selinux_mode_not_disabled() 42 | assert state == 0 43 | 44 | @patch.object(CISAudit, "_shellexec", mock_selinux_mode_not_disabled_permissive) 45 | def test_selinux_not_disabled_permissive_pass(self): 46 | state = self.test.audit_selinux_mode_not_disabled() 47 | assert state == 0 48 | 49 | @patch.object(CISAudit, "_shellexec", mock_selinux_mode_not_disabled_disabled) 50 | def test_selinux_not_disabled_disabled_fail(self): 51 | state = self.test.audit_selinux_mode_not_disabled() 52 | assert state == 3 53 | 54 | 55 | if __name__ == '__main__': 56 | pytest.main([__file__, '--no-cov']) 57 | -------------------------------------------------------------------------------- /tests/unit/test_audit_chrony_is_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_chrony_configured_pass(self, cmd): 12 | stderr = [''] 13 | returncode = 0 14 | 15 | if 'is-enabled' in cmd: 16 | stdout = ['enabled'] 17 | elif 'is-active' in cmd: 18 | stdout = ['active'] 19 | elif 'server' in cmd: 20 | stdout = ['server 0.centos.pool.ntp.org iburst', 'server 1.centos.pool.ntp.org iburst', 'server 2.centos.pool.ntp.org iburst', 'server 3.centos.pool.ntp.org iburst'] 21 | elif 'ps aux' in cmd: 22 | stdout = ['chrony'] 23 | 24 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 25 | 26 | 27 | def mock_chrony_configured_fail(self, cmd): 28 | returncode = 0 29 | stderr = [''] 30 | stdout = [''] 31 | 32 | if 'is-enabled' in cmd: 33 | stdout = ['disabled'] 34 | elif 'is-active' in cmd: 35 | stdout = ['inactive'] 36 | elif 'server' in cmd: 37 | returncode = 1 38 | elif 'ps aux' in cmd: 39 | returncode = 1 40 | 41 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 42 | 43 | 44 | test = CISAudit() 45 | 46 | 47 | class TestChronyIsConfigured: 48 | @patch.object(CISAudit, "_shellexec", mock_chrony_configured_pass) 49 | def test_chrony_is_configure_pass(self): 50 | state = test.audit_chrony_is_configured() 51 | assert state == 0 52 | 53 | @patch.object(CISAudit, "_shellexec", mock_chrony_configured_fail) 54 | def test_chrony_is_configure_fail(self): 55 | state = test.audit_chrony_is_configured() 56 | assert state == 15 57 | 58 | 59 | if __name__ == '__main__': 60 | pytest.main([__file__, '--no-cov']) 61 | -------------------------------------------------------------------------------- /tests/unit/test_audit_bootloader_password_is_set.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_bootloader_password_pass(self, cmd): 12 | output = ['GRUB2_PASSWORD=supersecret'] 13 | error = [''] 14 | returncode = 0 15 | 16 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 17 | 18 | 19 | def mock_bootloader_password_fail_blank(self, cmd): 20 | output = [''] 21 | error = [''] 22 | returncode = 1 23 | 24 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 25 | 26 | 27 | def mock_bootloader_password_fail_commented(self, cmd): 28 | output = ['#GRUB2_PASSWORD=supersecret'] 29 | error = [''] 30 | returncode = 0 31 | 32 | return SimpleNamespace(stdout=output, stderr=error, returncode=returncode) 33 | 34 | 35 | def mock_bootloader_password_error(self, cmd): 36 | raise Exception 37 | 38 | 39 | class TestBootloaderPasswordSet: 40 | test = CISAudit() 41 | 42 | @patch.object(CISAudit, "_shellexec", mock_bootloader_password_pass) 43 | def test_bootloader_password_set_pass(self): 44 | state = self.test.audit_bootloader_password_is_set() 45 | assert state == 0 46 | 47 | @patch.object(CISAudit, "_shellexec", mock_bootloader_password_fail_blank) 48 | def test_bootloader_password_set_fail_blank(self): 49 | state = self.test.audit_bootloader_password_is_set() 50 | assert state == 1 51 | 52 | @patch.object(CISAudit, "_shellexec", mock_bootloader_password_fail_commented) 53 | def test_bootloader_password_set_fail_commented(self): 54 | state = self.test.audit_bootloader_password_is_set() 55 | assert state == 1 56 | 57 | 58 | if __name__ == '__main__': 59 | pytest.main([__file__, '--no-cov']) 60 | -------------------------------------------------------------------------------- /tests/unit/test_audit_updates_installed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | test = CISAudit() 11 | 12 | 13 | def mock_updates_pass(*args, **kwargs): 14 | stdout = [''] 15 | stderr = [''] 16 | returncode = 0 17 | 18 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 19 | 20 | 21 | def mock_updates_fail(*args, **kwargs): 22 | stdout = [ 23 | 'kernel.x86_64 3.10.0-1160.59.1.el7 updates', 24 | 'kernel-tools.x86_64 3.10.0-1160.59.1.el7 updates', 25 | 'kernel-tools-libs.x86_64 3.10.0-1160.59.1.el7 updates', 26 | ] 27 | stderr = [''] 28 | returncode = 100 29 | 30 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 31 | 32 | 33 | def mock_updates_error(*args, **kwargs): 34 | stdout = ['Loaded plugins: fastestmirror'] 35 | stderr = ['No such command: checkupdate. Please use /bin/yum --help'] 36 | returncode = 1 37 | 38 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 39 | 40 | 41 | @patch.object(CISAudit, "_shellexec", mock_updates_pass) 42 | def test_audit_updates_installed_pass(): 43 | state = test.audit_updates_installed() 44 | assert state == 0 45 | 46 | 47 | @patch.object(CISAudit, "_shellexec", mock_updates_fail) 48 | def test_audit_updates_installed_fail(): 49 | state = test.audit_updates_installed() 50 | assert state == 1 51 | 52 | 53 | @patch.object(CISAudit, "_shellexec", mock_updates_error) 54 | def test_audit_updates_installed_error(): 55 | state = test.audit_updates_installed() 56 | assert state == -1 57 | 58 | 59 | if __name__ == '__main__': 60 | pytest.main([__file__, '--no-cov']) 61 | -------------------------------------------------------------------------------- /tests/integration/test_integration_audit_nftables_base_chain_exists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | 5 | from cis_audit import CISAudit 6 | from tests.integration import shellexec 7 | 8 | 9 | @pytest.fixture 10 | def setup_to_pass(): 11 | shellexec(r'nft create table inet filter') 12 | shellexec(r'nft create chain inet filter input { type filter hook input priority 0 \; }') 13 | shellexec(r'nft create chain inet filter forward { type filter hook forward priority 0 \; }') 14 | shellexec(r'nft create chain inet filter output { type filter hook output priority 0 \; }') 15 | 16 | yield None 17 | 18 | shellexec('nft delete table inet filter') 19 | 20 | 21 | @pytest.fixture 22 | def setup_to_fail(): 23 | shellexec('nft flush chain inet filter input') 24 | shellexec('nft flush chain inet filter forward') 25 | shellexec('nft flush chain inet filter output') 26 | 27 | shellexec('nft delete chain inet filter input') 28 | shellexec('nft delete chain inet filter forward') 29 | shellexec('nft delete chain inet filter output') 30 | 31 | print(shellexec('nft list ruleset')) 32 | 33 | yield None 34 | 35 | shellexec(r'nft create chain inet filter input { type filter hook input priority 0 \; }') 36 | shellexec(r'nft create chain inet filter forward { type filter hook forward priority 0 \; }') 37 | shellexec(r'nft create chain inet filter output { type filter hook output priority 0 \; }') 38 | 39 | 40 | def test_integration_audit_nftables_base_chains_exist_pass(setup_install_nftables): 41 | state = CISAudit().audit_nftables_base_chains_exist() 42 | assert state == 0 43 | 44 | 45 | def test_integration_audit_nftables_base_chains_exist_fail(setup_install_nftables, setup_to_fail): 46 | state = CISAudit().audit_nftables_base_chains_exist() 47 | assert state == 7 48 | 49 | 50 | if __name__ == '__main__': 51 | pytest.main([__file__, '--no-cov']) 52 | -------------------------------------------------------------------------------- /tests/unit/test_audit_nftables_connections_are_configured.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from types import SimpleNamespace 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | from cis_audit import CISAudit 9 | 10 | 11 | def mock_nftables_connections_are_configured_pass(self, cmd): 12 | returncode = 0 13 | stderr = [''] 14 | 15 | if 'input' in cmd: 16 | stdout = [ 17 | 'ip protocol tcp ct state established accept', 18 | 'ip protocol udp ct state established accept', 19 | 'ip protocol icmp ct state established accept', 20 | ] 21 | elif 'output' in cmd: 22 | stdout = [ 23 | 'ip protocol tcp ct state established,related,new accept', 24 | 'ip protocol udp ct state established,related,new accept', 25 | 'ip protocol icmp ct state established,related,new accept', 26 | ] 27 | else: 28 | stdout = [''] 29 | returncode = 1 30 | 31 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 32 | 33 | 34 | def mock_nftables_connections_are_configured_fail(self, cmd): 35 | stdout = [''] 36 | stderr = [''] 37 | returncode = 1 38 | 39 | return SimpleNamespace(returncode=returncode, stderr=stderr, stdout=stdout) 40 | 41 | 42 | test = CISAudit() 43 | 44 | 45 | @patch.object(CISAudit, "_shellexec", mock_nftables_connections_are_configured_pass) 46 | def test_audit_nftables_connections_are_configured_pass(): 47 | state = test.audit_nftables_outbound_and_established_connections() 48 | assert state == 0 49 | 50 | 51 | @patch.object(CISAudit, "_shellexec", mock_nftables_connections_are_configured_fail) 52 | def test_audit_nftables_connections_are_configured_fail_all(): 53 | state = test.audit_nftables_outbound_and_established_connections() 54 | assert state == 3 55 | 56 | 57 | if __name__ == '__main__': 58 | pytest.main([__file__, '--no-cov']) 59 | -------------------------------------------------------------------------------- /tests/unit/test_audit_gdm_login_banner_configure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from unittest.mock import mock_open, patch 5 | 6 | import pytest 7 | 8 | # from cis_audit import CISAudit 9 | import cis_audit 10 | 11 | test = cis_audit.CISAudit() 12 | 13 | 14 | def mock_audit_package_is_installed_true(*args, **kwargs): 15 | return 0 16 | 17 | 18 | def mock_audit_package_is_installed_false(*args, **kwargs): 19 | return 1 20 | 21 | 22 | @patch.object(cis_audit.CISAudit, "audit_package_is_installed", mock_audit_package_is_installed_false) 23 | def test_audit_gdm_login_banner_configured_skipped(): 24 | state = test.audit_gdm_login_banner_configured() 25 | assert state == -2 26 | 27 | 28 | @patch.object(cis_audit.CISAudit, "audit_package_is_installed", mock_audit_package_is_installed_true) 29 | def test_audit_gdm_login_banner_configured_fail_files_not_found(): 30 | state = test.audit_gdm_login_banner_configured() 31 | assert state == 17 32 | 33 | 34 | @patch.object(cis_audit, "open", mock_open()) 35 | @patch.object(os.path, "exists", return_value=True) 36 | @patch.object(cis_audit.CISAudit, "audit_package_is_installed", mock_audit_package_is_installed_true) 37 | def test_audit_gdm_login_banner_configured_fail(MagickMock): 38 | state = test.audit_gdm_login_banner_configured() 39 | assert state == 46 40 | 41 | 42 | @patch.object(cis_audit, "open", mock_open(read_data='user-db:user\nsystem-db:gdm\nfile-db:/usr/share/gdm/greeter-dconf-defaults\n[org/gnome/login-screen]\nbanner-message-enable=true\nbanner-message-text=')) 43 | @patch.object(os.path, "exists", return_value=True) 44 | @patch.object(cis_audit.CISAudit, "audit_package_is_installed", mock_audit_package_is_installed_true) 45 | def test_audit_gdm_login_banner_configured_pass(MagickMock): 46 | state = test.audit_gdm_login_banner_configured() 47 | assert state == 0 48 | 49 | 50 | if __name__ == '__main__': 51 | pytest.main([__file__, '--no-cov']) 52 | --------------------------------------------------------------------------------