diff --git a/conf/remediation.py b/conf/remediation.py index 700babb..21ea943 100644 --- a/conf/remediation.py +++ b/conf/remediation.py @@ -39,4 +39,18 @@ def excludes(): 'enable_fips_mode', ] + # RHEL Image Mode + # TODO: revisit these, see which ones we really need for use with Contest + if test_name.startswith('/hardening/container'): + rules += [ + 'no_direct_root_logins', + 'firewalld_sshd_disabled', + 'service_sshd_disabled', + 'sshd_disable_root_login', + 'mount_option_nodev_nonroot_local_partitions', + 'enable_fips_mode', + 'configure_crypto_policy', + 'accounts_tmout', + ] + return rules diff --git a/hardening/container/bootc-image-builder/main.fmf b/hardening/container/bootc-image-builder/main.fmf new file mode 100644 index 0000000..74faef7 --- /dev/null +++ b/hardening/container/bootc-image-builder/main.fmf @@ -0,0 +1,105 @@ +summary: Runs bootc-image-builder remediation and scan inside VMs +test: python3 -m lib.runtest ./test.py +result: custom +environment+: + PYTHONPATH: ../../.. +duration: 1h +require+: + # virt library dependencies + - libvirt-daemon + - libvirt-daemon-driver-qemu + - libvirt-daemon-driver-storage-core + - libvirt-daemon-driver-network + - firewalld + - qemu-kvm + - libvirt-client + - virt-install + - rpm-build + - createrepo + # podman library dependencies + - podman +extra-hardware: | + keyvalue = HVM=1 + hostrequire = memory>=3720 +adjust: + - when: arch != x86_64 + enabled: false + because: we want to run virtualization on x86_64 only + - when: distro ~< rhel-8.10 or distro ~< rhel-9.5 + enabled: false + because: TODO - what is bootc supported on? + +/anssi_bp28_high: + +/anssi_bp28_enhanced: + tag+: + - subset-profile + +/anssi_bp28_intermediary: + tag+: + - subset-profile + +/anssi_bp28_minimal: + tag+: + - subset-profile + +/cis: + +/cis_server_l1: + tag+: + - subset-profile + +/cis_workstation_l2: + +/cis_workstation_l1: + tag+: + - subset-profile + +/cui: + adjust+: + - when: distro >= rhel-10 + enabled: false + because: there is no CUI profile on RHEL-10+ + +/e8: + +/hipaa: + +/ism_o: + +/ospp: + adjust+: + - when: distro >= rhel-10 + enabled: false + because: there is no OSPP profile on RHEL-10+ + +/pci-dss: + +/stig: + +/stig_gui: + adjust+: + - enabled: false + because: not supported without GUI, use stig instead + +/ccn_advanced: + adjust+: + - when: distro == rhel-8 or distro == rhel-10 + enabled: false + because: CCN profiles are not present on RHEL-8 and on RHEL-10 + +/ccn_intermediate: + tag+: + - subset-profile + adjust+: + - when: distro == rhel-8 or distro == rhel-10 + enabled: false + because: CCN profiles are not present on RHEL-8 and on RHEL-10 + +/ccn_basic: + tag+: + - subset-profile + adjust+: + - when: distro == rhel-8 or distro == rhel-10 + enabled: false + because: CCN profiles are not present on RHEL-8 and on RHEL-10 diff --git a/hardening/container/bootc-image-builder/test.py b/hardening/container/bootc-image-builder/test.py new file mode 100755 index 0000000..7e55a8f --- /dev/null +++ b/hardening/container/bootc-image-builder/test.py @@ -0,0 +1,114 @@ +#!/usr/bin/python3 + +#import contextlib +import shutil +from pathlib import Path + +from lib import results, oscap, osbuild, virt, podman, util +from conf import remediation + +# +# TODO: specify --root and --storage-opt for all podman commands, +# have ie. /var/lib/containers/contest-storage to not conflict +# with other images (image names) that might be on the system +# while having a storage share-able across tests +# + +#podman.Host.setup() +virt.Host.setup() + +_, variant, profile = util.get_test_name().rsplit('/', 2) + +oscap.unselect_rules(util.get_datastream(), 'remediation-ds.xml', remediation.excludes()) + + +pull_images = [ + 'quay.io/centos-bootc/centos-bootc:stream9', + 'quay.io/centos-bootc/bootc-image-builder:latest', +] +for img in pull_images: + # --quiet because screen-redrawing progress bars don't work well with logs + podman.podman('image', 'pull', '--quiet', img) + + +# TODO: Containerfile, needs 'podman' module support for 'class Repository' +# using a locally-hosted HTTP server +# TODO: probably use localhost/ for pulled images ? +containerfile_text = util.dedent(fr''' + FROM quay.io/centos-bootc/centos-bootc:stream9 + RUN ["dnf", "-y", "install", "dnf-plugins-core"] + RUN ["dnf", "-y", "copr", "enable", "packit/OpenSCAP-openscap-2170", "centos-stream-9-x86_64"] + RUN ["dnf", "-y", "install", "openscap-utils"] + COPY remediation-ds.xml /root/. + RUN ["oscap-bootc", "--profile", "{profile}", "/root/remediation-ds.xml"] +''') + +Path('Containerfile').write_text(containerfile_text) +podman.podman('image', 'build', '--tag', 'bootc-centos-openscap', '.') + + +guest = virt.Guest() +guest.wipe() +guest.generate_ssh_keypair() + +# TODO: probably move this to class Containerfile, managed by the 'podman' module, +# so sshkey insertion is generic across all container-based workflows +blueprint = osbuild.Blueprint(template='') +blueprint.add_user('root', password=virt.GUEST_LOGIN_PASS, ssh_pubkey=guest.ssh_pubkey) + +#c = podman.Container('quay.io/centos-bootc/bootc-image-builder:latest') + + +bootc_output_dir = Path(virt.GUEST_IMG_DIR) / 'bootc-image-builder-output' +if bootc_output_dir.exists(): + shutil.rmtree(bootc_output_dir) +bootc_output_dir.mkdir(parents=True) + +#with contextlib.ExitStack() as stack: +#with tempfile.NamedTemporaryFile(mode='w', suffix='.toml') as config_toml: +# Path(config_toml).write_text(blueprint + +with blueprint.to_tmpfile() as config_toml: + # TODO: maybe refer to pulled images as localhost/ so they don't get re-queried? + # (and drop --pull never) + podman.podman( + 'container', 'run', + '--rm', + '--pull', 'never', + '--privileged', + '--security-opt', 'label=type:unconfined_t', + '--volume', f'{config_toml}:/config.toml:ro', + '--volume', f'{bootc_output_dir}:/output', + '--volume', '/var/lib/containers/storage:/var/lib/containers/storage', + 'quay.io/centos-bootc/bootc-image-builder:latest', + 'build', + '--type', 'qcow2', + '--local', + 'localhost/bootc-centos-openscap:latest', + ) +# 'quay.io/centos-bootc/centos-bootc:stream9', + +# seems to be hardcoded by bootc-image-builder +qcow2_path = bootc_output_dir / 'qcow2' / 'disk.qcow2' + +guest.import_image(qcow2_path, 'qcow2') + + +with guest.booted(): + # copy the original DS to the guest + guest.copy_to(util.get_datastream(), 'scan-ds.xml') + # scan the remediated system + proc, lines = guest.ssh_stream( + f'oscap xccdf eval --profile {profile} --progress --report report.html' + f' --results-arf results-arf.xml scan-ds.xml' + ) + oscap.report_from_verbose(lines) + if proc.returncode not in [0,2]: + raise RuntimeError("post-reboot oscap failed unexpectedly") + + guest.copy_from('report.html') + guest.copy_from('results-arf.xml') + +util.subprocess_run(['gzip', '-9', 'results-arf.xml'], check=True) + +results.report_and_exit(logs=['report.html', 'results-arf.xml.gz']) diff --git a/lib/podman.py b/lib/podman.py new file mode 100644 index 0000000..7f2c202 --- /dev/null +++ b/lib/podman.py @@ -0,0 +1,16 @@ +""" +Provides utilities and wrappers for creating and manipulating images and +containers using the 'podman' utility. +""" + +from lib import util + + +def podman(*args, **kwargs): + """ + A simple wrapper for the podman(1) CLI, passing python arguments + as shell arguments. + """ + # TODO: make subprocess_run able to pass skip_frames to underlying calls, + # and use it here, to print out our caller, not podman.podman() + util.subprocess_run(['podman', *args], check=True, universal_newlines=True, **kwargs)