├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .packit.yaml ├── .pep8speaks.yml ├── AUTHORS ├── COPYING ├── Makefile ├── README.md ├── create_update_image.sh ├── data ├── org.fedoraproject.Anaconda.Addons.OSCAP.conf └── org.fedoraproject.Anaconda.Addons.OSCAP.service ├── docs └── manual │ └── developer_guide.adoc ├── org_fedora_oscap ├── __init__.py ├── common.py ├── constants.py ├── content_discovery.py ├── content_handling.py ├── cpioarchive.py ├── data_fetch.py ├── gui │ ├── __init__.py │ └── spokes │ │ ├── __init__.py │ │ ├── oscap.glade │ │ └── oscap.py ├── rule_handling.py ├── scap_content_handler.py ├── service │ ├── __init__.py │ ├── __main__.py │ ├── installation.py │ ├── kickstart.py │ ├── oscap.py │ └── oscap_interface.py ├── structures.py └── utils.py ├── oscap-anaconda-addon.spec ├── po ├── Makefile └── oscap-anaconda-addon.pot ├── testing_files ├── README.md ├── basic_fedora_kickstart.cfg ├── check.sh ├── cpe-dict.xml ├── customized_stig-1-1.noarch.rpm ├── ds-with-tailoring.zip ├── run_oscap_test.sh ├── scap-mycheck-oval.xml ├── scap-security-guide.noarch.rpm ├── separate-scap-files-1-1.noarch.rpm ├── separate-scap-files.zip ├── single-ds-1-1.noarch.rpm ├── single-ds.zip ├── ssg-fedora-ds-tailoring-1-1.noarch.rpm ├── tailoring.xml ├── test_report_anaconda_fixes.xccdf.xml ├── testing_ds.xml ├── testing_ks.cfg ├── testing_xccdf.xml ├── xccdf-1.1.xml ├── xccdf-with-tailoring-1-1.noarch.rpm ├── xccdf-with-tailoring.zip └── xccdf.xml ├── tests ├── Dockerfile ├── data │ └── file ├── test_common.py ├── test_content_handling.py ├── test_content_paths.py ├── test_data_fetch.py ├── test_installation.py ├── test_interface.py ├── test_kickstart.py ├── test_rule_handling.py ├── test_scap_content_handler.py ├── test_service_kickstart.py └── test_utils.py └── tools ├── make-language-patch └── pre-push /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Gating 2 | on: 3 | push: 4 | branches: [ rawhide ] 5 | pull_request: 6 | branches: [ rawhide ] 7 | jobs: 8 | validate-rawhide: 9 | name: Unit Tests on Fedora (Container) 10 | if: github.ref == 'refs/heads/rawhide' || github.event.pull_request.base.ref == 'rawhide' 11 | runs-on: ubuntu-latest 12 | container: 13 | image: fedora:rawhide 14 | steps: 15 | - name: Install Deps 16 | run: dnf install -y make anaconda python3-pytest python3-pycurl openscap-scanner 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Test 20 | run: make unittest 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.tar* 3 | *.rpm 4 | *.pyc 5 | *~ 6 | \#* 7 | .#* 8 | *.orig 9 | updates* 10 | update.img 11 | testing_files/testing_results.xml 12 | po/*.po 13 | .idea/ 14 | .ropeproject/ 15 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | downstream_package_name: oscap-anaconda-addon 2 | upstream_package_name: oscap-anaconda-addon 3 | specfile_path: oscap-anaconda-addon.spec 4 | 5 | actions: 6 | get-current-version: 7 | - bash -c "grep '^\s*VERSION\s*=\s*' Makefile | sed 's/VERSION\s*=\s*//'" 8 | 9 | srpm_build_deps: 10 | - bash 11 | 12 | jobs: 13 | - job: copr_build 14 | trigger: pull_request 15 | metadata: 16 | targets: 17 | - fedora-all 18 | 19 | -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | pycodestyle: 2 | max-line-length: 99 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Vratislav Podzimek - original author of the addon 2 | Ignacio Vazquez-Abrams - original author of the cpio module which has been merged into the addon 3 | See https://github.com/OpenSCAP/oscap-anaconda-addon/graphs/contributors for an exhaustive list of other contributors. 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = oscap-anaconda-addon 2 | 3 | VERSION = 0.37.0 4 | 5 | ADDON = org_fedora_oscap 6 | TESTS = tests \ 7 | testing_files 8 | 9 | DEFAULT_INSTALL_OF_PO_FILES ?= yes 10 | 11 | PYVERSION = -3 12 | 13 | TRANSLATIONS_DIR ?= po 14 | 15 | FILES = $(ADDON) \ 16 | $(TESTS) \ 17 | data \ 18 | po \ 19 | COPYING \ 20 | Makefile \ 21 | README.md 22 | 23 | EXCLUDES = \ 24 | *~ \ 25 | *.pyc 26 | 27 | L10N_REPO_RELATIVE_PATH ?= OpenSCAP/oscap-anaconda-addon-l10n.git 28 | L10N_REPOSITORY ?= https://github.com/$(L10N_REPO_RELATIVE_PATH) 29 | L10N_REPOSITORY_RW ?= git@github.com:$(L10N_REPO_RELATIVE_PATH) 30 | # Branch used in anaconda-l10n repository. 31 | # This should be master all the time, unless you are testing translation PRs. 32 | GIT_L10N_BRANCH ?= master 33 | # The base branch, used to pair code with translations 34 | OAA_PARENT_BRANCH ?= rawhide 35 | 36 | all: 37 | 38 | DISTNAME = $(NAME)-$(VERSION) 39 | ADDONDIR = /usr/share/anaconda/addons/ 40 | SERVICEDIR = /usr/share/anaconda/dbus/services/ 41 | CONFDIR = /usr/share/anaconda/dbus/confs/ 42 | DISTBALL = $(DISTNAME).tar.gz 43 | NUM_PROCS = $$(getconf _NPROCESSORS_ONLN) 44 | 45 | install: 46 | mkdir -p $(DESTDIR)$(ADDONDIR) 47 | mkdir -p $(DESTDIR)$(SERVICEDIR) 48 | mkdir -p $(DESTDIR)$(CONFDIR) 49 | cp -rv $(ADDON) $(DESTDIR)$(ADDONDIR) 50 | install -c -m 644 data/*.service $(DESTDIR)$(SERVICEDIR) 51 | install -c -m 644 data/*.conf $(DESTDIR)$(CONFDIR) 52 | ifeq ($(DEFAULT_INSTALL_OF_PO_FILES),yes) 53 | $(MAKE) install-po-files 54 | endif 55 | 56 | uninstall: 57 | rm -rfv $(DESTDIR)$(ADDONDIR) 58 | 59 | dist: 60 | rm -rf $(DISTNAME) 61 | mkdir -p $(DISTNAME) 62 | @if test -d ".git"; \ 63 | then \ 64 | echo Creating ChangeLog && \ 65 | ( cd "$(top_srcdir)" && \ 66 | echo '# Generate automatically. Do not edit.'; echo; \ 67 | git log --stat --date=short ) > ChangeLog.tmp \ 68 | && mv -f ChangeLog.tmp $(DISTNAME)/ChangeLog \ 69 | || ( rm -f ChangeLog.tmp ; \ 70 | echo Failed to generate ChangeLog >&2 ); \ 71 | else \ 72 | echo A git clone is required to generate a ChangeLog >&2; \ 73 | fi 74 | for file in $(FILES); do \ 75 | cp -rpv $$file $(DISTNAME)/$$file; \ 76 | done 77 | for excl in $(EXCLUDES); do \ 78 | find $(DISTNAME) -name "$$excl" -delete; \ 79 | done 80 | tar -czvf $(DISTBALL) $(DISTNAME) 81 | rm -rf $(DISTNAME) 82 | 83 | potfile: 84 | $(MAKE) -C po potfile 85 | 86 | # po-pull and update-pot are "inspired" by corresponding Anaconda code at 87 | # https://github.com/rhinstaller/anaconda/blob/master/Makefile.am 88 | # Our use case is slightly simpler (only one pot file), but we don't use automake, 89 | # so there have to be some differences. 90 | 91 | po-pull: 92 | TEMP_DIR=$$(mktemp --tmpdir -d oscap-anaconda-addon-l10n-XXXXXXXXXX) && \ 93 | git clone --depth 1 -b $(GIT_L10N_BRANCH) -- $(L10N_REPOSITORY) $$TEMP_DIR && \ 94 | mkdir -p $(TRANSLATIONS_DIR) && \ 95 | cp $$TEMP_DIR/$(OAA_PARENT_BRANCH)/*.po $(TRANSLATIONS_DIR)/ && \ 96 | rm -rf $$TEMP_DIR 97 | 98 | # This algorithm will make these steps: 99 | # - clone localization repository 100 | # - copy pot file to this repository 101 | # - check if pot file is changed (ignore the POT-Creation-Date otherwise it's always changed) 102 | # - if not changed: 103 | # - remove cloned repository 104 | # - if changed: 105 | # - add pot file 106 | # - commit pot file 107 | # - tell user to verify this file and push to the remote from the temp dir 108 | POTFILE_BASENAME = oscap-anaconda-addon.pot 109 | update-pot: 110 | $(MAKE) -C po potfile 111 | TEMP_DIR=$$(mktemp --tmpdir -d oscap-anaconda-addon-l10n-XXXXXXXXXX) || exit 1 ; \ 112 | git clone --depth 1 -b $(GIT_L10N_BRANCH) -- $(L10N_REPOSITORY_RW) $$TEMP_DIR || exit 2 ; \ 113 | mkdir -p $$TEMP_DIR/$(OAA_PARENT_BRANCH) ; \ 114 | cp po/$(POTFILE_BASENAME) $$TEMP_DIR/$(OAA_PARENT_BRANCH)/ || exit 3 ; \ 115 | pushd $$TEMP_DIR/$(OAA_PARENT_BRANCH) ; \ 116 | git difftool --trust-exit-code -y -x "diff -u -I '^\"POT-Creation-Date: .*$$'" HEAD ./$(POTFILE_BASENAME) &>/dev/null ; \ 117 | if [ $$? -eq 0 ] ; then \ 118 | popd ; \ 119 | echo "Pot file is up to date" ; \ 120 | rm -rf $$TEMP_DIR ; \ 121 | else \ 122 | git add ./$(POTFILE_BASENAME) && \ 123 | git commit -m "Update $(POTFILE_BASENAME)" && \ 124 | popd && \ 125 | echo "Pot file updated for the localization repository $(L10N_REPOSITORY)" && \ 126 | echo "Please confirm changes (git diff HEAD~1) and push:" && \ 127 | echo "$$TEMP_DIR" ; \ 128 | fi ; 129 | 130 | install-po-files: 131 | $(MAKE) -C po install RPM_BUILD_ROOT=$(DESTDIR) 132 | 133 | CONTAINER_NAME = oscap-anaconda-addon-ci 134 | container-test: 135 | podman build --tag $(CONTAINER_NAME) --file tests/Dockerfile 136 | podman run --volume .:/oscap-anaconda-addon:Z $(CONTAINER_NAME) make test 137 | 138 | container-update-image: 139 | podman build --tag $(CONTAINER_NAME) --file tests/Dockerfile 140 | podman run --volume .:/oscap-anaconda-addon:Z $(CONTAINER_NAME) ./create_update_image.sh -r / download_rpms 141 | 142 | test: unittest runpylint 143 | 144 | runpylint: 145 | @echo "***Running pylint checks***" 146 | python3 -m pylint org_fedora_oscap -E 2> /dev/null 147 | @echo "[ OK ]" 148 | 149 | unittest: 150 | @echo "***Running unittests checks***" 151 | PYTHONPATH=. python3 -m pytest -v tests/ 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OSCAP Anaconda addon is an addon for the Anaconda installer that integrates 2 | OpenSCAP to the installation process and allows installation of system following 3 | some SCAP-defined restrictions and recommendations. 4 | 5 | The addon is compatible with Anaconda version >= 32 6 | 7 | For testing and other development information, see the [OSCAP Anaconda Addon Developer Guide](https://github.com/OpenSCAP/oscap-anaconda-addon/blob/rawhide/docs/manual/developer_guide.adoc). 8 | -------------------------------------------------------------------------------- /create_update_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | build_dir=$PWD 4 | 5 | actions=(download_rpms install_rpms install_addon_from_repo create_image cleanup) 6 | 7 | 8 | # ARG_POSITIONAL_SINGLE([start-with],[Action to start with - one of: ${actions[*]}],[install_rpms]) 9 | # ARG_OPTIONAL_BOOLEAN([languages],[l],[Include languages in the image. The increased image size may cause problems.]) 10 | # ARG_OPTIONAL_SINGLE([rpm-dir],[],[Where to put/take from RPMs to install],[$build_dir/rpm]) 11 | # ARG_OPTIONAL_SINGLE([tmp-root],[],[Fake temp root],[$(mktemp -d)]) 12 | # ARG_OPTIONAL_SINGLE([releasever],[r],[Version of the target OS],[28]) 13 | # ARG_TYPE_GROUP_SET([action],[ACTION],[start-with],[download_rpms,install_rpms,install_addon_from_repo,create_image,cleanup],[index]) 14 | # ARG_HELP([]) 15 | # ARGBASH_GO() 16 | # needed because of Argbash --> m4_ignore([ 17 | ### START OF CODE GENERATED BY Argbash v2.8.1 one line above ### 18 | # Argbash is a bash code generator used to get arguments parsing right. 19 | # Argbash is FREE SOFTWARE, see https://argbash.io for more info 20 | 21 | 22 | die() 23 | { 24 | local _ret=$2 25 | test -n "$_ret" || _ret=1 26 | test "$_PRINT_HELP" = yes && print_help >&2 27 | echo "$1" >&2 28 | exit ${_ret} 29 | } 30 | 31 | # validators 32 | 33 | action() 34 | { 35 | local _allowed=("download_rpms" "install_rpms" "install_addon_from_repo" "create_image" "cleanup") _seeking="$1" _idx=0 36 | for element in "${_allowed[@]}" 37 | do 38 | test "$element" = "$_seeking" && { test "$3" = "idx" && echo "$_idx" || echo "$element"; } && return 0 39 | _idx=$((_idx + 1)) 40 | done 41 | die "Value '$_seeking' (of argument '$2') doesn't match the list of allowed values: 'download_rpms', 'install_rpms', 'install_addon_from_repo', 'create_image' and 'cleanup'" 4 42 | } 43 | 44 | 45 | begins_with_short_option() 46 | { 47 | local first_option all_short_options='lrh' 48 | first_option="${1:0:1}" 49 | test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0 50 | } 51 | 52 | # THE DEFAULTS INITIALIZATION - POSITIONALS 53 | _positionals=() 54 | _arg_start_with="install_rpms" 55 | # THE DEFAULTS INITIALIZATION - OPTIONALS 56 | _arg_languages="off" 57 | _arg_rpm_dir="$build_dir/rpm" 58 | _arg_tmp_root="$(mktemp -d)" 59 | _arg_releasever="rawhide" 60 | 61 | 62 | print_help() 63 | { 64 | printf 'Usage: %s [-l|--(no-)languages] [--rpm-dir ] [--tmp-root ] [-r|--releasever ] [-h|--help] []\n' "$0" 65 | printf '\t%s\n' ": Action to start with - one of: ${actions[*]}. Can be one of: 'download_rpms', 'install_rpms', 'install_addon_from_repo', 'create_image' and 'cleanup' (default: 'install_rpms')" 66 | printf '\t%s\n' "-l, --languages, --no-languages: Include languages in the image. The increased image size may cause problems. (off by default)" 67 | printf '\t%s\n' "--rpm-dir: Where to put/take from RPMs to install (default: '$build_dir/rpm')" 68 | printf '\t%s\n' "--tmp-root: Fake temp root (default: '$(mktemp -d)')" 69 | printf '\t%s\n' "-r, --releasever: Version of the target OS (default: 'rawhide')" 70 | printf '\t%s\n' "-h, --help: Prints help" 71 | } 72 | 73 | 74 | parse_commandline() 75 | { 76 | _positionals_count=0 77 | while test $# -gt 0 78 | do 79 | _key="$1" 80 | case "$_key" in 81 | -l|--no-languages|--languages) 82 | _arg_languages="on" 83 | test "${1:0:5}" = "--no-" && _arg_languages="off" 84 | ;; 85 | -l*) 86 | _arg_languages="on" 87 | _next="${_key##-l}" 88 | if test -n "$_next" -a "$_next" != "$_key" 89 | then 90 | { begins_with_short_option "$_next" && shift && set -- "-l" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." 91 | fi 92 | ;; 93 | --rpm-dir) 94 | test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 95 | _arg_rpm_dir="$2" 96 | shift 97 | ;; 98 | --rpm-dir=*) 99 | _arg_rpm_dir="${_key##--rpm-dir=}" 100 | ;; 101 | --tmp-root) 102 | test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 103 | _arg_tmp_root="$2" 104 | shift 105 | ;; 106 | --tmp-root=*) 107 | _arg_tmp_root="${_key##--tmp-root=}" 108 | ;; 109 | -r|--releasever) 110 | test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 111 | _arg_releasever="$2" 112 | shift 113 | ;; 114 | --releasever=*) 115 | _arg_releasever="${_key##--releasever=}" 116 | ;; 117 | -r*) 118 | _arg_releasever="${_key##-r}" 119 | ;; 120 | -h|--help) 121 | print_help 122 | exit 0 123 | ;; 124 | -h*) 125 | print_help 126 | exit 0 127 | ;; 128 | *) 129 | _last_positional="$1" 130 | _positionals+=("$_last_positional") 131 | _positionals_count=$((_positionals_count + 1)) 132 | ;; 133 | esac 134 | shift 135 | done 136 | } 137 | 138 | 139 | handle_passed_args_count() 140 | { 141 | test "${_positionals_count}" -le 1 || _PRINT_HELP=yes die "FATAL ERROR: There were spurious positional arguments --- we expect between 0 and 1, but got ${_positionals_count} (the last one was: '${_last_positional}')." 1 142 | } 143 | 144 | 145 | assign_positional_args() 146 | { 147 | local _positional_name _shift_for=$1 148 | _positional_names="_arg_start_with " 149 | 150 | shift "$_shift_for" 151 | for _positional_name in ${_positional_names} 152 | do 153 | test $# -gt 0 || break 154 | eval "$_positional_name=\${1}" || die "Error during argument parsing, possibly an Argbash bug." 1 155 | shift 156 | done 157 | } 158 | 159 | parse_commandline "$@" 160 | handle_passed_args_count 161 | assign_positional_args 1 "${_positionals[@]}" 162 | 163 | # OTHER STUFF GENERATED BY Argbash 164 | # Validation of values 165 | _arg_start_with="$(action "$_arg_start_with" "start-with")" || exit 1 166 | _arg_start_with_index="$(action "$_arg_start_with" "start-with" idx)" 167 | 168 | ### END OF CODE GENERATED BY Argbash (sortof) ### ]) 169 | # [ <-- needed because of Argbash 170 | 171 | tmp_root="$_arg_tmp_root" 172 | rpmdir="$_arg_rpm_dir" 173 | 174 | 175 | packages=" 176 | openscap 177 | openscap-python3 178 | openscap-scanner 179 | python3-cpio 180 | python3-pycurl 181 | xmlsec1-openssl 182 | oscap-anaconda-addon 183 | xmlsec1 184 | xmlsec1-openssl 185 | " 186 | 187 | 188 | download_rpms() { 189 | mkdir -p "$rpmdir" 190 | (cd "$rpmdir" && dnf download --arch x86_64,noarch --releasever "$_arg_releasever" $packages) || die "Failed to download RPMs for Fedora $_arg_releasever" 191 | } 192 | 193 | 194 | install_rpms() { 195 | test -d "$rpmdir" || return 0 # Nothing to do, no RPM dir exists 196 | # Install pre-downloaded RPMs to the fake root, sudo is required 197 | for pkg in "$rpmdir/"*.rpm; do 198 | sudo rpm -i --nodeps --root "$tmp_root" "$pkg" || die "Failed to install dependency $pkg to $tmp_root, which is needed for the installer to be fully operational." 199 | done 200 | } 201 | 202 | 203 | install_addon_from_repo() { 204 | local install_po_files 205 | if test "$_arg_languages" = off; then 206 | install_po_files="DEFAULT_INSTALL_OF_PO_FILES=no" 207 | else 208 | install_po_files="DEFAULT_INSTALL_OF_PO_FILES=yes" 209 | fi 210 | # "copy files" to new root, sudo may be needed because we may overwrite files installed by rpm 211 | $SUDO make install "$install_po_files" DESTDIR="${tmp_root}" >&2 || die "Failed to install the addon to $tmp_root." 212 | } 213 | 214 | 215 | create_image() { 216 | # create update image 217 | (cd "$tmp_root" && find -L . | cpio -oc | gzip > "$build_dir/update.img") || die "Failed to create the update image from the $tmp_root." 218 | } 219 | 220 | 221 | cleanup() { 222 | # cleanup, sudo may be needed because former RPM installs 223 | $SUDO rm -rf "$tmp_root" 224 | } 225 | 226 | if test $_arg_start_with_index -gt 1; then 227 | SUDO= 228 | else 229 | SUDO=sudo 230 | $SUDO true || die "Unable to get sudo working, bailing out." 231 | fi 232 | 233 | for (( action_index=_arg_start_with_index; action_index < ${#actions[*]}; action_index++ )) do 234 | "${actions[$action_index]}" 235 | done 236 | 237 | # ] <-- needed because of Argbash 238 | -------------------------------------------------------------------------------- /data/org.fedoraproject.Anaconda.Addons.OSCAP.conf: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /data/org.fedoraproject.Anaconda.Addons.OSCAP.service: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | # Start the org.fedoraproject.Anaconda.Addons.OSCAP service. 3 | Name=org.fedoraproject.Anaconda.Addons.OSCAP 4 | Exec=/usr/libexec/anaconda/start-module org_fedora_oscap.service 5 | User=root 6 | -------------------------------------------------------------------------------- /docs/manual/developer_guide.adoc: -------------------------------------------------------------------------------- 1 | = OSCAP Anaconda Addon Developer Guide 2 | :imagesdir: ./images 3 | :toc: 4 | :toc-placement: preamble 5 | :numbered: 6 | 7 | toc::[] 8 | 9 | 10 | == How to Test oscap Anaconda Addon 11 | 12 | Anaconda has capability to load installer updates using _"updates image"_. This image can be loaded from different storages and use different formats. This page should show one working use case, not all possibilities. 13 | 14 | What do you need to test your changes? 15 | 16 | 1. Clone addon repository & use required branch & change code. 17 | 2. Build addon update image. 18 | 3. Run a VM using reanaconda. 19 | 20 | 21 | === Clone repository & use required branch 22 | 23 | The rhel7-branch uses Python 2 and supports RHEL7 Anaconda, whereas the `rawhide` uses Python 3, and targets the Fedora Rawhide. 24 | 25 | 26 | === Build image 27 | 28 | We will create cpio archive `ASCII cpio archive (SVR4 with no CRC)` packed using gzip (`gzip compressed data`). 29 | On RHEL installation media, the OAA package and its dependencies are included, but this is not the case with Fedora. 30 | Therefore, the archive has to contain those dependencies. 31 | 32 | You can use the `create_update_image.sh` script in the oscap-anaconda-addon repository to create the `update.img` image. 33 | If some time has passed since you have created the image, you typically want to re-download fresh RPM dependencies in the process, so the image is up-to-date in all aspects. 34 | 35 | For further reading, see the https://fedoraproject.org/wiki/Anaconda/Updates#How_to_Create_an_Anaconda_Updates_Image[official docs]. 36 | 37 | ---- 38 | ./create_update_image.sh download_rpms 39 | ---- 40 | 41 | Or install `podman` and create the update image with dependencies in a container using: 42 | 43 | ---- 44 | make container-update-image 45 | ---- 46 | 47 | If you want to see what was packed, you can extract the image. 48 | 49 | ---- 50 | gunzip -c update.img | cpio -id 51 | ---- 52 | 53 | or use the `lsinitrd` command, which is part of the `dracut` package on RHEL and Fedora: 54 | 55 | ---- 56 | lsinitrd update.img 57 | ---- 58 | 59 | === Use reanaconda 60 | 61 | The `reanaconda` script prepares and starts a VM with the update image. 62 | You can get it from the repository at 63 | https://github.com/rhinstaller/devel-tools/tree/master/reanaconda 64 | 65 | Prepare the VM: 66 | 67 | ---- 68 | ./reanaconda.py prime --sensible --tree http://ftp.fi.muni.cz/pub/linux/fedora/linux/releases/34/Everything/x86_64/os 69 | ---- 70 | 71 | After the script terminates, provide the update image: 72 | 73 | ---- 74 | ./reanaconda.py updates path/to/updates.img 75 | ---- 76 | 77 | The VM should be shown in a QEMU window, which you can play with and you can 78 | close it any time. 79 | 80 | If you run a Fedora VM, there won't be `scap-security-guide` content available, 81 | so you will have to serve the content from your host machine using a HTTP 82 | server. For example, you can provide your local SSG build. In a new terminal, 83 | run: 84 | 85 | ---- 86 | cd ~/work/git/scap-security-guide/build 87 | python3 -m http.server 88 | ---- 89 | 90 | And then, in the OSCAP Anaconda Addon user interface, enter the URL. Your host 91 | is visible from your guest at `10.0.2.2`. For example: 92 | 93 | ---- 94 | http://10.0.2.2:8000/ssg-fedora-ds.xml 95 | ---- 96 | 97 | Watch the console, as the VM is supposed to download the update image, and the Python server should output the corresponding HTTP request: 98 | 99 | ` - - [] "GET /update.img HTTP/1.1" 200 -` 100 | 101 | 200 is the OK request status. 102 | 103 | There is a cleanup step, but you don't have to run it if you only want to 104 | restart the VM with a new image: 105 | 106 | ---- 107 | ./reanaconda.py cleanup 108 | ---- 109 | 110 | === Further introspection of Anaconda 111 | 112 | After reaching the Anaconda GUI, you can switch into another VT and check out that the update went OK by examining the files on the disc. 113 | You can also debug Anaconda in a sophisticated way - as of 04/2018, switching to tty1 brought you to a TMUX session with windows attached to various processes. 114 | There is also an official https://fedoraproject.org/wiki/How_to_debug_installation_problems[how-to-debug documentation] though. 115 | 116 | === Older method without reanaconda 117 | 118 | ==== Serve image using HTTP server 119 | 120 | You don't need public HTTP server or setup Apache. 121 | You can use simple python HTTP server - it serves all files in you current directory. 122 | 123 | ---- 124 | python3 -m http.server 125 | ---- 126 | 127 | **Setup your firewall rules correctly to make webserver port accessible from virtual machine.** 128 | 129 | 130 | ==== Load system with update image 131 | 132 | If you want to load your changes to anaconda, you have to setup boot options correctly. 133 | You have two ways how to setup it: 134 | 135 | - Manually 136 | - With Network Install/PXE boot 137 | 138 | If you want to set it manually, you have to boot your machine into grub. Then you can change options (usually using "tab" key). 139 | 140 | If you use Network Install/PXE boot you can pass requires parameters there - look for `kernel options`. 141 | Advantage of this solution is that you will not need to change parameters during every boot. 142 | 143 | **Required boot parameters:** 144 | 145 | ---- 146 | inst.updates=http://gateway:8000/update.img 147 | ---- 148 | 149 | Here, `gateway` is supposed to refers to your host machine: 150 | 151 | * On Fedora, you have to enter the IP address of the virtual bridge interface. 152 | * On RHEL, the `gateway` hostname will be recognized correctly. 153 | 154 | Remember that you can also provide your custom-built SSG content to the insaller this way - 155 | you may copy your datastream to the directory that is served by the server as it contains the image, and then, 156 | enter `http://gateway:8000/my-custom-ds.xml` as a remote content URL. 157 | 158 | Watch the console, as the VM is supposed to download the update image, and the Python server should output the corresponding HTTP request: 159 | 160 | ` - - [] "GET /update.img HTTP/1.1" 200 -` 161 | 162 | 200 is the OK request status. 163 | 164 | ==== Installing a VM using update image and kickstart 165 | 166 | You can also use the `virt-install` command, which is useful when you want to test kickstart installation. 167 | 168 | Some kickstarts (`.cfg` files) can be found in the `testing_files` directory. 169 | You will also need installation URL and the update image described above. 170 | 171 | For example: 172 | 173 | ---- 174 | virt-install \ 175 | --connect qemu:///system \ 176 | --name oaa_test \ 177 | --memory 2048 --vcpus 2 --disk size=8 \ 178 | --os-variant fedora35 \ 179 | --location INSTALLATION_URL \ 180 | -x inst.updates=http://192.168.122.1:8000/update.img \ 181 | -x inst.ks=http://192.168.122.1:8000/ks.cfg \ 182 | --network default 183 | ---- 184 | 185 | Replace `INSTALLATION_URL` with correct URL and `ks.cfg` with the real kickstart file name. 186 | 187 | ==== Testing with newer OpenSCAP 188 | 189 | If you have a new RPM in a repo, eg. in a COPR repository created by Packit, you can add a link to the repo to your kickstart. 190 | 191 | For example: 192 | 193 | ---- 194 | repo --name oscap --baseurl=https://download.copr.fedorainfracloud.org/results/packit/OpenSCAP-openscap-1838/epel-8-x86_64/ 195 | ---- 196 | 197 | ==== Testing code in rawhide branch 198 | 199 | Our `rawhide` branch is compatible with the latest released Fedora. 200 | However, testing this code has some caveats, for example, the `reanaconda` tool can't be used at this moment. 201 | The recommended procedure for testing the rawhide branch is the following: 202 | 203 | . Download the latest Fedora Server DVD ISO file from https://fedoraproject.org/server/download/, for example `Fedora-Server-dvd-x86_64-38-1.6.iso`. 204 | . Create an OAA update image with the `-r` parameter set to the version: 205 | + 206 | ---- 207 | sudo ./create_update_image.sh -r 38 download_rpms 208 | ---- 209 | + 210 | . Start a HTTP server in the project directory: 211 | + 212 | ---- 213 | python3 -m http.server 214 | ---- 215 | + 216 | . Create a virtual machine in `virt-manager` choosing the downloaded ISO file as the installation resource. 217 | . When the virtual machine screen appears, navigate using arrow to highlight `Install Fedora 38` and press `e`. 218 | . On the line starting with `linux`, append this command: 219 | + 220 | ---- 221 | inst.updates=http://192.168.122.1:8000/update.img 222 | ---- 223 | + 224 | where the IP address may vary - if the update image is not downloaded during the installation media boot (you can tell that something happened from the output of the Python server command), look up the gateway IP address from the respective VM configuration. 225 | + 226 | . Press `F10`. 227 | 228 | The HTTP server should display a `GET /update.img` message, otherwise fix the `firewalld` settings. 229 | 230 | == Available make commands 231 | 232 | Following commands are available to be used in make command: 233 | 234 | ---- 235 | dist - Build the release tarball 236 | install - Install the plugin into your system 237 | uninstall - Uninstall the plugin from your system 238 | po-pull - Pull translations from Zanata 239 | potfile - Update translation template file 240 | push-pot - Push translation template to Zanata 241 | test - Run pylint checks and unit tests 242 | pylint - Run only pylint checks 243 | unittest - Run only unit tests 244 | ---- 245 | 246 | === Translations 247 | 248 | Following packages are needed to manage translations: 249 | 250 | ---- 251 | intltool 252 | git 253 | ---- 254 | 255 | === Running Unit Tests 256 | 257 | Following packages are needed to run unit tests: 258 | 259 | ---- 260 | anaconda 261 | openscap-python3 262 | python3-cpio 263 | python3-mock 264 | python3-pytest 265 | python3-pycurl 266 | ---- 267 | 268 | Run the unit tests using: 269 | 270 | ---- 271 | make unittest 272 | ---- 273 | 274 | Or install `podman` and run the tests in a container using: 275 | 276 | ---- 277 | make container-test 278 | ---- 279 | 280 | == Updating translations 281 | 282 | Sometimes it is neccessary to create a patch that updates translations present in the release tarball with custom translations, or translations from Zanata. 283 | You can use the `make-language-patch` script in the `tools` subdirectory for this task. 284 | You just supply the release tarball, and a filesystem path to the directory with `.po` files if you don't want to use Zanata to update the `po` directory contents and use that one. 285 | The resulting patch can then be applied to the release package without any additional steps needed. 286 | -------------------------------------------------------------------------------- /org_fedora_oscap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/org_fedora_oscap/__init__.py -------------------------------------------------------------------------------- /org_fedora_oscap/constants.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2020 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | from dasbus.identifier import DBusServiceIdentifier 19 | from pyanaconda.core.dbus import DBus 20 | from pyanaconda.modules.common.constants.namespaces import ADDONS_NAMESPACE 21 | 22 | # DBus constants 23 | OSCAP_NAMESPACE = ( 24 | *ADDONS_NAMESPACE, 25 | "OSCAP" 26 | ) 27 | 28 | OSCAP = DBusServiceIdentifier( 29 | namespace=OSCAP_NAMESPACE, 30 | message_bus=DBus 31 | ) 32 | -------------------------------------------------------------------------------- /org_fedora_oscap/content_handling.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2013 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | # Red Hat Author(s): Vratislav Podzimek 19 | # 20 | 21 | """ 22 | Module with various classes for SCAP content processing and retrieving data 23 | from it. 24 | 25 | """ 26 | 27 | import os.path 28 | 29 | from collections import namedtuple 30 | import multiprocessing 31 | 32 | from pyanaconda.core.util import execReadlines 33 | try: 34 | from html.parser import HTMLParser 35 | except ImportError: 36 | from HTMLParser import HTMLParser 37 | 38 | import logging 39 | log = logging.getLogger("anaconda") 40 | 41 | 42 | CONTENT_TYPES = dict( 43 | DATASTREAM="Source Data Stream", 44 | XCCDF_CHECKLIST="XCCDF Checklist", 45 | OVAL="OVAL Definitions", 46 | CPE_DICT="CPE Dictionary", 47 | TAILORING="XCCDF Tailoring", 48 | ) 49 | 50 | 51 | class ContentHandlingError(Exception): 52 | """Exception class for errors related to SCAP content handling.""" 53 | 54 | pass 55 | 56 | 57 | class ContentCheckError(ContentHandlingError): 58 | """ 59 | Exception class for errors related to content (integrity,...) checking. 60 | """ 61 | 62 | pass 63 | 64 | 65 | class ParseHTMLContent(HTMLParser): 66 | """Parser class for HTML tags within content""" 67 | 68 | def __init__(self): 69 | HTMLParser.__init__(self) 70 | self.content = "" 71 | 72 | def handle_starttag(self, tag, attrs): 73 | if tag == "html:ul": 74 | self.content += "\n" 75 | elif tag == "html:li": 76 | self.content += "\n" 77 | elif tag == "html:br": 78 | self.content += "\n" 79 | 80 | def handle_endtag(self, tag): 81 | if tag == "html:ul": 82 | self.content += "\n" 83 | elif tag == "html:li": 84 | self.content += "\n" 85 | 86 | def handle_data(self, data): 87 | self.content += data.strip() 88 | 89 | def get_content(self): 90 | return self.content 91 | 92 | 93 | def parse_HTML_from_content(content): 94 | """This is a very simple HTML to text parser. 95 | 96 | HTML tags will be removed while trying to maintain readability 97 | of content. 98 | 99 | :param content: content whose HTML tags will be parsed 100 | :return: content without HTML tags 101 | """ 102 | 103 | parser = ParseHTMLContent() 104 | parser.feed(content) 105 | return parser.get_content() 106 | 107 | 108 | # namedtuple class for info about content files found 109 | # pylint: disable-msg=C0103 110 | ContentFiles = namedtuple("ContentFiles", ["xccdf", "cpe", "tailoring"]) 111 | 112 | 113 | def identify_files(fpaths): 114 | result = {path: get_doc_type(path) for path in fpaths} 115 | return result 116 | 117 | 118 | def get_doc_type(file_path): 119 | content_type = "unknown" 120 | try: 121 | for line in execReadlines("oscap", ["info", file_path]): 122 | if line.startswith("Document type:"): 123 | _prefix, _sep, type_info = line.partition(":") 124 | content_type = type_info.strip() 125 | break 126 | except OSError: 127 | # 'oscap info' exitted with a non-zero exit code -> unknown doc 128 | # type 129 | pass 130 | except UnicodeDecodeError: 131 | # 'oscap info' supplied weird output, which happens when it tries 132 | # to explain why it can't examine e.g. a JPG. 133 | pass 134 | except Exception as e: 135 | log.warning(f"OSCAP addon: Unexpected error when looking at {file_path}: {str(e)}") 136 | log.info("OSCAP addon: Identified {file_path} as {content_type}" 137 | .format(file_path=file_path, content_type=content_type)) 138 | return content_type 139 | 140 | 141 | def explore_content_files(fpaths): 142 | """ 143 | Function for finding content files in a list of file paths. SIMPLY PICKS 144 | THE FIRST USABLE CONTENT FILE OF A PARTICULAR TYPE AND JUST PREFERS DATA 145 | STREAMS OVER STANDALONE BENCHMARKS. 146 | 147 | :param fpaths: a list of file paths to search for content files in 148 | :type fpaths: [str] 149 | :return: ContentFiles instance containing the file names of the XCCDF file, 150 | CPE dictionary and tailoring file or "" in place of those items 151 | if not found 152 | :rtype: ContentFiles 153 | 154 | """ 155 | xccdf_file = "" 156 | cpe_file = "" 157 | tailoring_file = "" 158 | found_ds = False 159 | 160 | for fpath in fpaths: 161 | doc_type = get_doc_type(fpath) 162 | if not doc_type: 163 | continue 164 | 165 | # prefer DS over standalone XCCDF 166 | if doc_type == "Source Data Stream" and (not xccdf_file or not found_ds): 167 | xccdf_file = fpath 168 | found_ds = True 169 | elif doc_type == "XCCDF Checklist" and not xccdf_file: 170 | xccdf_file = fpath 171 | elif doc_type == "CPE Dictionary" and not cpe_file: 172 | cpe_file = fpath 173 | elif doc_type == "XCCDF Tailoring" and not tailoring_file: 174 | tailoring_file = fpath 175 | 176 | # TODO: raise exception if no xccdf_file is found? 177 | files = ContentFiles(xccdf_file, cpe_file, tailoring_file) 178 | return files 179 | -------------------------------------------------------------------------------- /org_fedora_oscap/cpioarchive.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import atexit 4 | 5 | """ cpioarchive: Support for cpio archives 6 | Copyright (C) 2006 Ignacio Vazquez-Abrams """ 7 | 8 | """ This library is free software; you can redistribute it and/or modify it under the terms of the 9 | GNU Lesser General Public License as published by the Free Software Foundation; 10 | either version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 13 | without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | See the GNU Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public License along with this library; 17 | if not, write to the Free Software Foundation, Inc., 18 | 59 Temple Place, Suite 330, 19 | Boston, MA 02111-1307 USA 20 | """ 21 | 22 | 23 | def version(): 24 | """Returns the version number of the module.""" 25 | return '0.1' 26 | 27 | 28 | class CpioError(Exception): 29 | """Exception class for cpioarchive exceptions""" 30 | pass 31 | 32 | 33 | class CpioEntry(object): 34 | """Information about a single file in a cpio archive. 35 | Provides a file-like interface for interacting with the entry.""" 36 | def __init__(self, hdr, cpio, offset): 37 | """Create a new CpioEntry instance. Internal use only.""" 38 | if len(hdr) < 110: 39 | raise CpioError('cpio header too short') 40 | if not hdr.startswith(b'070701'): 41 | raise CpioError('cpio header invalid') 42 | self.inode = int(hdr[6:14], 16) 43 | self.mode = int(hdr[14:22], 16) 44 | self.uid = int(hdr[22:30], 16) 45 | self.gid = int(hdr[30:38], 16) 46 | self.nlinks = int(hdr[38:46], 16) 47 | self.mtime = int(hdr[46:54], 16) 48 | self.size = int(hdr[54:62], 16) 49 | """Size of the file stored in the entry.""" 50 | self.devmajor = int(hdr[62:70], 16) 51 | self.devminor = int(hdr[70:78], 16) 52 | self.rdevmajor = int(hdr[78:86], 16) 53 | self.rdevminor = int(hdr[86:94], 16) 54 | namesize = int(hdr[94:102], 16) 55 | self.checksum = int(hdr[102:110], 16) 56 | if len(hdr) < 110+namesize: 57 | raise CpioError('cpio header too short') 58 | self.name = hdr[110:110+namesize-1].decode("utf-8") 59 | """Name of the file stored in the entry.""" 60 | self.datastart = offset+110+namesize 61 | self.datastart += (4-(self.datastart % 4)) % 4 62 | self.curoffset = 0 63 | self.cpio = cpio 64 | self.closed = False 65 | 66 | def close(self): 67 | """Close this cpio entry. Further calls to methods will raise an exception.""" 68 | self.closed = True 69 | 70 | def flush(self): 71 | """Flush the entry (no-op).""" 72 | pass 73 | 74 | def read(self, size=None): 75 | """Read data from the entry. 76 | 77 | Args: 78 | size -- Number of bytes to read (default: whole entry) 79 | """ 80 | if self.closed: 81 | raise ValueError('read operation on closed file') 82 | self.cpio.file.seek(self.datastart+self.curoffset, 0) 83 | if size and size < self.size-self.curoffset: 84 | ret = self.cpio.file.read(size) 85 | else: 86 | ret = self.cpio.file.read(self.size-self.curoffset) 87 | self.curoffset += len(ret) 88 | return ret 89 | 90 | def seek(self, offset, whence=0): 91 | """Move to new position within an entry. 92 | 93 | Args: 94 | offset -- Byte count 95 | whence -- Describes how offset is used. 96 | 0: From beginning of file 97 | 1: Forwards from current position 98 | 2: Backwards from current position 99 | Other values are ignored. 100 | """ 101 | if self.closed: 102 | raise ValueError('seek operation on closed file') 103 | if whence == 0: 104 | self.curoffset = offset 105 | elif whence == 1: 106 | self.curoffset += offset 107 | elif whence == 2: 108 | self.curoffset -= offset 109 | self.curoffset = min(max(0, self.curoffset), self.size) 110 | 111 | def tell(self): 112 | """Get current position within an entry""" 113 | if self.closed: 114 | raise ValueError('tell operation on closed file') 115 | return self.curoffset 116 | 117 | 118 | class CpioArchive(object): 119 | @classmethod 120 | def open(name=None, mode='r', fileobj=None): 121 | """Open a cpio archive. Defers to CpioArchive.__init__().""" 122 | return CpioArchive(name, mode, fileobj) 123 | 124 | def __init__(self, name=None, mode='r', fileobj=None): 125 | """Open a cpio archive. 126 | 127 | Args: 128 | name -- Filename to open (default: open a file object instead) 129 | mode -- Filemode to open the archive in (default: read-only) 130 | fileobj -- File object to use (default: open by filename instead) 131 | """ 132 | if not mode == 'r': 133 | raise NotImplementedError() 134 | self._infos = [] 135 | if name: 136 | self._readfile(name) 137 | self.external = False 138 | elif fileobj: 139 | self._readobj(fileobj) 140 | self.external = True 141 | else: 142 | raise CpioError('Oh come on! Pass me something to work with...') 143 | self._ptr = 0 144 | self.closed = False 145 | atexit.register(self.close) 146 | 147 | def close(self): 148 | """Close the CpioArchive. Also closes all associated entries.""" 149 | if self.closed: 150 | return 151 | [x.close() for x in self._infos] 152 | self.closed = True 153 | if not self.external: 154 | self.file.close() 155 | 156 | def __next__(self): 157 | # pylint: disable = E1102 158 | return self.next() 159 | 160 | def next(self): 161 | """Return the next entry in the archive.""" 162 | if self.closed: 163 | raise ValueError('next operation on closed file') 164 | if self._ptr > len(self._infos): 165 | raise StopIteration() 166 | ret = self._infos[self._ptr] 167 | self._ptr += 1 168 | return ret 169 | 170 | def __iter__(self): 171 | return iter(self._infos) 172 | 173 | def _readfile(self, name): 174 | self._readobj(open(name, 'rb')) 175 | 176 | def _readobj(self, fileobj): 177 | self.file = fileobj 178 | start = self.file.tell() 179 | istart = self.file.tell() 180 | text = self.file.read(110) 181 | while text: 182 | namelen = int(text[94:102], 16) 183 | text += self.file.read(namelen) 184 | ce = CpioEntry(text, self, istart) 185 | if not ce.name == "TRAILER!!!": 186 | self._infos.append(ce) 187 | else: 188 | return 189 | self.file.seek((4-(self.file.tell()-istart) % 4) % 4, 1) 190 | self.file.seek(self._infos[-1].size, 1) 191 | self.file.seek((4-(self.file.tell()-istart) % 4) % 4, 1) 192 | 193 | istart = self.file.tell() 194 | text = self.file.read(110) 195 | else: 196 | raise CpioError('premature end of headers') 197 | -------------------------------------------------------------------------------- /org_fedora_oscap/data_fetch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for fetching files via HTTP and FTP. Directly or over SSL (HTTPS) with 3 | server certificate validation. 4 | 5 | """ 6 | 7 | import re 8 | import os 9 | import os.path 10 | import pycurl 11 | 12 | from pyanaconda.core.configuration.anaconda import conf 13 | from pyanaconda.core import constants 14 | from pyanaconda.core.threads import thread_manager, AnacondaThread 15 | from pyanaconda.modules.common.constants.services import NETWORK 16 | 17 | from org_fedora_oscap import common 18 | from org_fedora_oscap.common import _ 19 | from org_fedora_oscap import utils 20 | 21 | import logging 22 | log = logging.getLogger("anaconda") 23 | 24 | 25 | # everything else should be private 26 | __all__ = ["fetch_data", "can_fetch_from"] 27 | 28 | # prefixes of the URLs that need network connection 29 | NET_URL_PREFIXES = ("http", "https", "ftp") 30 | 31 | # prefixes of the URLs that may not need network connection 32 | LOCAL_URL_PREFIXES = ("file",) 33 | 34 | # TODO: needs improvements 35 | HTTP_URL_RE_STR = r"(https?)://(.*)" 36 | HTTP_URL_RE = re.compile(HTTP_URL_RE_STR) 37 | 38 | FTP_URL_RE_STR = r"(ftp)://(.*)" 39 | FTP_URL_RE = re.compile(FTP_URL_RE_STR) 40 | 41 | FILE_URL_RE_STR = r"(file)://(.*)" 42 | FILE_URL_RE = re.compile(FILE_URL_RE_STR) 43 | 44 | 45 | class DataFetchError(common.OSCAPaddonError): 46 | """Parent class for the exception classes defined in this module.""" 47 | 48 | pass 49 | 50 | 51 | class CertificateValidationError(DataFetchError): 52 | """Class for the certificate validation related errors.""" 53 | 54 | pass 55 | 56 | 57 | class WrongRequestError(DataFetchError): 58 | """Class for the wrong combination of parameters errors.""" 59 | 60 | pass 61 | 62 | 63 | class UnknownURLformatError(DataFetchError): 64 | """Class for invalid URL cases.""" 65 | 66 | pass 67 | 68 | 69 | class FetchError(DataFetchError): 70 | """ 71 | Class for the errors when fetching data. Usually due to I/O errors. 72 | 73 | """ 74 | 75 | pass 76 | 77 | 78 | def fetch_local_data(url, out_file): 79 | """ 80 | Function that fetches data locally. 81 | 82 | :see: org_fedora_oscap.data_fetch.fetch_data 83 | :return: the name of the thread running fetch_data 84 | :rtype: str 85 | 86 | """ 87 | fetch_data_thread = AnacondaThread(name=common.THREAD_FETCH_DATA, 88 | target=fetch_data, 89 | args=(url, out_file, None), 90 | fatal=False) 91 | 92 | # register and run the thread 93 | thread_manager.add(fetch_data_thread) 94 | 95 | return common.THREAD_FETCH_DATA 96 | 97 | 98 | def wait_and_fetch_net_data(url, out_file, ca_certs_path=None): 99 | """ 100 | Function that waits for network connection and starts a thread that fetches 101 | data over network. 102 | 103 | :see: org_fedora_oscap.data_fetch.fetch_data 104 | :return: the name of the thread running fetch_data 105 | :rtype: str 106 | 107 | """ 108 | 109 | # get thread that tries to establish a network connection 110 | nm_conn_thread = thread_manager.get(constants.THREAD_WAIT_FOR_CONNECTING_NM) 111 | if nm_conn_thread: 112 | # NM still connecting, wait for it to finish 113 | nm_conn_thread.join() 114 | 115 | network_proxy = NETWORK.get_proxy() 116 | if not network_proxy.Connected: 117 | raise common.OSCAPaddonNetworkError(_("Network connection needed to fetch data.")) 118 | 119 | log.info(f"Fetching data from {url}") 120 | fetch_data_thread = AnacondaThread(name=common.THREAD_FETCH_DATA, 121 | target=fetch_data, 122 | args=(url, out_file, ca_certs_path), 123 | fatal=False) 124 | 125 | # register and run the thread 126 | thread_manager.add(fetch_data_thread) 127 | 128 | return common.THREAD_FETCH_DATA 129 | 130 | 131 | def can_fetch_from(url): 132 | """ 133 | Function telling whether the fetch_data function understands the type of 134 | given URL or not. 135 | 136 | :param url: URL 137 | :type url: str 138 | :return: whether the type of the URL is supported or not 139 | :rtype: str 140 | 141 | """ 142 | resources = NET_URL_PREFIXES + LOCAL_URL_PREFIXES 143 | return any(url.startswith(prefix) for prefix in resources) 144 | 145 | 146 | def fetch_data(url, out_file, ca_certs_path=None): 147 | """ 148 | Fetch data from a given URL. If the URL starts with https://, ca_certs_path can 149 | be a path to PEM file with CA certificate chain to validate server 150 | certificate. 151 | 152 | :param url: URL of the data 153 | :type url: str 154 | :param out_file: path to the output file 155 | :type out_file: str 156 | :param ca_certs_path: path to a PEM file with CA certificate chain 157 | :type ca_certs_path: str 158 | :raise WrongRequestError: if a wrong combination of arguments is passed 159 | (ca_certs_path file path given and url starting with 160 | http://) or arguments don't have required format 161 | :raise CertificateValidationError: if server certificate validation fails 162 | :raise FetchError: if data fetching fails (usually due to I/O errors) 163 | 164 | """ 165 | 166 | # create the directory for the out_file if it doesn't exist 167 | out_dir = os.path.dirname(out_file) 168 | utils.ensure_dir_exists(out_dir) 169 | 170 | if can_fetch_from(url): 171 | _curl_fetch(url, out_file, ca_certs_path) 172 | else: 173 | msg = "Cannot fetch data from '%s': unknown URL format" % url 174 | raise UnknownURLformatError(msg) 175 | log.info(f"Data fetch from {url} completed") 176 | 177 | 178 | def _curl_fetch(url, out_file, ca_certs_path=None): 179 | """ 180 | Function that fetches data and writes it out to the given file path. If a 181 | path to the file with CA certificates is given and the url starts with 182 | 'https', the server certificate is validated. 183 | 184 | :param url: url of the data that has to start with 'http://' or "https://" 185 | :type url: str 186 | :param out_file: path to the output file 187 | :type out_file: str 188 | :param ca_certs_path: path to the file with CA certificates for server 189 | certificate validation 190 | :type ca_certs_path: str 191 | :raise WrongRequestError: if a wrong combination of arguments is passed 192 | (ca_certs_path file path given and url starting with 193 | http://) or arguments don't have required format 194 | :raise CertificateValidationError: if server certificate validation fails 195 | :raise FetchError: if data fetching fails (usually due to I/O errors) 196 | 197 | """ 198 | 199 | if url.startswith("ftp"): 200 | match = FTP_URL_RE.match(url) 201 | if not match: 202 | msg = "Wrong url not matching '%s'" % FTP_URL_RE_STR 203 | raise WrongRequestError(msg) 204 | else: 205 | protocol, path = match.groups() 206 | if '@' not in path: 207 | # no user:pass given -> use anonymous login to the FTP server 208 | url = protocol + "://anonymous:@" + path 209 | elif url.startswith("file"): 210 | match = FILE_URL_RE.match(url) 211 | if not match: 212 | msg = "Wrong url not matching '%s'" % FILE_URL_RE_STR 213 | raise WrongRequestError(msg) 214 | else: 215 | match = HTTP_URL_RE.match(url) 216 | if not match: 217 | msg = "Wrong url not matching '%s'" % HTTP_URL_RE_STR 218 | raise WrongRequestError(msg) 219 | 220 | # the first group contains the protocol, the second one the rest 221 | protocol = match.groups()[0] 222 | 223 | if not out_file: 224 | raise WrongRequestError("out_file cannot be an empty string") 225 | 226 | if ca_certs_path and protocol != "https": 227 | msg = "Cannot verify server certificate when using plain HTTP" 228 | raise WrongRequestError(msg) 229 | 230 | curl = pycurl.Curl() 231 | curl.setopt(pycurl.URL, url) 232 | 233 | if ca_certs_path and protocol == "https": 234 | # the strictest verification 235 | curl.setopt(pycurl.SSL_VERIFYHOST, 2) 236 | curl.setopt(pycurl.SSL_VERIFYPEER, 1) 237 | curl.setopt(pycurl.CAINFO, ca_certs_path) 238 | 239 | # may be turned off by flags (specified on command line, take precedence) 240 | if not conf.payload.verify_ssl: 241 | log.warning("Disabling SSL verification due to the noverifyssl flag") 242 | curl.setopt(pycurl.SSL_VERIFYHOST, 0) 243 | curl.setopt(pycurl.SSL_VERIFYPEER, 0) 244 | 245 | try: 246 | with open(out_file, "wb") as fobj: 247 | curl.setopt(pycurl.WRITEDATA, fobj) 248 | curl.perform() 249 | except pycurl.error as err: 250 | # first arg is the error code 251 | if err.args[0] == pycurl.E_SSL_CACERT: 252 | msg = "Failed to connect to server and validate its "\ 253 | "certificate: %s" % err 254 | raise CertificateValidationError(msg) 255 | else: 256 | msg = "Failed to fetch data: %s" % err 257 | raise FetchError(msg) 258 | 259 | if protocol in ("http", "https"): 260 | return_code = curl.getinfo(pycurl.HTTP_CODE) 261 | if 400 <= return_code < 600: 262 | msg = _(f"Failed to fetch data - the request returned HTTP error code {return_code}") 263 | raise FetchError(msg) 264 | -------------------------------------------------------------------------------- /org_fedora_oscap/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/org_fedora_oscap/gui/__init__.py -------------------------------------------------------------------------------- /org_fedora_oscap/gui/spokes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/org_fedora_oscap/gui/spokes/__init__.py -------------------------------------------------------------------------------- /org_fedora_oscap/scap_content_handler.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | # Red Hat Author(s): Jan Černý 19 | # 20 | 21 | from collections import namedtuple 22 | import os 23 | import re 24 | import xml.etree.ElementTree as ET 25 | 26 | from org_fedora_oscap.content_handling import parse_HTML_from_content 27 | 28 | # namedtuple class (not a constant, pylint!) for info about a XCCDF profile 29 | # pylint: disable-msg=C0103 30 | ProfileInfo = namedtuple("ProfileInfo", ["id", "title", "description"]) 31 | 32 | ns = { 33 | "ds": "http://scap.nist.gov/schema/scap/source/1.2", 34 | "xccdf-1.1": "http://checklists.nist.gov/xccdf/1.1", 35 | "xccdf-1.2": "http://checklists.nist.gov/xccdf/1.2", 36 | "xlink": "http://www.w3.org/1999/xlink" 37 | } 38 | 39 | 40 | class SCAPContentHandlerError(Exception): 41 | """Exception class for errors related to SCAP content handling.""" 42 | pass 43 | 44 | 45 | class SCAPContentHandler: 46 | def __init__(self, file_path, tailoring_file_path=None): 47 | """ 48 | Constructor for the SCAPContentHandler class. 49 | 50 | :param file_path: path to an SCAP file (only SCAP source data streams, 51 | XCCDF files and tailoring files are supported) 52 | :type file_path: str 53 | :param tailoring_file_path: path to the tailoring file, can be None if 54 | no tailoring exists 55 | :type tailoring_file_path: str 56 | """ 57 | self.file_path = file_path 58 | tree = ET.parse(file_path) 59 | self.root = tree.getroot() 60 | if not tailoring_file_path: 61 | self.tailoring = None 62 | else: 63 | self.tailoring = ET.parse(tailoring_file_path).getroot() 64 | self.scap_type = self._get_scap_type(self.root) 65 | self._data_stream_id = None 66 | self._checklist_id = None 67 | 68 | def _get_scap_type(self, root): 69 | if root.tag == f"{{{ns['ds']}}}data-stream-collection": 70 | return "SCAP_SOURCE_DATA_STREAM" 71 | elif (root.tag == f"{{{ns['xccdf-1.1']}}}Benchmark" or 72 | root.tag == f"{{{ns['xccdf-1.2']}}}Benchmark"): 73 | return "XCCDF" 74 | elif (root.tag == f"{{{ns['xccdf-1.1']}}}Tailoring" or 75 | root.tag == f"{{{ns['xccdf-1.2']}}}Tailoring"): 76 | return "TAILORING" 77 | else: 78 | msg = f"Unsupported SCAP content type {root.tag}" 79 | raise SCAPContentHandlerError(msg) 80 | 81 | def get_data_streams_checklists(self): 82 | """ 83 | Method to get data streams and their checklists found in the SCAP 84 | source data stream represented by the SCAPContentHandler. 85 | 86 | :return: a dictionary consisting of the IDs of the data streams as keys 87 | and lists of their checklists' IDs as values 88 | None if the file isn't a SCAP source data stream 89 | :rtype: dict(str -> list of strings) 90 | """ 91 | if self.scap_type != "SCAP_SOURCE_DATA_STREAM": 92 | return None 93 | checklists = {} 94 | for data_stream in self.root.findall("ds:data-stream", ns): 95 | data_stream_id = data_stream.get("id") 96 | crefs = [] 97 | for cref in data_stream.findall( 98 | "ds:checklists/ds:component-ref", ns): 99 | cref_id = cref.get("id") 100 | crefs.append(cref_id) 101 | checklists[data_stream_id] = crefs 102 | return checklists 103 | 104 | def _parse_profiles_from_xccdf(self, benchmark): 105 | if benchmark is None: 106 | return [] 107 | 108 | # Find out the namespace of the benchmark element 109 | match = re.match(r"^\{([^}]+)\}", benchmark.tag) 110 | if match is None: 111 | raise SCAPContentHandlerError("The document has no namespace.") 112 | root_element_ns = match.groups()[0] 113 | for prefix, uri in ns.items(): 114 | if uri == root_element_ns: 115 | xccdf_ns_prefix = prefix 116 | break 117 | else: 118 | raise SCAPContentHandlerError( 119 | f"Unsupported XML namespace {root_element_ns}") 120 | 121 | profiles = [] 122 | for profile in benchmark.findall(f"{xccdf_ns_prefix}:Profile", ns): 123 | profile_id = profile.get("id") 124 | title = profile.find(f"{xccdf_ns_prefix}:title", ns) 125 | description = profile.find(f"{xccdf_ns_prefix}:description", ns) 126 | if description is None: 127 | description_text = "" 128 | else: 129 | description_text = parse_HTML_from_content(description.text) 130 | profile_info = ProfileInfo( 131 | profile_id, title.text, description_text) 132 | profiles.append(profile_info) 133 | # if there are no profiles we would like to prevent empty profile 134 | # selection list in the GUI so we create the default profile 135 | if len(profiles) == 0: 136 | default_profile = ProfileInfo( 137 | "default", 138 | "Default", 139 | "The implicit XCCDF profile. Usually, the default profile " 140 | "contains no rules.") 141 | profiles.append(default_profile) 142 | return profiles 143 | 144 | def select_checklist(self, data_stream_id, checklist_id): 145 | """ 146 | Method to select a specific XCCDF Benchmark using 147 | :param data_stream_id: value of ds:data-stream/@id 148 | :type data_stream_id: str 149 | :param checklist_id: value of ds:component-ref/@id pointing to 150 | an xccdf:Benchmark 151 | :type checklist_id: str 152 | :return: None 153 | 154 | """ 155 | self._data_stream_id = data_stream_id 156 | self._checklist_id = checklist_id 157 | 158 | def _find_benchmark_in_source_data_stream(self): 159 | cref_xpath = f"ds:data-stream[@id='{self._data_stream_id}']/" \ 160 | f"ds:checklists/ds:component-ref[@id='{self._checklist_id}']" 161 | cref = self.root.find(cref_xpath, ns) 162 | if cref is None: 163 | msg = f"Can't find ds:component-ref " \ 164 | f"with id='{self._checklist_id}' " \ 165 | f"in ds:datastream with id='{self._data_stream_id}'" 166 | raise SCAPContentHandlerError(msg) 167 | cref_href = cref.get(f"{{{ns['xlink']}}}href") 168 | if cref_href is None: 169 | msg = f"The ds:component-ref with id='{self._checklist_id} '" \ 170 | f"in ds:datastream with id='{self._data_stream_id}' " \ 171 | f"doesn't have a xlink:href attribute." 172 | raise SCAPContentHandlerError(msg) 173 | if not cref_href.startswith("#"): 174 | msg = f"The component {cref_href} isn't local." 175 | raise SCAPContentHandlerError(msg) 176 | component_id = cref_href[1:] 177 | component = self.root.find( 178 | f"ds:component[@id='{component_id}']", ns) 179 | if component is None: 180 | msg = f"Can't find component {component_id}" 181 | raise SCAPContentHandlerError(msg) 182 | benchmark = component.find("xccdf-1.1:Benchmark", ns) 183 | if benchmark is None: 184 | benchmark = component.find("xccdf-1.2:Benchmark", ns) 185 | if benchmark is None: 186 | msg = f"The component {cref_href} doesn't contain an XCCDF " \ 187 | "Benchmark." 188 | raise SCAPContentHandlerError(msg) 189 | return benchmark 190 | 191 | def get_profiles(self): 192 | """ 193 | Method to get a list of profiles defined in the currently selected 194 | checklist that is defined in the currently selected data stream. 195 | 196 | :return: list of profiles found in the checklist 197 | :rtype: list of ProfileInfo instances 198 | 199 | """ 200 | if self.scap_type not in ("XCCDF", "SCAP_SOURCE_DATA_STREAM"): 201 | msg = f"Unsupported SCAP content type '{self.scap_type}'." 202 | raise SCAPContentHandlerError(msg) 203 | if self.scap_type == "XCCDF" and ( 204 | self._data_stream_id is not None or 205 | self._checklist_id is not None): 206 | msg = "For XCCDF documents, the data_stream_id and checklist_id " \ 207 | "must be both None." 208 | raise SCAPContentHandlerError(msg) 209 | if self.scap_type == "SCAP_SOURCE_DATA_STREAM" and ( 210 | self._data_stream_id is None or self._checklist_id is None): 211 | msg = "For SCAP source data streams, data_stream_id and " \ 212 | "checklist_id must be both different than None" 213 | raise SCAPContentHandlerError(msg) 214 | 215 | if self.scap_type == "SCAP_SOURCE_DATA_STREAM": 216 | benchmark = self._find_benchmark_in_source_data_stream() 217 | else: 218 | benchmark = self.root 219 | benchmark_profiles = self._parse_profiles_from_xccdf(benchmark) 220 | tailoring_profiles = self._parse_profiles_from_xccdf(self.tailoring) 221 | return benchmark_profiles + tailoring_profiles 222 | -------------------------------------------------------------------------------- /org_fedora_oscap/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/org_fedora_oscap/service/__init__.py -------------------------------------------------------------------------------- /org_fedora_oscap/service/__main__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2020 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | 19 | # Initialize the service. 20 | from pyanaconda.modules.common import init 21 | init() 22 | 23 | # Start the service. 24 | from org_fedora_oscap.service.oscap import OSCAPService 25 | service = OSCAPService() 26 | service.run() 27 | -------------------------------------------------------------------------------- /org_fedora_oscap/service/kickstart.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2020 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | import logging 19 | import re 20 | 21 | from pyanaconda.core.kickstart import KickstartSpecification 22 | from pyanaconda.core.kickstart.addon import AddonData 23 | from pykickstart.errors import KickstartValueError, KickstartParseError 24 | 25 | from org_fedora_oscap import common, utils 26 | from org_fedora_oscap.structures import PolicyData 27 | 28 | log = logging.getLogger("anaconda") 29 | 30 | __all__ = ["OSCAPKickstartSpecification"] 31 | 32 | 33 | FINGERPRINT_REGEX = re.compile(r'^[a-z0-9]+$') 34 | 35 | 36 | def key_value_pair(key, value, indent=4): 37 | return "%s%s = %s" % (indent * " ", key, value) 38 | 39 | 40 | class AdditionalPropertiesMixin: 41 | @property 42 | def content_name(self) -> str: 43 | return common.get_content_name(self.policy_data) 44 | 45 | @property 46 | def preinst_content_path(self) -> str: 47 | return common.get_preinst_content_path(self.policy_data) 48 | 49 | @property 50 | def preinst_tailoring_path(self) -> str: 51 | return common.get_preinst_tailoring_path(self.policy_data) 52 | 53 | @property 54 | def postinst_content_path(self) -> str: 55 | return common.get_postinst_content_path(self.policy_data) 56 | 57 | @property 58 | def postinst_tailoring_path(self) -> str: 59 | return common.get_postinst_tailoring_path(self.policy_data) 60 | 61 | @property 62 | def raw_preinst_content_path(self) -> str: 63 | return common.get_raw_preinst_content_path(self.policy_data) 64 | 65 | 66 | class OSCAPKickstartData(AddonData, AdditionalPropertiesMixin): 67 | """The kickstart data for the add-on.""" 68 | 69 | def __init__(self): 70 | super().__init__() 71 | self.policy_data = PolicyData() 72 | 73 | """The name of the %addon section.""" 74 | self.name = common.ADDON_NAMES[0] 75 | self.addon_section_present = False 76 | 77 | def handle_header(self, args, line_number=None): 78 | """Handle the arguments of the %addon line. 79 | 80 | :param args: a list of additional arguments 81 | :param line_number: a line number 82 | :raise: KickstartParseError for invalid arguments 83 | """ 84 | self.addon_section_present = True 85 | 86 | def handle_line(self, line, line_number=None): 87 | """Handle one line of the section. 88 | 89 | :param line: a line to parse 90 | :param line_number: a line number 91 | :raise: KickstartParseError for invalid lines 92 | """ 93 | actions = { 94 | "content-type": self._parse_content_type, 95 | "content-url": self._parse_content_url, 96 | "content-path": self._parse_content_path, 97 | "datastream-id": self._parse_datastream_id, 98 | "profile": self._parse_profile_id, 99 | "xccdf-id": self._parse_xccdf_id, 100 | "xccdf-path": self._parse_content_path, 101 | "cpe-path": self._parse_cpe_path, 102 | "tailoring-path": self._parse_tailoring_path, 103 | "fingerprint": self._parse_fingerprint, 104 | "certificates": self._parse_certificates, 105 | "remediate": self._parse_remediate, 106 | } 107 | 108 | line = line.strip() 109 | (pre, sep, post) = line.partition("=") 110 | pre = pre.strip() 111 | post = post.strip() 112 | post = post.strip('"') 113 | 114 | try: 115 | actions[pre](post) 116 | except KeyError: 117 | msg = "Unknown item '%s' for %s addon" % (line, self.name) 118 | raise KickstartParseError(msg) 119 | 120 | def _parse_content_type(self, value): 121 | value_low = value.lower() 122 | if value_low in common.SUPPORTED_CONTENT_TYPES: 123 | self.policy_data.content_type = value_low 124 | else: 125 | msg = "Unsupported content type '%s' in the %s addon" % (value, 126 | self.name) 127 | raise KickstartValueError(msg) 128 | 129 | def _parse_content_url(self, value): 130 | if any(value.startswith(prefix) 131 | for prefix in common.SUPPORTED_URL_PREFIXES): 132 | self.policy_data.content_url = value 133 | else: 134 | msg = "Unsupported url '%s' in the %s addon" % (value, self.name) 135 | raise KickstartValueError(msg) 136 | 137 | def _parse_datastream_id(self, value): 138 | # need to be checked? 139 | self.policy_data.datastream_id = value 140 | 141 | def _parse_xccdf_id(self, value): 142 | # need to be checked? 143 | self.policy_data.xccdf_id = value 144 | 145 | def _parse_profile_id(self, value): 146 | # need to be checked? 147 | self.policy_data.profile_id = value 148 | 149 | def _parse_content_path(self, value): 150 | # need to be checked? 151 | self.policy_data.content_path = value 152 | 153 | def _parse_cpe_path(self, value): 154 | # need to be checked? 155 | self.policy_data.cpe_path = value 156 | 157 | def _parse_tailoring_path(self, value): 158 | # need to be checked? 159 | self.policy_data.tailoring_path = value 160 | 161 | def _parse_fingerprint(self, value): 162 | if FINGERPRINT_REGEX.match(value) is None: 163 | msg = "Unsupported or invalid fingerprint" 164 | raise KickstartValueError(msg) 165 | 166 | if utils.get_hashing_algorithm(value) is None: 167 | msg = "Unsupported fingerprint" 168 | raise KickstartValueError(msg) 169 | 170 | self.policy_data.fingerprint = value 171 | 172 | def _parse_certificates(self, value): 173 | self.policy_data.certificates = value 174 | 175 | def _parse_remediate(self, value): 176 | assert value in ("none", "post", "firstboot", "both") 177 | self.policy_data.remediate = value 178 | 179 | def handle_end(self): 180 | """Handle the end of the section.""" 181 | tmpl = "%s missing for the %s addon" 182 | 183 | # check provided data 184 | if not self.policy_data.content_type: 185 | raise KickstartValueError(tmpl % ("content-type", self.name)) 186 | 187 | if ( 188 | self.policy_data.content_type != "scap-security-guide" 189 | and not self.policy_data.content_url): 190 | raise KickstartValueError(tmpl % ("content-url", self.name)) 191 | 192 | if not self.policy_data.profile_id: 193 | self.policy_data.profile_id = "default" 194 | 195 | if ( 196 | self.policy_data.content_type in ("rpm", "archive") 197 | and not self.policy_data.content_path): 198 | msg = "Path to the XCCDF file has to be given if content in RPM "\ 199 | "or archive is used" 200 | raise KickstartValueError(msg) 201 | 202 | if ( 203 | self.policy_data.content_type == "rpm" 204 | and not self.policy_data.content_url.endswith(".rpm")): 205 | msg = "Content type set to RPM, but the content URL doesn't end "\ 206 | "with '.rpm'" 207 | raise KickstartValueError(msg) 208 | 209 | if self.policy_data.content_type == "archive": 210 | supported_archive = any( 211 | self.policy_data.content_url.endswith(arch_type) 212 | for arch_type in common.SUPPORTED_ARCHIVES 213 | ) 214 | if not supported_archive: 215 | msg = "Unsupported archive type of the content "\ 216 | "file '%s'" % self.policy_data.content_url 217 | raise KickstartValueError(msg) 218 | 219 | # do some initialization magic in case of SSG 220 | if self.policy_data.content_type == "scap-security-guide": 221 | if not common.ssg_available(): 222 | msg = "SCAP Security Guide not found on the system" 223 | raise KickstartValueError(msg) 224 | 225 | self.policy_data.content_path = common.SSG_DIR + common.SSG_CONTENT 226 | 227 | def __str__(self): 228 | """Generate the kickstart representation. 229 | 230 | What should end up in the resulting kickstart file, 231 | i.e. string representation of the stored data. 232 | 233 | :return: a string 234 | """ 235 | if not self.policy_data.profile_id: 236 | return "" 237 | 238 | ret = "%%addon %s" % self.name 239 | ret += "\n%s" % key_value_pair("content-type", self.policy_data.content_type) 240 | 241 | if self.policy_data.content_url: 242 | ret += "\n%s" % key_value_pair("content-url", self.policy_data.content_url) 243 | 244 | if self.policy_data.datastream_id: 245 | ret += "\n%s" % key_value_pair("datastream-id", self.policy_data.datastream_id) 246 | 247 | if self.policy_data.xccdf_id: 248 | ret += "\n%s" % key_value_pair("xccdf-id", self.policy_data.xccdf_id) 249 | 250 | if ( 251 | self.policy_data.content_path 252 | and self.policy_data.content_type != "scap-security-guide"): 253 | ret += "\n%s" % key_value_pair("content-path", self.policy_data.content_path) 254 | 255 | if self.policy_data.cpe_path: 256 | ret += "\n%s" % key_value_pair("cpe-path", self.policy_data.cpe_path) 257 | 258 | if self.policy_data.tailoring_path: 259 | ret += "\n%s" % key_value_pair("tailoring-path", self.policy_data.tailoring_path) 260 | 261 | ret += "\n%s" % key_value_pair("profile", self.policy_data.profile_id) 262 | 263 | if self.policy_data.fingerprint: 264 | ret += "\n%s" % key_value_pair("fingerprint", self.policy_data.fingerprint) 265 | 266 | if self.policy_data.certificates: 267 | ret += "\n%s" % key_value_pair("certificates", self.policy_data.certificates) 268 | 269 | if self.policy_data.remediate: 270 | ret += "\n%s" % key_value_pair("remediate", self.policy_data.remediate) 271 | 272 | ret += "\n%end\n\n" 273 | return ret 274 | 275 | 276 | def get_oscap_kickstart_data(name): 277 | class NamedOSCAPKickstartData(OSCAPKickstartData): 278 | def __init__(self): 279 | super().__init__() 280 | self.name = name 281 | 282 | return NamedOSCAPKickstartData 283 | 284 | 285 | class OSCAPKickstartSpecification(KickstartSpecification): 286 | """The kickstart specification of the OSCAP service.""" 287 | 288 | addons = { 289 | name: get_oscap_kickstart_data(name) for name in common.ADDON_NAMES 290 | } 291 | -------------------------------------------------------------------------------- /org_fedora_oscap/service/oscap.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2020 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | import logging 19 | import warnings 20 | 21 | from pykickstart.errors import KickstartDeprecationWarning 22 | from pyanaconda.core.configuration.anaconda import conf 23 | from pyanaconda.core.dbus import DBus 24 | from pyanaconda.core.signal import Signal 25 | from pyanaconda.modules.common.base import KickstartService 26 | from pyanaconda.modules.common.containers import TaskContainer 27 | from pyanaconda.modules.common.structures.requirement import Requirement 28 | 29 | from org_fedora_oscap import common 30 | from org_fedora_oscap.constants import OSCAP 31 | from org_fedora_oscap.service.installation import PrepareValidContent, \ 32 | EvaluateRulesTask, InstallContentTask, RemediateSystemTask, ScheduleFirstbootRemediationTask 33 | from org_fedora_oscap.service.kickstart import OSCAPKickstartSpecification, KickstartParseError 34 | from org_fedora_oscap.service.oscap_interface import OSCAPInterface 35 | from org_fedora_oscap.structures import PolicyData 36 | 37 | log = logging.getLogger("anaconda") 38 | 39 | __all__ = ["OSCAPService"] 40 | 41 | 42 | class OSCAPService(KickstartService): 43 | """The implementation of the OSCAP service.""" 44 | 45 | def __init__(self): 46 | """Create a service.""" 47 | super().__init__() 48 | self._policy_enabled = True 49 | self.policy_enabled_changed = Signal() 50 | 51 | self._policy_data = PolicyData() 52 | self.policy_data_changed = Signal() 53 | 54 | self.installation_canceled = Signal() 55 | 56 | self.canonical_addon_name = common.ADDON_NAMES[0] 57 | 58 | @property 59 | def policy_enabled(self): 60 | """Is the security policy enabled? 61 | 62 | :return: True or False 63 | """ 64 | return self._policy_enabled 65 | 66 | @policy_enabled.setter 67 | def policy_enabled(self, value): 68 | """Should be the security policy enabled? 69 | 70 | :param value: True or False 71 | """ 72 | self._policy_enabled = value 73 | self.policy_enabled_changed.emit() 74 | log.debug("OSCAP Addon: Policy enabled is set to '%s'.", value) 75 | 76 | @property 77 | def policy_data(self): 78 | """The security policy data. 79 | 80 | :return: an instance of PolicyData 81 | """ 82 | return self._policy_data 83 | 84 | @policy_data.setter 85 | def policy_data(self, value): 86 | """Set the security policy data. 87 | 88 | :param value: an instance of PolicyData 89 | """ 90 | self._policy_data = value 91 | self.policy_data_changed.emit() 92 | log.debug("OSCAP Addon: Policy data is set to '%s'.", value) 93 | 94 | @property 95 | def installation_enabled(self): 96 | """Is the installation enabled? 97 | 98 | :return: True or False 99 | """ 100 | return self.policy_enabled and self.policy_data.profile_id 101 | 102 | def publish(self): 103 | """Publish the DBus objects.""" 104 | TaskContainer.set_namespace(OSCAP.namespace) 105 | DBus.publish_object(OSCAP.object_path, OSCAPInterface(self)) 106 | DBus.register_service(OSCAP.service_name) 107 | 108 | @property 109 | def kickstart_specification(self): 110 | """Return the kickstart specification.""" 111 | return OSCAPKickstartSpecification 112 | 113 | def process_kickstart(self, data): 114 | """Process the kickstart data.""" 115 | preferred_section_header = f"%addon {self.canonical_addon_name}" 116 | all_addon_data = [ 117 | getattr(data.addons, name) for name in common.ADDON_NAMES] 118 | relevant_data = [d for d in all_addon_data if d.addon_section_present] 119 | if len(relevant_data) > 1: 120 | msg = common._( 121 | "You have used more than one oscap addon sections in the kickstart. " 122 | f"Please use only one, preferably '{preferred_section_header}'.") 123 | raise KickstartParseError(msg) 124 | if len(relevant_data) == 0: 125 | addon_data = all_addon_data[0] 126 | else: 127 | addon_data = relevant_data[0] 128 | 129 | self.policy_data = addon_data.policy_data 130 | 131 | if (common.COMPLAIN_ABOUT_NON_CANONICAL_NAMES 132 | and addon_data.name != self.canonical_addon_name): 133 | used_section_header = f"%addon {addon_data.name}" 134 | msg = common._( 135 | f"You have configured the oscap addon using '{used_section_header}' section. " 136 | f"Please update your configuration and use '{preferred_section_header}'. " 137 | "Support for legacy sections will be removed in the future major version.") 138 | warnings.warn(msg, KickstartDeprecationWarning) 139 | 140 | def setup_kickstart(self, data): 141 | """Set the given kickstart data.""" 142 | policy_data = self.policy_data 143 | addon_data = getattr(data.addons, self.canonical_addon_name) 144 | 145 | addon_data.policy_data = policy_data 146 | 147 | def collect_requirements(self): 148 | """Return installation requirements. 149 | 150 | :return: a list of requirements 151 | """ 152 | if not self.installation_enabled: 153 | log.debug("OSCAP Addon: The installation is disabled. Skip the requirements.") 154 | return [] 155 | 156 | requirements = [ 157 | Requirement.for_package( 158 | package_name="openscap", 159 | reason="Required by oscap add-on." 160 | ), 161 | Requirement.for_package( 162 | package_name="openscap-scanner", 163 | reason="Required by oscap add-on." 164 | ) 165 | ] 166 | 167 | if self.policy_data.content_type == "scap-security-guide": 168 | requirements.append( 169 | Requirement.for_package( 170 | package_name="scap-security-guide", 171 | reason="Required by oscap add-on." 172 | ) 173 | ) 174 | 175 | return requirements 176 | 177 | def configure_with_tasks(self): 178 | """Return configuration tasks. 179 | 180 | :return: a list of tasks 181 | """ 182 | if not self.installation_enabled: 183 | log.debug("OSCAP Addon: The installation is disabled. Skip the configuration.") 184 | return [] 185 | 186 | tasks = [ 187 | PrepareValidContent( 188 | policy_data=self.policy_data, 189 | file_path=common.get_raw_preinst_content_path(self.policy_data), 190 | content_path=common.get_preinst_content_path(self.policy_data), 191 | ), 192 | EvaluateRulesTask( 193 | policy_data=self.policy_data, 194 | content_path=common.get_preinst_content_path(self.policy_data), 195 | tailoring_path=common.get_preinst_tailoring_path(self.policy_data), 196 | ), 197 | ] 198 | 199 | self._cancel_tasks_on_error(tasks) 200 | return tasks 201 | 202 | def install_with_tasks(self): 203 | """Return installation tasks. 204 | 205 | :return: a list of tasks 206 | """ 207 | if not self.installation_enabled: 208 | log.debug("OSCAP Addon: The installation is disabled. Skip the installation.") 209 | return [] 210 | 211 | tasks = [] 212 | tasks.append(InstallContentTask( 213 | sysroot=conf.target.system_root, 214 | policy_data=self.policy_data, 215 | file_path=common.get_raw_preinst_content_path(self.policy_data), 216 | content_path=common.get_preinst_content_path(self.policy_data), 217 | tailoring_path=common.get_preinst_tailoring_path(self.policy_data), 218 | target_directory=common.TARGET_CONTENT_DIR 219 | )) 220 | if self.policy_data.remediate in ("", "post", "both"): 221 | tasks.append(RemediateSystemTask( 222 | sysroot=conf.target.system_root, 223 | policy_data=self.policy_data, 224 | target_content_path=common.get_postinst_content_path(self.policy_data), 225 | target_tailoring_path=common.get_postinst_tailoring_path(self.policy_data) 226 | )) 227 | 228 | if self.policy_data.remediate in ("firstboot", "both"): 229 | tasks.append(ScheduleFirstbootRemediationTask( 230 | sysroot=conf.target.system_root, 231 | policy_data=self.policy_data, 232 | target_content_path=common.get_postinst_content_path(self.policy_data), 233 | target_tailoring_path=common.get_postinst_tailoring_path(self.policy_data) 234 | )) 235 | 236 | self._cancel_tasks_on_error(tasks) 237 | return tasks 238 | 239 | def _cancel_tasks_on_error(self, tasks): 240 | """Cancel all tasks on error. 241 | 242 | If one of the installation tasks fails, we will emit the 243 | installation_canceled signal that will cancel all scheduled 244 | installation tasks. 245 | 246 | This signal allows to cancel tasks from the install_with_tasks 247 | method based on a failure of a task from the configure_with_tasks 248 | method. All these tasks are created and scheduled before Anaconda 249 | starts to execute them. 250 | 251 | :param tasks: a list of tasks 252 | """ 253 | for task in tasks: 254 | # Cancel the installation if the task fails. 255 | task.failed_signal.connect(self.installation_canceled.emit) 256 | 257 | # Cancel the task if the installation was canceled. 258 | self.installation_canceled.connect(task.cancel) 259 | -------------------------------------------------------------------------------- /org_fedora_oscap/service/oscap_interface.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2020 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | from dasbus.server.interface import dbus_interface 19 | from dasbus.server.property import emits_properties_changed 20 | from dasbus.typing import * # pylint: disable=wildcard-import 21 | 22 | from pyanaconda.modules.common.base import KickstartModuleInterface 23 | 24 | from org_fedora_oscap.constants import OSCAP 25 | from org_fedora_oscap.structures import PolicyData 26 | 27 | __all__ = ["OSCAPInterface"] 28 | 29 | 30 | @dbus_interface(OSCAP.interface_name) 31 | class OSCAPInterface(KickstartModuleInterface): 32 | """The DBus interface of the OSCAP service.""" 33 | 34 | def connect_signals(self): 35 | super().connect_signals() 36 | self.watch_property("PolicyEnabled", self.implementation.policy_enabled_changed) 37 | self.watch_property("PolicyData", self.implementation.policy_data_changed) 38 | 39 | @property 40 | def PolicyEnabled(self) -> Bool: 41 | """Is the security policy enabled? 42 | 43 | :return: True or False 44 | """ 45 | return self.implementation.policy_enabled 46 | 47 | @PolicyEnabled.setter 48 | @emits_properties_changed 49 | def PolicyEnabled(self, value: Bool): 50 | """Should be the security policy enabled? 51 | 52 | :param value: True or False 53 | """ 54 | self.implementation.policy_enabled = value 55 | 56 | @property 57 | def PolicyData(self) -> Structure: 58 | """The security policy data. 59 | 60 | :return: a structure defined by the PolicyData class 61 | """ 62 | return PolicyData.to_structure(self.implementation.policy_data) 63 | 64 | @PolicyData.setter 65 | @emits_properties_changed 66 | def PolicyData(self, value: Structure): 67 | """Set the security policy data. 68 | 69 | :param value: a structure defined by the PolicyData class 70 | """ 71 | self.implementation.policy_data = PolicyData.from_structure(value) 72 | -------------------------------------------------------------------------------- /org_fedora_oscap/structures.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2020 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | from dasbus.structure import DBusData 19 | from dasbus.typing import * # pylint: disable=wildcard-import 20 | 21 | __all__ = ["PolicyData"] 22 | 23 | 24 | class PolicyData(DBusData): 25 | """The security policy data.""" 26 | 27 | def __init__(self): 28 | # values specifying the content 29 | self._content_type = "" 30 | self._content_url = "" 31 | self._datastream_id = "" 32 | self._xccdf_id = "" 33 | self._profile_id = "" 34 | self._content_path = "" 35 | self._cpe_path = "" 36 | self._tailoring_path = "" 37 | self._fingerprint = "" 38 | self._certificates = "" 39 | self._remediate = "" 40 | 41 | def update_from(self, rhs): 42 | self._content_type = rhs._content_type 43 | self._content_url = rhs._content_url 44 | self._datastream_id = rhs._datastream_id 45 | self._xccdf_id = rhs._xccdf_id 46 | self._profile_id = rhs._profile_id 47 | self._content_path = rhs._content_path 48 | self._cpe_path = rhs._cpe_path 49 | self._tailoring_path = rhs._tailoring_path 50 | self._fingerprint = rhs._fingerprint 51 | self._certificates = rhs._certificates 52 | self._remediate = rhs._remediate 53 | 54 | @property 55 | def content_type(self) -> Str: 56 | """Type of the security content. 57 | 58 | If the content type is scap-security-guide, the add-on 59 | will use content provided by the scap-security-guide. 60 | All other attributes except profile will have no effect. 61 | 62 | Supported values: 63 | 64 | datastream 65 | archive 66 | rpm 67 | scap-security-guide 68 | 69 | :return: a string 70 | """ 71 | return self._content_type 72 | 73 | @content_type.setter 74 | def content_type(self, value: Str): 75 | self._content_type = value 76 | 77 | @property 78 | def content_url(self) -> Str: 79 | """Location of the security content. 80 | 81 | So far only http, https, and ftp URLs are supported. 82 | 83 | :return: an URL 84 | """ 85 | return self._content_url 86 | 87 | @content_url.setter 88 | def content_url(self, value: Str): 89 | self._content_url = value 90 | 91 | @property 92 | def datastream_id(self) -> Str: 93 | """ID of the data stream. 94 | 95 | It is an ID of the data stream from a datastream 96 | collection referenced by the content url. Used only 97 | if the content type is datastream. 98 | 99 | :return: a string 100 | """ 101 | return self._datastream_id 102 | 103 | @datastream_id.setter 104 | def datastream_id(self, value: Str): 105 | self._datastream_id = value 106 | 107 | @property 108 | def xccdf_id(self) -> Str: 109 | """ID of the benchmark that should be used. 110 | 111 | :return: a string 112 | """ 113 | return self._xccdf_id 114 | 115 | @xccdf_id.setter 116 | def xccdf_id(self, value: Str): 117 | self._xccdf_id = value 118 | 119 | @property 120 | def profile_id(self) -> Str: 121 | """ID of the profile that should be applied. 122 | 123 | Use 'default' if the default profile should be used. 124 | 125 | :return: a string 126 | """ 127 | return self._profile_id 128 | 129 | @profile_id.setter 130 | def profile_id(self, value: Str): 131 | self._profile_id = value 132 | 133 | @property 134 | def content_path(self) -> Str: 135 | """Path to the datastream or the XCCDF file which should be used. 136 | 137 | :return: a relative path in the archive 138 | """ 139 | return self._content_path 140 | 141 | @content_path.setter 142 | def content_path(self, value: Str): 143 | self._content_path = value 144 | 145 | @property 146 | def cpe_path(self) -> Str: 147 | """Path to the datastream or the XCCDF file that should be used. 148 | 149 | :return: a relative path in the archive 150 | """ 151 | return self._cpe_path 152 | 153 | @cpe_path.setter 154 | def cpe_path(self, value: Str): 155 | self._cpe_path = value 156 | 157 | @property 158 | def tailoring_path(self) -> Str: 159 | """Path of the tailoring file that should be used. 160 | 161 | :return: a relative path in the archive 162 | """ 163 | return self._tailoring_path 164 | 165 | @tailoring_path.setter 166 | def tailoring_path(self, value: Str): 167 | self._tailoring_path = value 168 | 169 | @property 170 | def fingerprint(self) -> Str: 171 | """Checksum of the security content. 172 | 173 | It is an MD5, SHA1 or SHA2 fingerprint/hash/checksum 174 | of the content referred by the content url. 175 | 176 | :return: a string 177 | """ 178 | return self._fingerprint 179 | 180 | @fingerprint.setter 181 | def fingerprint(self, value: Str): 182 | self._fingerprint = value 183 | 184 | @property 185 | def certificates(self) -> Str: 186 | """Path to a PEM file with CA certificate chain. 187 | 188 | :return: a path 189 | """ 190 | return self._certificates 191 | 192 | @certificates.setter 193 | def certificates(self, value: Str): 194 | self._certificates = value 195 | 196 | @property 197 | def remediate(self) -> Str: 198 | """What remediations to perform 199 | 200 | :return: a remediation mode 201 | """ 202 | return self._remediate 203 | 204 | @remediate.setter 205 | def remediate(self, value: Str): 206 | self._remediate = value 207 | -------------------------------------------------------------------------------- /org_fedora_oscap/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2013 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | # Red Hat Author(s): Vratislav Podzimek 19 | # 20 | 21 | """Module with various utility functions used by the addon.""" 22 | 23 | import os 24 | import os.path 25 | import shutil 26 | import glob 27 | import hashlib 28 | 29 | 30 | def ensure_dir_exists(dirpath): 31 | """ 32 | Checks if a given directory exists and if not, it creates the directory as 33 | well as all the nonexisting directories in its path. 34 | 35 | :param dirpath: path to the directory to be checked/created 36 | :type dirpath: str 37 | 38 | """ 39 | 40 | if not dirpath: 41 | # nothing can be done for an empty string 42 | return 43 | 44 | if not os.path.isdir(dirpath): 45 | os.makedirs(dirpath) 46 | 47 | 48 | def universal_copy(src, dst): 49 | """ 50 | Function that copies the files or directories specified by the src argument 51 | to the destination given by the dst argument. It should follow the same 52 | rules as the standard 'cp' utility. 53 | 54 | :param src: source to copy -- may be a glob, file path or a directory path 55 | :type src: str 56 | :param dst: destination to copy to 57 | :type src: str 58 | 59 | """ 60 | 61 | if glob.has_magic(src): 62 | # src is a glob 63 | sources = glob.glob(src) 64 | else: 65 | # not a glob 66 | sources = [src] 67 | 68 | for item in sources: 69 | if os.path.isdir(item): 70 | if os.path.isdir(dst): 71 | item = item.rstrip("/") 72 | dirname = item.rsplit("/", 1)[-1] 73 | shutil.copytree(item, join_paths(dst, dirname)) 74 | else: 75 | shutil.copytree(item, dst) 76 | else: 77 | shutil.copy2(item, dst) 78 | 79 | 80 | def keep_type_map(func, iterable): 81 | """ 82 | Function that maps the given function to items in the given iterable 83 | keeping the type of the iterable. 84 | 85 | :param func: function to be mapped on the items in the iterable 86 | :type func: in_item -> out_item 87 | :param iterable: iterable providing the items the function should be mapped 88 | on 89 | :type iterable: iterable 90 | :return: iterable providing items produced by the function mapped on the 91 | input items 92 | :rtype: the same type as input iterable or generator if the iterable is not 93 | of any basic Python types 94 | 95 | """ 96 | 97 | if isinstance(iterable, dict): 98 | return dict((func(key), iterable[key]) for key in iterable) 99 | 100 | items_gen = (func(item) for item in iterable) 101 | if isinstance(iterable, list): 102 | return list(items_gen) 103 | elif isinstance(iterable, tuple): 104 | if iterable.__class__ is tuple: 105 | return tuple(items_gen) 106 | else: 107 | return iterable.__class__(*items_gen) 108 | elif isinstance(iterable, set): 109 | return set(items_gen) 110 | elif isinstance(iterable, str): 111 | return "".join(items_gen) 112 | else: 113 | return items_gen 114 | 115 | 116 | def join_paths(path1, path2): 117 | """ 118 | Joins two paths as one would expect -- i.e. just like the os.path.join 119 | function except for doing crazy things when the second argument is an 120 | absolute path. 121 | 122 | :param path1: first path 123 | :type path1: str 124 | :param path2: second path 125 | :type path2: str 126 | :return: path1 and path2 joined with the file separator 127 | :rtype: str 128 | 129 | """ 130 | 131 | # os.path.normpath doesn't squash two starting slashes 132 | path1.replace("//", "/") 133 | 134 | return os.path.normpath(path1 + os.path.sep + path2) 135 | 136 | 137 | def get_hashing_algorithm(fingerprint): 138 | """ 139 | Get hashing algorithm for the given fingerprint or None if fingerprint of 140 | unsupported length is given. 141 | 142 | :param fingerprint: hexa fingerprint to get the hashing algorithm for 143 | :type fingerprint: hexadecimal str 144 | :return: one of the hashlib.* hash objects 145 | :rtype: hashlib.HASH object 146 | 147 | """ 148 | 149 | hashes = (hashlib.md5(), hashlib.sha1(), hashlib.sha224(), 150 | hashlib.sha256(), hashlib.sha384(), hashlib.sha512()) 151 | 152 | if len(fingerprint) % 2 == 1: 153 | return None 154 | 155 | num_bytes = len(fingerprint) / 2 156 | 157 | for hash_obj in hashes: 158 | # pylint: disable-msg=E1103 159 | if hash_obj.digest_size == num_bytes: 160 | return hash_obj 161 | 162 | return None 163 | 164 | 165 | def get_file_fingerprint(fpath, hash_obj): 166 | """ 167 | Get fingerprint of the given file with the given hashing algorithm. 168 | 169 | :param fpath: path to the file to get fingerprint for 170 | :type fpath: str 171 | :param hash_obj: hashing algorithm to get fingerprint with 172 | :type hash_obj: hashlib.HASH 173 | :return: fingerprint of the given file with the given algorithm 174 | :rtype: hexadecimal str 175 | 176 | """ 177 | 178 | with open(fpath, "rb") as fobj: 179 | bsize = 4 * 1024 180 | # process file as 4 KB blocks 181 | buf = fobj.read(bsize) 182 | while buf: 183 | hash_obj.update(buf) 184 | buf = fobj.read(bsize) 185 | 186 | return hash_obj.hexdigest() 187 | -------------------------------------------------------------------------------- /oscap-anaconda-addon.spec: -------------------------------------------------------------------------------- 1 | %if 0%{?rhel} == 8 2 | %define anaconda_core_version 33 3 | %endif 4 | %if 0%{?rhel} == 9 5 | %define anaconda_core_version 34 6 | %endif 7 | %if 0%{?fedora} 8 | %define anaconda_core_version %{fedora} 9 | %endif 10 | 11 | Name: oscap-anaconda-addon 12 | Version: 0.37.0 13 | Release: 0%{?dist} 14 | Summary: Anaconda addon integrating OpenSCAP to the installation process 15 | 16 | License: GPLv2+ 17 | URL: https://github.com/OpenSCAP/oscap-anaconda-addon 18 | Source0: https://github.com/OpenSCAP/oscap-anaconda-addon/releases/download/r%{version}/%{name}-%{version}.tar.gz 19 | 20 | BuildArch: noarch 21 | BuildRequires: make 22 | BuildRequires: gettext 23 | BuildRequires: python3-devel 24 | BuildRequires: python3-pycurl 25 | BuildRequires: openscap openscap-utils openscap-python3 26 | BuildRequires: anaconda-core >= %{anaconda_core_version} 27 | Requires: anaconda-core >= %{anaconda_core_version} 28 | Requires: python3-pycurl 29 | Requires: python3-kickstart 30 | Requires: openscap openscap-utils openscap-python3 31 | Requires: scap-security-guide 32 | 33 | %description 34 | This is an addon that integrates OpenSCAP utilities with the Anaconda installer 35 | and allows installation of systems following restrictions given by a SCAP 36 | content. 37 | 38 | %prep 39 | %autosetup -p1 40 | 41 | %build 42 | 43 | %check 44 | 45 | %install 46 | make install DESTDIR=%{buildroot} DEFAULT_INSTALL_OF_PO_FILES=no 47 | 48 | %files 49 | %{_datadir}/anaconda/addons/org_fedora_oscap 50 | %{_datadir}/anaconda/dbus/confs/org.fedoraproject.Anaconda.Addons.OSCAP.conf 51 | %{_datadir}/anaconda/dbus/services/org.fedoraproject.Anaconda.Addons.OSCAP.service 52 | -------------------------------------------------------------------------------- /po/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # taken from python-meh sources 3 | # Makefile for the PO files (translation) catalog 4 | # 5 | # $Id$ 6 | 7 | TOP = ../.. 8 | 9 | # What is this package? 10 | NLSPACKAGE = oscap-anaconda-addon 11 | POTFILE = $(NLSPACKAGE).pot 12 | INSTALL = /usr/bin/install -c 13 | INSTALL_DATA = $(INSTALL) -m 644 14 | INSTALL_DIR = /usr/bin/install -d 15 | 16 | # destination directory 17 | INSTALL_NLS_DIR = $(RPM_BUILD_ROOT)/usr/share/locale 18 | 19 | # PO catalog handling 20 | MSGMERGE = msgmerge -v 21 | XGETTEXT = xgettext --default-domain=$(NLSPACKAGE) \ 22 | --add-comments 23 | MSGFMT = msgfmt --statistics --verbose 24 | 25 | # What do we need to do 26 | POFILES = $(wildcard *.po) 27 | MOFILES = $(patsubst %.po,%.mo,$(POFILES)) 28 | PYSRC = $(wildcard ../org_fedora_oscap/*.py ../org_fedora_oscap/*/*.py ../org_fedora_oscap/*/*/*.py) 29 | GLADEFILES = $(wildcard ../org_fedora_oscap/*/*/*.glade) 30 | 31 | all:: update-po $(MOFILES) 32 | 33 | potfile: $(PYSRC) glade-po 34 | $(XGETTEXT) -L Python --keyword=_ --keyword=N_ $(PYSRC) tmp/*.h 35 | @if cmp -s $(NLSPACKAGE).po $(POTFILE); then \ 36 | rm -f $(NLSPACKAGE).po; \ 37 | else \ 38 | mv -f $(NLSPACKAGE).po $(POTFILE); \ 39 | fi; \ 40 | rm -rf tmp/ 41 | 42 | glade-po: $(GLADEFILES) 43 | rm -rf tmp/ 44 | @which intltool-extract > /dev/null 2>&1 || echo "You may not have the intltool-extract installed, don't be surprised if the operation fails." 45 | for f in $(GLADEFILES); do \ 46 | intltool-extract --type=gettext/glade -l $$f ;\ 47 | done 48 | 49 | update-po: Makefile refresh-po 50 | 51 | refresh-po: Makefile 52 | for cat in $(POFILES); do \ 53 | lang=`basename $$cat .po`; \ 54 | if $(MSGMERGE) $$lang.po $(POTFILE) > $$lang.pot ; then \ 55 | mv -f $$lang.pot $$lang.po ; \ 56 | echo "$(MSGMERGE) of $$lang succeeded" ; \ 57 | else \ 58 | echo "$(MSGMERGE) of $$lang failed" ; \ 59 | rm -f $$lang.pot ; \ 60 | fi \ 61 | done 62 | 63 | clean: 64 | @rm -fv *mo *~ .depend 65 | @rm -rf tmp 66 | 67 | install: $(MOFILES) 68 | @for n in $(MOFILES); do \ 69 | l=`basename $$n .mo`; \ 70 | $(INSTALL_DIR) $(INSTALL_NLS_DIR)/$$l/LC_MESSAGES; \ 71 | $(INSTALL_DATA) --verbose $$n $(INSTALL_NLS_DIR)/$$l/LC_MESSAGES/$(NLSPACKAGE).mo; \ 72 | done 73 | 74 | uninstall: 75 | rm -rfv $(INSTALL_NLS_DIR)/*/LC_MESSAGES/$(NLSPACKAGE).mo 76 | 77 | %.mo: %.po 78 | $(MSGFMT) -o $@ $< 79 | 80 | .PHONY: missing depend 81 | 82 | -------------------------------------------------------------------------------- /po/oscap-anaconda-addon.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-08-20 17:44+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: ../org_fedora_oscap/common.py:358 21 | #, python-brace-format 22 | msgid "Error extracting archive as a zipfile: {exc}" 23 | msgstr "" 24 | 25 | #: ../org_fedora_oscap/content_discovery.py:190 26 | #, python-brace-format 27 | msgid "" 28 | "OSCAP Addon: Integrity check of the content failed - {hash_obj.name} hash " 29 | "didn't match" 30 | msgstr "" 31 | 32 | #: ../org_fedora_oscap/data_fetch.py:117 33 | msgid "Network connection needed to fetch data." 34 | msgstr "" 35 | 36 | #: ../org_fedora_oscap/data_fetch.py:262 37 | #, python-brace-format 38 | msgid "" 39 | "Failed to fetch data - the request returned HTTP error code {return_code}" 40 | msgstr "" 41 | 42 | #: ../org_fedora_oscap/rule_handling.py:441 43 | #, python-brace-format 44 | msgid "" 45 | "{0} must be on a separate partition or logical volume and has to be created " 46 | "in the partitioning layout before installation can occur with a security " 47 | "profile" 48 | msgstr "" 49 | 50 | #. template for the message 51 | #: ../org_fedora_oscap/rule_handling.py:452 52 | #, python-format 53 | msgid "" 54 | "mount option '%(mount_option)s' added for the mount point %(mount_point)s" 55 | msgstr "" 56 | 57 | #. root password was not set 58 | #: ../org_fedora_oscap/rule_handling.py:560 59 | #, python-format 60 | msgid "make sure to create password with minimal length of %d characters" 61 | msgstr "" 62 | 63 | #: ../org_fedora_oscap/rule_handling.py:567 64 | msgid "cannot check root password length (password is crypted)" 65 | msgstr "" 66 | 67 | #. too short 68 | #: ../org_fedora_oscap/rule_handling.py:573 69 | #, python-format 70 | msgid "" 71 | "root password is too short, a longer one with at least %d characters is " 72 | "required" 73 | msgstr "" 74 | 75 | #: ../org_fedora_oscap/rule_handling.py:712 76 | #: ../org_fedora_oscap/rule_handling.py:727 77 | #, python-format 78 | msgid "package '%s' has been added to the list of to be installed packages" 79 | msgstr "" 80 | 81 | #: ../org_fedora_oscap/rule_handling.py:737 82 | #, python-brace-format 83 | msgid "" 84 | "package '{package}' has been added to the list of excluded packages, but it " 85 | "can't be removed from the current software selection without breaking the " 86 | "installation." 87 | msgstr "" 88 | 89 | #: ../org_fedora_oscap/rule_handling.py:744 90 | #: ../org_fedora_oscap/rule_handling.py:759 91 | #, python-format 92 | msgid "package '%s' has been added to the list of excluded packages" 93 | msgstr "" 94 | 95 | #: ../org_fedora_oscap/rule_handling.py:866 96 | msgid "Kdump will be disabled on startup" 97 | msgstr "" 98 | 99 | #: ../org_fedora_oscap/rule_handling.py:868 100 | msgid "Kdump will be enabled on startup" 101 | msgstr "" 102 | 103 | #: ../org_fedora_oscap/rule_handling.py:1026 104 | msgid "Firewall will be disabled on startup" 105 | msgstr "" 106 | 107 | #: ../org_fedora_oscap/rule_handling.py:1033 108 | msgid "Firewall will be enabled on startup" 109 | msgstr "" 110 | 111 | #: ../org_fedora_oscap/rule_handling.py:1041 112 | #: ../org_fedora_oscap/rule_handling.py:1080 113 | #, python-format 114 | msgid "" 115 | "service '%s' has been added to the list of services to be added to the " 116 | "firewall" 117 | msgstr "" 118 | 119 | #: ../org_fedora_oscap/rule_handling.py:1048 120 | #: ../org_fedora_oscap/rule_handling.py:1093 121 | #, python-format 122 | msgid "" 123 | "port '%s' has been added to the list of ports to be added to the firewall" 124 | msgstr "" 125 | 126 | #: ../org_fedora_oscap/rule_handling.py:1055 127 | #: ../org_fedora_oscap/rule_handling.py:1106 128 | #, python-format 129 | msgid "" 130 | "trust '%s' has been added to the list of trusts to be added to the firewall" 131 | msgstr "" 132 | 133 | #: ../org_fedora_oscap/rule_handling.py:1118 134 | #: ../org_fedora_oscap/rule_handling.py:1133 135 | #, python-format 136 | msgid "" 137 | "service '%s' has been added to the list of services to be removed from the " 138 | "firewall" 139 | msgstr "" 140 | 141 | #: ../org_fedora_oscap/ks/oscap.py:375 142 | #: ../org_fedora_oscap/service/installation.py:56 143 | msgid "The installation should be aborted." 144 | msgstr "" 145 | 146 | #: ../org_fedora_oscap/ks/oscap.py:376 147 | msgid "Do you wish to continue anyway?" 148 | msgstr "" 149 | 150 | #: ../org_fedora_oscap/ks/oscap.py:399 151 | #: ../org_fedora_oscap/service/installation.py:41 152 | msgid "The integrity check of the security content failed." 153 | msgstr "" 154 | 155 | #: ../org_fedora_oscap/ks/oscap.py:403 156 | #: ../org_fedora_oscap/service/installation.py:46 157 | msgid "There was an error fetching and loading the security content:\n" 158 | msgstr "" 159 | 160 | #: ../org_fedora_oscap/ks/oscap.py:408 161 | #: ../org_fedora_oscap/service/installation.py:51 162 | #: ../org_fedora_oscap/gui/spokes/oscap.py:803 163 | msgid "There was an unexpected problem with the supplied content." 164 | msgstr "" 165 | 166 | #: ../org_fedora_oscap/ks/oscap.py:461 167 | #: ../org_fedora_oscap/service/installation.py:148 168 | msgid "Wrong configuration detected!" 169 | msgstr "" 170 | 171 | #: ../org_fedora_oscap/service/oscap.py:121 172 | msgid "You have used more than one oscap addon sections in the kickstart. " 173 | msgstr "" 174 | 175 | #: ../org_fedora_oscap/service/oscap.py:135 176 | #, python-brace-format 177 | msgid "" 178 | "You have configured the oscap addon using '{used_section_header}' section. " 179 | msgstr "" 180 | 181 | #. title of the spoke (will be displayed on the hub) 182 | #: ../org_fedora_oscap/gui/spokes/oscap.py:201 183 | msgid "_Security Profile" 184 | msgstr "" 185 | 186 | #. the first status provided 187 | #: ../org_fedora_oscap/gui/spokes/oscap.py:228 188 | msgid "Not ready" 189 | msgstr "" 190 | 191 | #: ../org_fedora_oscap/gui/spokes/oscap.py:403 192 | msgid "Fetching content data" 193 | msgstr "" 194 | 195 | #: ../org_fedora_oscap/gui/spokes/oscap.py:441 196 | msgid "Fetch complete, analyzing data." 197 | msgstr "" 198 | 199 | #: ../org_fedora_oscap/gui/spokes/oscap.py:653 200 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1077 201 | msgid "No profile selected" 202 | msgstr "" 203 | 204 | #: ../org_fedora_oscap/gui/spokes/oscap.py:658 205 | msgid "No rules for the pre-installation phase" 206 | msgstr "" 207 | 208 | #: ../org_fedora_oscap/gui/spokes/oscap.py:811 209 | msgid "Invalid content provided. Enter a different URL, please." 210 | msgstr "" 211 | 212 | #: ../org_fedora_oscap/gui/spokes/oscap.py:819 213 | msgid "Invalid or unsupported content URL, please enter a different one." 214 | msgstr "" 215 | 216 | #: ../org_fedora_oscap/gui/spokes/oscap.py:827 217 | msgid "Failed to fetch content. Enter a different URL, please." 218 | msgstr "" 219 | 220 | #: ../org_fedora_oscap/gui/spokes/oscap.py:835 221 | msgid "" 222 | "Network error encountered when fetching data. Please check that network is " 223 | "setup and working." 224 | msgstr "" 225 | 226 | #: ../org_fedora_oscap/gui/spokes/oscap.py:844 227 | msgid "The integrity check of the content failed. Cannot use the content." 228 | msgstr "" 229 | 230 | #: ../org_fedora_oscap/gui/spokes/oscap.py:852 231 | #, python-format 232 | msgid "Failed to extract content (%s). Enter a different URL, please." 233 | msgstr "" 234 | 235 | #: ../org_fedora_oscap/gui/spokes/oscap.py:870 236 | #, python-format 237 | msgid "" 238 | "Profile with ID '%s' not defined in the content. Select a different profile, " 239 | "please" 240 | msgstr "" 241 | 242 | #: ../org_fedora_oscap/gui/spokes/oscap.py:889 243 | msgid "Not applying security profile" 244 | msgstr "" 245 | 246 | #. TRANSLATORS: the other choice if SCAP Security Guide is also 247 | #. available 248 | #: ../org_fedora_oscap/gui/spokes/oscap.py:926 249 | msgid " or enter data stream content or archive URL below:" 250 | msgstr "" 251 | 252 | #: ../org_fedora_oscap/gui/spokes/oscap.py:930 tmp/oscap.glade.h:12 253 | msgid "" 254 | "No content found. Please enter data stream content or archive URL below:" 255 | msgstr "" 256 | 257 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1067 258 | msgid "Error fetching and loading content" 259 | msgstr "" 260 | 261 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1074 262 | msgid "No content found" 263 | msgstr "" 264 | 265 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1085 266 | msgid "Misconfiguration detected" 267 | msgstr "" 268 | 269 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1091 270 | msgid "Warnings appeared" 271 | msgstr "" 272 | 273 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1093 274 | msgid "Everything okay" 275 | msgstr "" 276 | 277 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1177 278 | msgid "Invalid or unsupported URL" 279 | msgstr "" 280 | 281 | #: ../org_fedora_oscap/gui/spokes/oscap.py:1183 tmp/oscap.glade.h:14 282 | msgid "Fetching content..." 283 | msgstr "" 284 | 285 | #: tmp/oscap.glade.h:1 286 | msgid "SECURITY PROFILE" 287 | msgstr "" 288 | 289 | #: tmp/oscap.glade.h:2 290 | msgid "_Change content" 291 | msgstr "" 292 | 293 | #: tmp/oscap.glade.h:3 294 | msgid "Apply security policy:" 295 | msgstr "" 296 | 297 | #: tmp/oscap.glade.h:4 298 | msgid "Data stream:" 299 | msgstr "" 300 | 301 | #: tmp/oscap.glade.h:5 302 | msgid "Checklist:" 303 | msgstr "" 304 | 305 | #: tmp/oscap.glade.h:6 306 | msgid "Choose profile below:" 307 | msgstr "" 308 | 309 | #: tmp/oscap.glade.h:7 310 | msgid "Profile" 311 | msgstr "" 312 | 313 | #: tmp/oscap.glade.h:8 314 | msgid "Selected" 315 | msgstr "" 316 | 317 | #: tmp/oscap.glade.h:9 318 | msgid "_Select profile" 319 | msgstr "" 320 | 321 | #: tmp/oscap.glade.h:10 322 | msgid "Changes that were done or need to be done:" 323 | msgstr "" 324 | 325 | #: tmp/oscap.glade.h:11 326 | msgid "_Use SCAP Security Guide" 327 | msgstr "" 328 | 329 | #: tmp/oscap.glade.h:13 330 | msgid "_Fetch" 331 | msgstr "" 332 | -------------------------------------------------------------------------------- /testing_files/README.md: -------------------------------------------------------------------------------- 1 | # Testing files 2 | 3 | This directory contains files which can be used in unit tests or for manual testing. 4 | 5 | ## RPMs 6 | 7 | - **customized_stig-1-1.noarch.rpm** 8 | - RPM file with a SCAP source data stream and a tailoring file 9 | - customized profile ID: xccdf_org.ssgproject.content_profile_stig_customized 10 | - files shipped in this RPM: 11 | - /usr/share/xml/scap/customized_stig/ssg-rhel8-ds.xml 12 | - /usr/share/xml/scap/customized_stig/tailoring-xccdf.xml 13 | - **scap-security-guide.noarch.rpm** 14 | - a RPM package similar to a Fedora RPM package scap-security-guide 15 | - files shipped in this RPM: 16 | - /usr/share/doc/scap-security-guide/Contributors.md 17 | - /usr/share/doc/scap-security-guide/LICENSE 18 | - /usr/share/doc/scap-security-guide/README.md 19 | - /usr/share/man/man8/scap-security-guide.8.gz 20 | - /usr/share/scap-security-guide/ansible 21 | - /usr/share/scap-security-guide/ansible/ssg-fedora-role-default.yml 22 | - /usr/share/scap-security-guide/ansible/ssg-fedora-role-ospp.yml 23 | - /usr/share/scap-security-guide/ansible/ssg-fedora-role-pci-dss.yml 24 | - /usr/share/scap-security-guide/ansible/ssg-fedora-role-standard.yml 25 | - /usr/share/scap-security-guide/bash 26 | - /usr/share/scap-security-guide/bash/ssg-fedora-role-default.sh 27 | - /usr/share/scap-security-guide/bash/ssg-fedora-role-ospp.sh 28 | - /usr/share/scap-security-guide/bash/ssg-fedora-role-pci-dss.sh 29 | - /usr/share/scap-security-guide/bash/ssg-fedora-role-standard.sh 30 | - /usr/share/xml/scap/ssg/content 31 | - /usr/share/xml/scap/ssg/content/ssg-fedora-cpe-dictionary.xml 32 | - /usr/share/xml/scap/ssg/content/ssg-fedora-cpe-oval.xml 33 | - /usr/share/xml/scap/ssg/content/ssg-fedora-ds.xml 34 | - /usr/share/xml/scap/ssg/content/ssg-fedora-ocil.xml 35 | - /usr/share/xml/scap/ssg/content/ssg-fedora-oval.xml 36 | - /usr/share/xml/scap/ssg/content/ssg-fedora-xccdf.xml 37 | - **separate-scap-files-1-1.noarch.rpm** 38 | - contains SCAP content in form of separate components files (no data stream) 39 | - files shipped in this RPM: 40 | - /usr/share/xml/scap/separate-scap-files/ssg-rhel8-cpe-dictionary.xml 41 | - /usr/share/xml/scap/separate-scap-files/ssg-rhel8-cpe-oval.xml 42 | - /usr/share/xml/scap/separate-scap-files/ssg-rhel8-ocil.xml 43 | - /usr/share/xml/scap/separate-scap-files/ssg-rhel8-oval.xml 44 | - /usr/share/xml/scap/separate-scap-files/ssg-rhel8-xccdf.xml 45 | - **single-ds-1-1.noarch.rpm** 46 | - contains a single SCAP source data stream which is a common RHEL 8 SCAP content 47 | - files shipped in this RPM: 48 | - /usr/share/xml/scap/single-ds/some_rhel8_content.xml 49 | - **ssg-fedora-ds-tailoring-1-1.noarch.rpm** 50 | - RPM file containing a SCAP source data stream and a tailoring file 51 | - customized profile ID: xccdf_org.ssgproject.content_profile_ospp_customized2 52 | - files shipped in this RPM: 53 | - /usr/share/xml/scap/ssg-fedora-ds-tailoring/ssg-fedora-ds.xml 54 | - /usr/share/xml/scap/ssg-fedora-ds-tailoring/tailoring-xccdf.xml 55 | - **xccdf-with-tailoring-1-1.noarch.rpm** 56 | - tailoring that modifies a plain XCCDF 57 | - customized profile ID: xccdf_org.ssgproject.content_profile_ospp_customized 58 | - files shipped in this RPM: 59 | - /usr/share/xml/scap/xccdf-with-tailoring/ssg-fedora-oval.xml 60 | - /usr/share/xml/scap/xccdf-with-tailoring/ssg-fedora-xccdf.xml 61 | - /usr/share/xml/scap/xccdf-with-tailoring/tailoring.xml 62 | 63 | ## ZIP files 64 | 65 | - **ds-with-tailoring.zip** 66 | - this zip archive contains SCAP source data stream and a tailoring file that modifies one of the profiles 67 | - customized profile ID xccdf_org.ssgproject.content_profile_ospp_customized 68 | - contents of the archive: 69 | - ssg-fedora-ds.xml 70 | - tailoring.xml 71 | - **separate-scap-files.zip** 72 | - contains SCAP content in form of separate components files (no data stream) 73 | - contents of the archive: 74 | - ssg-rhel8-cpe-dictionary.xml 75 | - ssg-rhel8-cpe-oval.xml 76 | - ssg-rhel8-ocil.xml 77 | - ssg-rhel8-oval.xml 78 | - ssg-rhel8-xccdf.xml 79 | - **single-ds.zip** 80 | - contains a single SCAP source data stream which is a common RHEL 8 SCAP content 81 | - contents of the archive: 82 | - some_rhel8_content.xml 83 | - **xccdf-with-tailoring.zip** 84 | - tailoring that modifies a plain XCCDF 85 | - customized profile ID: xccdf_org.ssgproject.content_profile_ospp_customized 86 | - contents of the archive: 87 | - ssg-fedora-oval.xml 88 | - ssg-fedora-xccdf.xml 89 | - tailoring.xml 90 | 91 | 92 | ## SCAP content files 93 | 94 | - **tailoring.xml** 95 | - tailoring file for `xccdf.xml` (see below) 96 | - customized profiles: 97 | - xccdf_com.example_profile_my_profile2_tailored 98 | - xccdf_com.example_profile_my_profile_tailored 99 | - **testing_ds.xml** 100 | - SCAP source data stream that contains 2 XCCDF benchmarks, great to test selection of a benchmark 101 | - **testing_xccdf.xml** 102 | - very simple XCCDF file with a single rule which uses SCE 103 | - no profiles 104 | - **xccdf.xml** 105 | - simple XCCDF with 2 profiles 106 | 107 | ## Kickstarts 108 | 109 | - **testing_ks.cfg** 110 | -------------------------------------------------------------------------------- /testing_files/basic_fedora_kickstart.cfg: -------------------------------------------------------------------------------- 1 | # values saving a lot of clicks in the GUI 2 | lang en_US.UTF-9 3 | keyboard --xlayouts=us --vckeymap=us 4 | timezone Europe/Prague 5 | rootpw aaaaa 6 | bootloader --location=mbr 7 | clearpart --initlabel --all 8 | autopart --type=plain 9 | graphical 10 | 11 | url --url=https://dl.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/ 12 | 13 | %packages 14 | vim 15 | %end 16 | 17 | %addon org_fedora_oscap 18 | content-type = scap-security-guide 19 | profile = xccdf_org.ssgproject.content_profile_standard 20 | %end 21 | -------------------------------------------------------------------------------- /testing_files/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f /root/must_exist.txt ]; then 4 | exit 0 5 | else 6 | exit 1 7 | fi 8 | -------------------------------------------------------------------------------- /testing_files/cpe-dict.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Applicable example platform 7 | oval:x:def:1 8 | 9 | 10 | -------------------------------------------------------------------------------- /testing_files/customized_stig-1-1.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/customized_stig-1-1.noarch.rpm -------------------------------------------------------------------------------- /testing_files/ds-with-tailoring.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/ds-with-tailoring.zip -------------------------------------------------------------------------------- /testing_files/run_oscap_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #oscap xccdf eval --profile xccdf_com.example_profile_my_profile --remediate --results=testing_results2.xml testing_ds.xml 4 | 5 | oscap xccdf eval --datastream-id=scap_org.open-scap_datastream_tst --xccdf-id=scap_org.open-scap_cref_first-xccdf.xml --profile xccdf_com.example_profile_my_profile --remediate --results=testing_results.xml testing_ds.xml 6 | -------------------------------------------------------------------------------- /testing_files/scap-mycheck-oval.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | vim, emacs 5 | 5.5 6 | 2010-08-30T12:00:00-04:00 7 | 8 | 9 | 10 | 11 | Ensure that /root/must_exist.txt file exists. 12 | 13 | 14 | Testing check. 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ^/root$ 30 | must_exist.txt 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /testing_files/scap-security-guide.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/scap-security-guide.noarch.rpm -------------------------------------------------------------------------------- /testing_files/separate-scap-files-1-1.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/separate-scap-files-1-1.noarch.rpm -------------------------------------------------------------------------------- /testing_files/separate-scap-files.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/separate-scap-files.zip -------------------------------------------------------------------------------- /testing_files/single-ds-1-1.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/single-ds-1-1.noarch.rpm -------------------------------------------------------------------------------- /testing_files/single-ds.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/single-ds.zip -------------------------------------------------------------------------------- /testing_files/ssg-fedora-ds-tailoring-1-1.noarch.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSCAP/oscap-anaconda-addon/463a3f27c0f29777bf67a4f644ce5e63b1e0a867/testing_files/ssg-fedora-ds-tailoring-1-1.noarch.rpm -------------------------------------------------------------------------------- /testing_files/tailoring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 6 | My testing profile2 tailored 7 | 8 | 9 | My testing profile tailored 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /testing_files/test_report_anaconda_fixes.xccdf.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | accepted 5 | 1.0 6 | 7 | 8 | Some arbitrary hardening profile for anaconda testing 9 | 11 | 12 | 13 | 14 | Ensure /tmp Located On Separate Partition 15 | CCE-14161-4 16 | 17 | 18 | part /tmp 19 | 20 | 21 | 22 | Add nodev Option to /tmp 23 | CCE-14412-1 24 | 25 | part /tmp --mountoptions=nodev 26 | 27 | 28 | 29 | grep -e '^[^#].*/tmp.*nodev' /etc/fstab 30 | if [ "$?" -ne 0 ]; then 31 | new_fstab=$(cat /etc/fstab | sed -e 's%^[^#]([^ ]+)\s+/tmp([^ ]+)\s+([^ ]+)\s+(\d)\s+(\d)%\1\t/tmp\2\t\3,nodev\t\4 \5' 32 | echo $new_fstab > /etc/fstab 33 | fi 34 | 35 | 36 | 37 | 38 | Minimal password length 39 | 8 40 | 14 41 | 18 42 | 43 | 44 | Set Password Minimum Length in login.defs 45 | 46 | 47 | passwd --minlen= 48 | 49 | 50 | 51 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /testing_files/testing_ds.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | incomplete 22 | 1.0 23 | 24 | My testing profile 25 | A profile for testing purposes. 26 | 28 | 30 | 36 | 76 | 77 | 78 | 79 | touch /root/must_exist.txt 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | vim, emacs 88 | 5.5 89 | 2010-08-30T12:00:00-04:00 90 | 91 | 92 | 93 | 94 | Ensure that /root/must_exist.txt file exists. 95 | 96 | 97 | Testing check. 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 108 | 109 | 110 | 111 | 112 | 113 | ^/root$ 114 | must_exist.txt 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /testing_files/testing_ks.cfg: -------------------------------------------------------------------------------- 1 | # this is a simple kickstart file for testing OSCAP addon's features 2 | 3 | # values saving a lot of clicks in the GUI 4 | lang en_US.UTF-8 5 | keyboard --xlayouts=us --vckeymap=us 6 | timezone Europe/Prague 7 | rootpw aaaaa 8 | bootloader --location=mbr 9 | clearpart --initlabel --all 10 | autopart --type=plain 11 | 12 | %packages 13 | vim 14 | %end 15 | 16 | %addon org_fedora_oscap 17 | content-type = datastream 18 | content-url = http://cobra02/ks/vp/testing_ds.xml 19 | profile = xccdf_com.example_profile_my_profile 20 | %end -------------------------------------------------------------------------------- /testing_files/testing_xccdf.xml: -------------------------------------------------------------------------------- 1 | 2 | draft 3 | 0.1 4 | 5 | Simple rule for testing purposes 6 | 8 | touch /root/must_exist.txt 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /testing_files/xccdf-1.1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | incomplete 4 | 1.0 5 | 6 | My testing profile 7 | A profile for testing purposes. 8 | 10 | 12 | 18 | 9 | 11 | 13 | 14 | 15 | My testing profile2 16 | Another profile for testing purposes. 17 | 19 | 20 | 21 | 22 | touch /root/must_exist.txt 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | part /tmp --mountoptions="nodev,noauto" 31 | 32 | 33 | 34 | 35 | passwd --minlen=10 36 | 37 | 38 | 39 | 40 | package --remove=telnet 41 | 42 | 43 | 44 | 45 | package --add=iptables 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.fedoraproject.org/fedora:rawhide 2 | 3 | RUN dnf update -y \ 4 | ; dnf install -y \ 5 | 'dnf-command(download)' \ 6 | make \ 7 | anaconda \ 8 | openscap \ 9 | openscap-utils \ 10 | openscap-python3 \ 11 | scap-security-guide \ 12 | python3-cpio \ 13 | python3-pycurl \ 14 | python3-pip \ 15 | ; dnf clean all 16 | 17 | RUN pip install \ 18 | pylint \ 19 | pytest \ 20 | mock 21 | 22 | RUN mkdir /oscap-anaconda-addon 23 | WORKDIR /oscap-anaconda-addon 24 | -------------------------------------------------------------------------------- /tests/data/file: -------------------------------------------------------------------------------- 1 | Test file to be hashed. 2 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2013 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | # Red Hat Author(s): Vratislav Podzimek 19 | # 20 | 21 | """Module with unit tests for the common.py module""" 22 | 23 | import os 24 | from unittest import mock 25 | import shutil 26 | 27 | import pytest 28 | import tempfile 29 | 30 | from org_fedora_oscap import common 31 | 32 | TESTING_FILES_PATH = os.path.join( 33 | os.path.dirname(__file__), os.path.pardir, "testing_files") 34 | 35 | @pytest.fixture() 36 | def mock_subprocess(): 37 | mock_subprocess = mock.Mock() 38 | mock_subprocess.Popen = mock.Mock() 39 | mock_popen = mock.Mock() 40 | mock_communicate = mock.Mock() 41 | 42 | mock_communicate.return_value = (b"", b"") 43 | 44 | mock_popen.communicate = mock_communicate 45 | mock_popen.returncode = 0 46 | 47 | mock_subprocess.Popen.return_value = mock_popen 48 | mock_subprocess.PIPE = mock.Mock() 49 | 50 | return mock_subprocess 51 | 52 | 53 | def mock_run_remediate(mock_subprocess, monkeypatch): 54 | mock_utils = mock.Mock() 55 | mock_utils.ensure_dir_exists = mock.Mock() 56 | 57 | common_module_symbols = common.__dict__ 58 | 59 | monkeypatch.setitem(common_module_symbols, "subprocess", mock_subprocess) 60 | monkeypatch.setitem(common_module_symbols, "utils", mock_utils) 61 | 62 | 63 | def _run_oscap(mock_subprocess, additional_args): 64 | expected_args = [ 65 | "oscap", "xccdf", "eval", "--remediate", 66 | "--results=%s" % common.RESULTS_PATH, 67 | "--report=%s" % common.REPORT_PATH, 68 | "--profile=myprofile", 69 | ] 70 | expected_args.extend(additional_args) 71 | 72 | kwargs = { 73 | "stdout": mock_subprocess.PIPE, 74 | "stderr": mock_subprocess.PIPE, 75 | } 76 | 77 | return expected_args, kwargs 78 | 79 | 80 | def test_oscap_works(): 81 | assert common.assert_scanner_works(chroot="/") 82 | with pytest.raises(common.OSCAPaddonError, match="No such file"): 83 | common.assert_scanner_works(chroot="/", executable="i_dont_exist") 84 | with pytest.raises(common.OSCAPaddonError, match="non-zero"): 85 | common.assert_scanner_works(chroot="/", executable="false") 86 | 87 | 88 | def test_run_oscap_remediate_profile_only(mock_subprocess, monkeypatch): 89 | return run_oscap_remediate_profile( 90 | mock_subprocess, monkeypatch, 91 | ["myprofile", "my_ds.xml"], 92 | ["my_ds.xml"]) 93 | 94 | 95 | def test_run_oscap_remediate_with_ds(mock_subprocess, monkeypatch): 96 | return run_oscap_remediate_profile( 97 | mock_subprocess, monkeypatch, 98 | ["myprofile", "my_ds.xml", "my_ds_id"], 99 | ["--datastream-id=my_ds_id", "my_ds.xml"]) 100 | 101 | 102 | def test_run_oscap_remediate_with_ds_xccdf(mock_subprocess, monkeypatch): 103 | return run_oscap_remediate_profile( 104 | mock_subprocess, monkeypatch, 105 | ["myprofile", "my_ds.xml", "my_ds_id", "my_xccdf_id"], 106 | ["--datastream-id=my_ds_id", "--xccdf-id=my_xccdf_id", "my_ds.xml"]) 107 | 108 | 109 | def run_oscap_remediate_profile( 110 | mock_subprocess, monkeypatch, 111 | anaconda_remediate_args, oscap_remediate_args): 112 | mock_run_remediate(mock_subprocess, monkeypatch) 113 | common.run_oscap_remediate(* anaconda_remediate_args) 114 | 115 | expected_args = [ 116 | "oscap", "xccdf", "eval", "--remediate", 117 | "--results=%s" % common.RESULTS_PATH, 118 | "--report=%s" % common.REPORT_PATH, 119 | "--profile=myprofile", 120 | ] 121 | expected_args.extend(oscap_remediate_args) 122 | 123 | kwargs = { 124 | "stdout": mock_subprocess.PIPE, 125 | "stderr": mock_subprocess.PIPE, 126 | } 127 | 128 | # it's impossible to check the preexec_func as it is an internal 129 | # function of the run_oscap_remediate function 130 | for arg in expected_args: 131 | assert arg in mock_subprocess.Popen.call_args[0][0] 132 | mock_subprocess.Popen.call_args[0][0].remove(arg) 133 | 134 | # nothing else should have been passed 135 | assert not mock_subprocess.Popen.call_args[0][0] 136 | 137 | for (key, val) in kwargs.items(): 138 | assert kwargs[key] == mock_subprocess.Popen.call_args[1].pop(key) 139 | 140 | # plus the preexec_fn kwarg should have been passed 141 | assert "preexec_fn" in mock_subprocess.Popen.call_args[1] 142 | 143 | 144 | def test_run_oscap_remediate_create_dir(mock_subprocess, monkeypatch): 145 | mock_run_remediate(mock_subprocess, monkeypatch) 146 | common.run_oscap_remediate("myprofile", "my_ds.xml") 147 | 148 | common.utils.ensure_dir_exists.assert_called_with( 149 | os.path.dirname(common.RESULTS_PATH)) 150 | 151 | 152 | def test_run_oscap_remediate_create_chroot_dir(mock_subprocess, monkeypatch): 153 | mock_run_remediate(mock_subprocess, monkeypatch) 154 | common.run_oscap_remediate("myprofile", "my_ds.xml", chroot="/mnt/test") 155 | 156 | chroot_dir = "/mnt/test" + os.path.dirname(common.RESULTS_PATH) 157 | common.utils.ensure_dir_exists.assert_called_with(chroot_dir) 158 | 159 | 160 | rpm_ssg_file_list = [ 161 | "/usr/share/doc/scap-security-guide/Contributors.md", 162 | "/usr/share/doc/scap-security-guide/LICENSE", 163 | "/usr/share/doc/scap-security-guide/README.md", 164 | "/usr/share/man/man8/scap-security-guide.8.gz", 165 | "/usr/share/scap-security-guide/ansible", 166 | "/usr/share/scap-security-guide/ansible/ssg-fedora-role-default.yml", 167 | "/usr/share/scap-security-guide/ansible/ssg-fedora-role-ospp.yml", 168 | "/usr/share/scap-security-guide/ansible/ssg-fedora-role-pci-dss.yml", 169 | "/usr/share/scap-security-guide/ansible/ssg-fedora-role-standard.yml", 170 | "/usr/share/scap-security-guide/bash", 171 | "/usr/share/scap-security-guide/bash/ssg-fedora-role-default.sh", 172 | "/usr/share/scap-security-guide/bash/ssg-fedora-role-ospp.sh", 173 | "/usr/share/scap-security-guide/bash/ssg-fedora-role-pci-dss.sh", 174 | "/usr/share/scap-security-guide/bash/ssg-fedora-role-standard.sh", 175 | "/usr/share/xml/scap/ssg/content", 176 | "/usr/share/xml/scap/ssg/content/ssg-fedora-cpe-dictionary.xml", 177 | "/usr/share/xml/scap/ssg/content/ssg-fedora-cpe-oval.xml", 178 | "/usr/share/xml/scap/ssg/content/ssg-fedora-ds.xml", 179 | "/usr/share/xml/scap/ssg/content/ssg-fedora-ocil.xml", 180 | "/usr/share/xml/scap/ssg/content/ssg-fedora-oval.xml", 181 | "/usr/share/xml/scap/ssg/content/ssg-fedora-xccdf.xml", 182 | ] 183 | 184 | 185 | def test_extract_ssg_rpm(): 186 | temp_path = tempfile.mkdtemp(prefix="rpm") 187 | 188 | extracted_files = common._extract_rpm( 189 | TESTING_FILES_PATH + "/scap-security-guide.noarch.rpm", 190 | temp_path) 191 | 192 | assert len(rpm_ssg_file_list) == len(extracted_files) 193 | for rpm_file in rpm_ssg_file_list: 194 | assert temp_path + rpm_file in extracted_files 195 | 196 | shutil.rmtree(temp_path) 197 | 198 | 199 | def test_extract_ssg_rpm_ensure_filepath_there(): 200 | temp_path = tempfile.mkdtemp(prefix="rpm") 201 | 202 | extracted_files = common._extract_rpm( 203 | TESTING_FILES_PATH + "/scap-security-guide.noarch.rpm", 204 | temp_path, 205 | ["/usr/share/xml/scap/ssg/content/ssg-fedora-ds.xml"]) 206 | 207 | assert len(rpm_ssg_file_list) == len(extracted_files) 208 | for rpm_file in rpm_ssg_file_list: 209 | assert temp_path + rpm_file in extracted_files 210 | 211 | shutil.rmtree(temp_path) 212 | 213 | 214 | def test_extract_ssg_rpm_ensure_filepath_not_there(): 215 | temp_path = tempfile.mkdtemp(prefix="rpm") 216 | 217 | with pytest.raises(common.ExtractionError) as excinfo: 218 | extracted_files = common._extract_rpm( 219 | TESTING_FILES_PATH + "/scap-security-guide.noarch.rpm", 220 | temp_path, 221 | ["/usr/share/xml/scap/ssg/content/ssg-fedora-content.xml"]) 222 | 223 | assert "File '/usr/share/xml/scap/ssg/content/ssg-fedora-content.xml' "\ 224 | "not found in the archive" in str(excinfo.value) 225 | 226 | shutil.rmtree(temp_path) 227 | 228 | 229 | rpm_tailoring_file_list = [ 230 | "/usr/share/xml/scap/ssg-fedora-ds-tailoring/ssg-fedora-ds.xml", 231 | "/usr/share/xml/scap/ssg-fedora-ds-tailoring/tailoring-xccdf.xml", 232 | ] 233 | 234 | 235 | def test_extract_tailoring_rpm(): 236 | temp_path = tempfile.mkdtemp(prefix="rpm") 237 | 238 | extracted_files = common._extract_rpm( 239 | TESTING_FILES_PATH + "/ssg-fedora-ds-tailoring-1-1.noarch.rpm", 240 | temp_path) 241 | 242 | assert len(rpm_tailoring_file_list) == len(extracted_files) 243 | for rpm_file in rpm_tailoring_file_list: 244 | assert temp_path + rpm_file in extracted_files 245 | 246 | shutil.rmtree(temp_path) 247 | 248 | 249 | def test_extract_tailoring_rpm_ensure_filepath_there(): 250 | temp_path = tempfile.mkdtemp(prefix="rpm") 251 | 252 | extracted_files = common._extract_rpm( 253 | TESTING_FILES_PATH + "/ssg-fedora-ds-tailoring-1-1.noarch.rpm", 254 | temp_path, 255 | ["/usr/share/xml/scap/ssg-fedora-ds-tailoring/ssg-fedora-ds.xml"]) 256 | 257 | assert len(rpm_tailoring_file_list) == len(extracted_files) 258 | for rpm_file in rpm_tailoring_file_list: 259 | assert temp_path + rpm_file in extracted_files 260 | 261 | shutil.rmtree(temp_path) 262 | 263 | 264 | def test_extract_tailoring_rpm_ensure_filename_there(): 265 | temp_path = tempfile.mkdtemp(prefix="rpm") 266 | 267 | with pytest.raises(common.ExtractionError) as excinfo: 268 | extracted_files = common._extract_rpm( 269 | TESTING_FILES_PATH + "/ssg-fedora-ds-tailoring-1-1.noarch.rpm", 270 | temp_path, 271 | ["ssg-fedora-ds.xml"]) 272 | 273 | assert "File 'ssg-fedora-ds.xml' not found in the archive" \ 274 | in str(excinfo.value) 275 | 276 | shutil.rmtree(temp_path) 277 | 278 | 279 | def test_firstboot_config(): 280 | config_args = dict( 281 | profile="@PROFILE@", 282 | ds_path="@DS_PATH@", 283 | results_path="@RES_PATH@", 284 | report_path="@REP_PATH", 285 | ds_id="@DS_ID@", 286 | xccdf_id="@XCCDF_ID@", 287 | tailoring_path="@TAIL_PATH@", 288 | ) 289 | config_string = common._create_firstboot_config_string(** config_args) 290 | for arg in config_args.values(): 291 | assert arg in config_string 292 | -------------------------------------------------------------------------------- /tests/test_content_handling.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | 4 | import pytest 5 | 6 | from org_fedora_oscap import content_handling as ch 7 | 8 | 9 | TESTING_FILES_PATH = os.path.join( 10 | os.path.dirname(__file__), os.path.pardir, "testing_files") 11 | DS_FILEPATH = os.path.join( 12 | TESTING_FILES_PATH, "testing_ds.xml") 13 | 14 | DS_IDS = "scap_org.open-scap_datastream_tst" 15 | CHK_FIRST_ID = "scap_org.open-scap_cref_first-xccdf.xml" 16 | CHK_SECOND_ID = "scap_org.open-scap_cref_second-xccdf.xml" 17 | 18 | PROFILE1_ID = "xccdf_com.example_profile_my_profile" 19 | PROFILE2_ID = "xccdf_com.example_profile_my_profile2" 20 | PROFILE3_ID = "xccdf_com.example_profile_my_profile3" 21 | 22 | 23 | def test_identify_files(): 24 | filenames = glob.glob(TESTING_FILES_PATH + "/*") 25 | identified = ch.identify_files(filenames) 26 | assert identified[DS_FILEPATH] == ch.CONTENT_TYPES["DATASTREAM"] 27 | assert identified[ 28 | os.path.join(TESTING_FILES_PATH, "scap-mycheck-oval.xml")] == ch.CONTENT_TYPES["OVAL"] 29 | assert identified[ 30 | os.path.join(TESTING_FILES_PATH, "tailoring.xml")] == ch.CONTENT_TYPES["TAILORING"] 31 | assert identified[ 32 | os.path.join(TESTING_FILES_PATH, "testing_xccdf.xml")] == ch.CONTENT_TYPES["XCCDF_CHECKLIST"] 33 | assert identified[ 34 | os.path.join(TESTING_FILES_PATH, "cpe-dict.xml")] == ch.CONTENT_TYPES["CPE_DICT"] 35 | -------------------------------------------------------------------------------- /tests/test_content_paths.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | import pytest 19 | 20 | from org_fedora_oscap.structures import PolicyData 21 | from org_fedora_oscap import common 22 | 23 | 24 | def test_datastream_content_paths(): 25 | data = PolicyData() 26 | data.content_type = "datastream" 27 | data.content_url = "https://example.com/hardening.xml" 28 | data.datastream_id = "id_datastream_1" 29 | data.xccdf_id = "id_xccdf_new" 30 | data.content_path = "/usr/share/oscap/testing_ds.xml" 31 | data.cpe_path = "/usr/share/oscap/cpe.xml" 32 | data.tailoring_path = "/usr/share/oscap/tailoring.xml" 33 | data.profile_id = "Web Server" 34 | 35 | assert common.get_content_name(data) == "hardening.xml" 36 | 37 | expected_path = "/tmp/openscap_data/hardening.xml" 38 | assert common.get_raw_preinst_content_path(data) == expected_path 39 | 40 | expected_path = "/tmp/openscap_data/hardening.xml" 41 | assert common.get_preinst_content_path(data) == expected_path 42 | 43 | expected_path = "/root/openscap_data/hardening.xml" 44 | assert common.get_postinst_content_path(data) == expected_path 45 | 46 | expected_path = "/tmp/openscap_data/usr/share/oscap/tailoring.xml" 47 | assert common.get_preinst_tailoring_path(data) == expected_path 48 | 49 | expected_path = "/root/openscap_data/usr/share/oscap/tailoring.xml" 50 | assert common.get_postinst_tailoring_path(data) == expected_path 51 | 52 | 53 | def test_archive_content_paths(): 54 | data = PolicyData() 55 | data.content_type = "archive" 56 | data.content_url = "http://example.com/oscap_content.tar" 57 | data.content_path = "oscap/xccdf.xml" 58 | data.profile_id = "Web Server" 59 | data.content_path = "oscap/xccdf.xml" 60 | data.tailoring_path = "oscap/tailoring.xml" 61 | 62 | assert common.get_content_name(data) == "oscap_content.tar" 63 | 64 | expected_path = "/tmp/openscap_data/oscap_content.tar" 65 | assert common.get_raw_preinst_content_path(data) == expected_path 66 | 67 | expected_path = "/tmp/openscap_data/oscap/xccdf.xml" 68 | assert common.get_preinst_content_path(data) == expected_path 69 | 70 | expected_path = "/root/openscap_data/oscap/xccdf.xml" 71 | assert common.get_postinst_content_path(data) == expected_path 72 | 73 | expected_path = "/tmp/openscap_data/oscap/tailoring.xml" 74 | assert common.get_preinst_tailoring_path(data) == expected_path 75 | 76 | expected_path = "/root/openscap_data/oscap/tailoring.xml" 77 | assert common.get_postinst_tailoring_path(data) == expected_path 78 | 79 | 80 | def test_rpm_content_paths(): 81 | data = PolicyData() 82 | data.content_type = "rpm" 83 | data.content_url = "http://example.com/oscap_content.rpm" 84 | data.profile_id = "Web Server" 85 | data.content_path = "/usr/share/oscap/xccdf.xml" 86 | data.tailoring_path = "/usr/share/oscap/tailoring.xml" 87 | 88 | assert common.get_content_name(data) == "oscap_content.rpm" 89 | 90 | expected_path = "/tmp/openscap_data/oscap_content.rpm" 91 | assert common.get_raw_preinst_content_path(data) == expected_path 92 | 93 | expected_path = "/tmp/openscap_data/usr/share/oscap/xccdf.xml" 94 | assert common.get_preinst_content_path(data) == expected_path 95 | 96 | expected_path = "/usr/share/oscap/xccdf.xml" 97 | assert common.get_postinst_content_path(data) == expected_path 98 | 99 | expected_path = "/tmp/openscap_data/usr/share/oscap/tailoring.xml" 100 | assert common.get_preinst_tailoring_path(data) == expected_path 101 | 102 | expected_path = "/usr/share/oscap/tailoring.xml" 103 | assert common.get_postinst_tailoring_path(data) == expected_path 104 | 105 | 106 | def test_scap_security_guide_paths(): 107 | data = PolicyData() 108 | data.content_type = "scap-security-guide" 109 | data.profile_id = "Web Server" 110 | data.content_path = "/usr/share/xml/scap/ssg/content.xml" 111 | 112 | expected_msg = "Using scap-security-guide, no single content file" 113 | with pytest.raises(ValueError, match=expected_msg): 114 | common.get_content_name(data) 115 | 116 | expected_path = None 117 | assert common.get_raw_preinst_content_path(data) == expected_path 118 | 119 | expected_path = "/usr/share/xml/scap/ssg/content.xml" 120 | assert common.get_preinst_content_path(data) == expected_path 121 | 122 | expected_path = "/usr/share/xml/scap/ssg/content.xml" 123 | assert common.get_postinst_content_path(data) == expected_path 124 | 125 | expected_path = "" 126 | assert common.get_preinst_tailoring_path(data) == expected_path 127 | 128 | expected_path = "" 129 | assert common.get_postinst_tailoring_path(data) == expected_path 130 | -------------------------------------------------------------------------------- /tests/test_data_fetch.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import filecmp 3 | import contextlib 4 | import pathlib 5 | import sys 6 | import subprocess 7 | import time 8 | 9 | import pytest 10 | 11 | from org_fedora_oscap import data_fetch 12 | 13 | 14 | PORT = 8001 15 | 16 | 17 | @contextlib.contextmanager 18 | def serve_directory_in_separate_process(port): 19 | args = [sys.executable, "-m", "http.server", str(port)] 20 | proc = subprocess.Popen( 21 | args, 22 | stdout=subprocess.DEVNULL, 23 | stderr=subprocess.DEVNULL) 24 | # give the server some time to start 25 | time.sleep(0.4) 26 | yield 27 | proc.terminate() 28 | proc.wait() 29 | 30 | 31 | def test_file_retreival(): 32 | filename_to_test = pathlib.Path(__file__) 33 | relative_filename_to_test = filename_to_test.relative_to(pathlib.Path.cwd()) 34 | 35 | temp_file = tempfile.NamedTemporaryFile() 36 | temp_filename = temp_file.name 37 | 38 | with serve_directory_in_separate_process(PORT): 39 | data_fetch._curl_fetch( 40 | "http://localhost:{}/{}".format(PORT, relative_filename_to_test), temp_filename) 41 | 42 | assert filecmp.cmp(relative_filename_to_test, temp_filename) 43 | 44 | 45 | def test_file_absent(): 46 | relative_filename_to_test = "i_am_not_here.file" 47 | 48 | with serve_directory_in_separate_process(PORT): 49 | with pytest.raises(data_fetch.FetchError) as exc: 50 | data_fetch._curl_fetch( 51 | "http://localhost:{}/{}".format(PORT, relative_filename_to_test), "/dev/null") 52 | assert "error code 404" in str(exc) 53 | 54 | 55 | def test_supported_url(): 56 | assert data_fetch.can_fetch_from("http://example.com") 57 | assert data_fetch.can_fetch_from("https://example.com") 58 | 59 | 60 | def test_unsupported_url(): 61 | assert not data_fetch.can_fetch_from("aaaaa") 62 | 63 | 64 | def test_fetch_local(tmp_path): 65 | source_path = pathlib.Path(__file__).absolute() 66 | dest_path = tmp_path / "dest" 67 | data_fetch.fetch_data("file://" + str(source_path), dest_path) 68 | with open(dest_path, "r") as copied_file: 69 | assert "This line is here and in the copied file as well" in copied_file.read() 70 | -------------------------------------------------------------------------------- /tests/test_installation.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | import logging 19 | import tempfile 20 | import pytest 21 | from unittest.mock import Mock 22 | 23 | from pyanaconda.modules.common.errors.installation import NonCriticalInstallationError 24 | 25 | from org_fedora_oscap.service import installation 26 | from org_fedora_oscap.structures import PolicyData 27 | 28 | # FIXME: Extend the tests to test all paths of the installation tasks. 29 | 30 | 31 | @pytest.fixture() 32 | def file_path(): 33 | with tempfile.NamedTemporaryFile() as f: 34 | yield f.name 35 | 36 | 37 | @pytest.fixture() 38 | def content_path(): 39 | with tempfile.TemporaryDirectory() as tmpdir: 40 | yield tmpdir 41 | 42 | 43 | @pytest.fixture() 44 | def tailoring_path(): 45 | with tempfile.NamedTemporaryFile() as f: 46 | yield f.name 47 | 48 | 49 | @pytest.fixture() 50 | def sysroot_path(): 51 | with tempfile.TemporaryDirectory() as tmpdir: 52 | yield tmpdir 53 | 54 | 55 | @pytest.fixture() 56 | def rule_evaluator(monkeypatch): 57 | mock = Mock(return_value=[]) 58 | monkeypatch.setattr("org_fedora_oscap.rule_handling.RuleData.eval_rules", mock) 59 | return mock 60 | 61 | 62 | @pytest.fixture() 63 | def mock_payload(monkeypatch): 64 | proxy = Mock() 65 | monkeypatch.setattr("org_fedora_oscap.common.get_payload_proxy", proxy) 66 | return proxy 67 | 68 | 69 | def test_fetch_content_task(caplog, file_path, content_path): 70 | data = PolicyData() 71 | task = installation.PrepareValidContent( 72 | policy_data=data, 73 | file_path=file_path, 74 | content_path=content_path, 75 | ) 76 | 77 | assert task.name == "Fetch the content, and optionally perform check or archive extraction" 78 | 79 | with pytest.raises(NonCriticalInstallationError, match="Couldn't find a valid datastream"): 80 | task.run() 81 | 82 | 83 | def test_evaluate_rules_task(rule_evaluator, content_path, tailoring_path, mock_payload): 84 | data = PolicyData() 85 | task = installation.EvaluateRulesTask( 86 | policy_data=data, 87 | content_path=content_path, 88 | tailoring_path=tailoring_path 89 | ) 90 | 91 | assert task.name == "Evaluate the rules" 92 | task.run() 93 | 94 | rule_evaluator.assert_called_once() 95 | 96 | 97 | def test_install_content_task(sysroot_path, file_path, content_path, tailoring_path): 98 | data = PolicyData() 99 | data.content_type = "scap-security-guide" 100 | 101 | task = installation.InstallContentTask( 102 | sysroot=sysroot_path, 103 | policy_data=data, 104 | file_path=file_path, 105 | content_path=content_path, 106 | tailoring_path=tailoring_path, 107 | target_directory="target_dir" 108 | ) 109 | 110 | assert task.name == "Install the content" 111 | task.run() 112 | 113 | 114 | def test_remediate_system_task(sysroot_path, content_path, tailoring_path): 115 | data = PolicyData() 116 | task = installation.RemediateSystemTask( 117 | sysroot=sysroot_path, 118 | policy_data=data, 119 | target_content_path=content_path, 120 | target_tailoring_path=tailoring_path 121 | ) 122 | 123 | assert task.name == "Remediate the system" 124 | with pytest.raises(installation.NonCriticalInstallationError, match="No such file"): 125 | task.run() 126 | -------------------------------------------------------------------------------- /tests/test_interface.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | import pytest 19 | 20 | from unittest.mock import Mock 21 | from dasbus.typing import get_native, get_variant, Str 22 | from pyanaconda.core.constants import REQUIREMENT_TYPE_PACKAGE 23 | from pyanaconda.modules.common.containers import TaskContainer 24 | from pyanaconda.modules.common.structures.requirement import Requirement 25 | 26 | from org_fedora_oscap.constants import OSCAP 27 | from org_fedora_oscap.service import installation 28 | from org_fedora_oscap.service.oscap import OSCAPService 29 | from org_fedora_oscap.service.oscap_interface import OSCAPInterface 30 | from org_fedora_oscap.structures import PolicyData 31 | 32 | 33 | class PropertiesChangedCallback(Mock): 34 | 35 | def __call__(self, interface, changed, invalid): 36 | return super().__call__(interface, get_native(changed), invalid) 37 | 38 | def assert_call(self, property_name, value): 39 | self.assert_called_once_with( 40 | OSCAP.interface_name, 41 | {property_name: get_native(value)}, 42 | [] 43 | ) 44 | 45 | 46 | @pytest.fixture() 47 | def service(): 48 | return OSCAPService() 49 | 50 | 51 | @pytest.fixture() 52 | def interface(service): 53 | return OSCAPInterface(service) 54 | 55 | 56 | @pytest.fixture 57 | def callback(interface): 58 | callback = PropertiesChangedCallback() 59 | interface.PropertiesChanged.connect(callback) 60 | return callback 61 | 62 | 63 | @pytest.fixture(autouse=True) 64 | def object_publisher(monkeypatch): 65 | # Mock any publishing of DBus objects. 66 | monkeypatch.setattr("pyanaconda.core.dbus.DBus.publish_object", Mock()) 67 | 68 | 69 | def test_policy_enabled(interface: OSCAPInterface, callback): 70 | policy_enabled = False 71 | interface.PolicyEnabled = policy_enabled 72 | 73 | callback.assert_call("PolicyEnabled", policy_enabled) 74 | assert interface.PolicyEnabled == policy_enabled 75 | 76 | 77 | def test_policy_data(interface: OSCAPInterface, callback): 78 | policy_structure = { 79 | "content-type": get_variant(Str, "datastream"), 80 | "content-url": get_variant(Str, "https://example.com/hardening.xml"), 81 | "datastream-id": get_variant(Str, "id_datastream_1"), 82 | "xccdf-id": get_variant(Str, "id_xccdf_new"), 83 | "profile-id": get_variant(Str, "Web Server"), 84 | "content-path": get_variant(Str, "/usr/share/oscap/testing_ds.xml"), 85 | "cpe-path": get_variant(Str, "/usr/share/oscap/cpe.xml"), 86 | "tailoring-path": get_variant(Str, "/usr/share/oscap/tailoring.xml"), 87 | "fingerprint": get_variant(Str, "240f2f18222faa98856c3b4fc50c4195"), 88 | "certificates": get_variant(Str, "/usr/share/oscap/cacert.pem"), 89 | "remediate": get_variant(Str, "both"), 90 | } 91 | interface.PolicyData = policy_structure 92 | 93 | callback.assert_call("PolicyData", policy_structure) 94 | assert interface.PolicyData == policy_structure 95 | 96 | 97 | def test_default_requirements(interface: OSCAPInterface): 98 | assert interface.CollectRequirements() == [] 99 | 100 | 101 | def test_no_requirements(service: OSCAPService, interface: OSCAPInterface): 102 | service.policy_enabled = True 103 | service.policy_data = PolicyData() 104 | assert interface.CollectRequirements() == [] 105 | 106 | 107 | def test_datastream_requirements(service: OSCAPService, interface: OSCAPInterface): 108 | data = PolicyData() 109 | data.content_type = "datastream" 110 | data.profile_id = "Web Server" 111 | 112 | service.policy_enabled = True 113 | service.policy_data = data 114 | 115 | requirements = Requirement.from_structure_list( 116 | interface.CollectRequirements() 117 | ) 118 | 119 | assert len(requirements) == 2 120 | assert requirements[0].type == REQUIREMENT_TYPE_PACKAGE 121 | assert requirements[0].name == "openscap" 122 | assert requirements[1].type == REQUIREMENT_TYPE_PACKAGE 123 | assert requirements[1].name == "openscap-scanner" 124 | 125 | 126 | def test_scap_security_guide_requirements(service: OSCAPService, interface: OSCAPInterface): 127 | data = PolicyData() 128 | data.content_type = "scap-security-guide" 129 | data.profile_id = "Web Server" 130 | 131 | service.policy_enabled = True 132 | service.policy_data = data 133 | 134 | requirements = Requirement.from_structure_list( 135 | interface.CollectRequirements() 136 | ) 137 | 138 | assert len(requirements) == 3 139 | assert requirements[0].type == REQUIREMENT_TYPE_PACKAGE 140 | assert requirements[0].name == "openscap" 141 | assert requirements[1].type == REQUIREMENT_TYPE_PACKAGE 142 | assert requirements[1].name == "openscap-scanner" 143 | assert requirements[2].type == REQUIREMENT_TYPE_PACKAGE 144 | assert requirements[2].name == "scap-security-guide" 145 | 146 | 147 | def test_configure_with_no_tasks(interface: OSCAPInterface): 148 | object_paths = interface.ConfigureWithTasks() 149 | assert len(object_paths) == 0 150 | 151 | 152 | def test_configure_with_tasks(service: OSCAPService, interface: OSCAPInterface): 153 | data = PolicyData() 154 | data.content_type = "scap-security-guide" 155 | data.profile_id = "Web Server" 156 | 157 | service.policy_enabled = True 158 | service.policy_data = data 159 | 160 | object_paths = interface.ConfigureWithTasks() 161 | assert len(object_paths) == 2 162 | 163 | tasks = TaskContainer.from_object_path_list(object_paths) 164 | assert isinstance(tasks[0], installation.PrepareValidContent) 165 | assert isinstance(tasks[1], installation.EvaluateRulesTask) 166 | 167 | 168 | def test_install_with_no_tasks(interface: OSCAPInterface): 169 | object_paths = interface.InstallWithTasks() 170 | assert len(object_paths) == 0 171 | 172 | 173 | def test_install_with_tasks(service: OSCAPService, interface: OSCAPInterface): 174 | data = PolicyData() 175 | data.content_type = "scap-security-guide" 176 | data.profile_id = "Web Server" 177 | data.remediate = "both" 178 | 179 | service.policy_enabled = True 180 | service.policy_data = data 181 | 182 | object_paths = interface.InstallWithTasks() 183 | assert len(object_paths) == 3 184 | 185 | tasks = TaskContainer.from_object_path_list(object_paths) 186 | assert isinstance(tasks[0], installation.InstallContentTask) 187 | assert isinstance(tasks[1], installation.RemediateSystemTask) 188 | assert isinstance(tasks[2], installation.ScheduleFirstbootRemediationTask) 189 | 190 | 191 | def test_cancel_tasks(service: OSCAPService): 192 | data = PolicyData() 193 | data.content_type = "scap-security-guide" 194 | data.profile_id = "Web Server" 195 | 196 | service.policy_enabled = True 197 | service.policy_data = data 198 | 199 | # Collect all tasks. 200 | tasks = service.configure_with_tasks() + service.install_with_tasks() 201 | 202 | # No task is canceled by default. 203 | for task in tasks: 204 | assert task.check_cancel() is False 205 | 206 | callback = Mock() 207 | service.installation_canceled.connect(callback) 208 | 209 | # The first task should fail with the given data. 210 | with pytest.raises(Exception): 211 | tasks[0].run_with_signals() 212 | 213 | # That should cancel all tasks. 214 | callback.assert_called_once() 215 | 216 | for task in tasks: 217 | assert task.check_cancel() is True 218 | -------------------------------------------------------------------------------- /tests/test_kickstart.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2021 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | import pytest 19 | from textwrap import dedent 20 | from unittest.mock import Mock 21 | from org_fedora_oscap.service.oscap import OSCAPService 22 | 23 | 24 | ADDON_NAME = "org_fedora_oscap" 25 | 26 | 27 | @pytest.fixture() 28 | def service(): 29 | return OSCAPService() 30 | 31 | 32 | @pytest.fixture() 33 | def mock_ssg_available(monkeypatch): 34 | mocked_function = Mock(return_value=True) 35 | monkeypatch.setattr("org_fedora_oscap.common.ssg_available", mocked_function) 36 | return mocked_function 37 | 38 | 39 | def check_ks_input(ks_service, ks_in, errors=None, warnings=None): 40 | """Read a provided kickstart string. 41 | 42 | :param ks_service: the kickstart service 43 | :param ks_in: a kickstart string 44 | :param errors: a list of expected errors 45 | :param warnings: a list of expected warning 46 | """ 47 | errors = errors or [] 48 | warnings = warnings or [] 49 | report = ks_service.read_kickstart(ks_in) 50 | 51 | for index, error in enumerate(report.error_messages): 52 | assert errors[index] in error.message 53 | 54 | for index, warning in enumerate(report.warning_messages): 55 | assert warnings[index] in warning.message 56 | 57 | 58 | def check_ks_output(ks_service, ks_out): 59 | """Generate a new kickstart string. 60 | 61 | :param ks_service: a kickstart service 62 | :param ks_out: an expected kickstart string 63 | """ 64 | output = ks_service.generate_kickstart() 65 | assert output.strip() == dedent(ks_out).strip() 66 | 67 | 68 | def test_default(service): 69 | check_ks_output(service, "") 70 | 71 | 72 | def test_data(service): 73 | ks_in = f""" 74 | %addon {ADDON_NAME} 75 | content-type = datastream 76 | content-url = "https://example.com/hardening.xml" 77 | %end 78 | """ 79 | check_ks_input(service, ks_in) 80 | 81 | assert service.policy_data.content_type == "datastream" 82 | assert service.policy_data.content_url == "https://example.com/hardening.xml" 83 | 84 | 85 | def test_datastream(service): 86 | ks_in = f""" 87 | %addon {ADDON_NAME} 88 | content-type = datastream 89 | content-url = "https://example.com/hardening.xml" 90 | datastream-id = id_datastream_1 91 | xccdf-id = id_xccdf_new 92 | content-path = /usr/share/oscap/testing_ds.xml 93 | cpe-path = /usr/share/oscap/cpe.xml 94 | tailoring-path = /usr/share/oscap/tailoring.xml 95 | profile = "Web Server" 96 | %end 97 | """ 98 | check_ks_input(service, ks_in) 99 | 100 | ks_out = f""" 101 | %addon {ADDON_NAME} 102 | content-type = datastream 103 | content-url = https://example.com/hardening.xml 104 | datastream-id = id_datastream_1 105 | xccdf-id = id_xccdf_new 106 | content-path = /usr/share/oscap/testing_ds.xml 107 | cpe-path = /usr/share/oscap/cpe.xml 108 | tailoring-path = /usr/share/oscap/tailoring.xml 109 | profile = Web Server 110 | %end 111 | """ 112 | check_ks_output(service, ks_out) 113 | 114 | 115 | def test_no_content_type(service): 116 | ks_in = f""" 117 | %addon {ADDON_NAME} 118 | content-url = http://example.com/test_ds.xml 119 | profile = Web Server 120 | %end 121 | """ 122 | check_ks_input(service, ks_in, errors=[ 123 | f"content-type missing for the {ADDON_NAME} addon" 124 | ]) 125 | 126 | 127 | def test_no_content_url(service): 128 | ks_in = f""" 129 | %addon {ADDON_NAME} 130 | content-type = datastream 131 | profile = Web Server 132 | %end 133 | """ 134 | check_ks_input(service, ks_in, errors=[ 135 | f"content-url missing for the {ADDON_NAME} addon" 136 | ]) 137 | 138 | 139 | def test_no_profile(service): 140 | ks_in = f""" 141 | %addon {ADDON_NAME} 142 | content-url = http://example.com/test_ds.xml 143 | content-type = datastream 144 | %end 145 | """ 146 | check_ks_input(service, ks_in) 147 | 148 | ks_out = f""" 149 | %addon {ADDON_NAME} 150 | content-type = datastream 151 | content-url = http://example.com/test_ds.xml 152 | profile = default 153 | %end 154 | """ 155 | check_ks_output(service, ks_out) 156 | 157 | assert service.policy_data.profile_id == "default" 158 | 159 | 160 | def test_rpm(service): 161 | ks_in = f""" 162 | %addon {ADDON_NAME} 163 | content-url = http://example.com/oscap_content.rpm 164 | content-type = RPM 165 | profile = Web Server 166 | xccdf-path = /usr/share/oscap/xccdf.xml 167 | %end 168 | """ 169 | check_ks_input(service, ks_in) 170 | 171 | ks_out = f""" 172 | %addon {ADDON_NAME} 173 | content-type = rpm 174 | content-url = http://example.com/oscap_content.rpm 175 | content-path = /usr/share/oscap/xccdf.xml 176 | profile = Web Server 177 | %end 178 | """ 179 | check_ks_output(service, ks_out) 180 | 181 | 182 | def test_rpm_without_path(service): 183 | ks_in = f""" 184 | %addon {ADDON_NAME} 185 | content-url = http://example.com/oscap_content.rpm 186 | content-type = RPM 187 | profile = Web Server 188 | %end 189 | """ 190 | check_ks_input(service, ks_in, errors=[ 191 | "Path to the XCCDF file has to be given if content in RPM or archive is used" 192 | ]) 193 | 194 | 195 | def test_rpm_with_wrong_suffix(service): 196 | ks_in = f""" 197 | %addon {ADDON_NAME} 198 | content-url = http://example.com/oscap_content.xml 199 | content-type = RPM 200 | profile = Web Server 201 | xccdf-path = /usr/share/oscap/xccdf.xml 202 | %end 203 | """ 204 | check_ks_input(service, ks_in, errors=[ 205 | "Content type set to RPM, but the content URL doesn't end with '.rpm'" 206 | ]) 207 | 208 | 209 | def test_archive(service): 210 | ks_in = f""" 211 | %addon {ADDON_NAME} 212 | content-url = http://example.com/oscap_content.tar 213 | content-type = archive 214 | profile = Web Server 215 | xccdf-path = oscap/xccdf.xml 216 | %end 217 | """ 218 | check_ks_input(service, ks_in) 219 | 220 | ks_out = f""" 221 | %addon {ADDON_NAME} 222 | content-type = archive 223 | content-url = http://example.com/oscap_content.tar 224 | content-path = oscap/xccdf.xml 225 | profile = Web Server 226 | %end 227 | """ 228 | check_ks_output(service, ks_out) 229 | 230 | 231 | def test_archive_without_path(service): 232 | ks_in = f""" 233 | %addon {ADDON_NAME} 234 | content-url = http://example.com/oscap_content.tar 235 | content-type = archive 236 | profile = Web Server 237 | %end 238 | """ 239 | check_ks_input(service, ks_in, errors=[ 240 | "Path to the XCCDF file has to be given if content in RPM or archive is used" 241 | ]) 242 | 243 | 244 | def test_org_fedora_oscap(service): 245 | ks_in = """ 246 | %addon org_fedora_oscap 247 | content-type = datastream 248 | content-url = "https://example.com/hardening.xml" 249 | %end 250 | """ 251 | check_ks_input(service, ks_in, warnings=[ 252 | "org_fedora_oscap" 253 | ]) 254 | 255 | 256 | def test_section_confusion(service): 257 | ks_in = """ 258 | %addon org_fedora_oscap 259 | content-type = datastream 260 | content-url = "https://example.com/hardening.xml" 261 | %end 262 | 263 | %addon com_redhat_oscap 264 | content-type = datastream 265 | content-url = "https://example.com/hardening.xml" 266 | %end 267 | """ 268 | check_ks_input(service, ks_in, errors=[ 269 | "You have used more than one oscap addon sections in the kickstart." 270 | ]) 271 | 272 | 273 | def test_scap_security_guide(service, mock_ssg_available): 274 | ks_in = f""" 275 | %addon {ADDON_NAME} 276 | content-type = scap-security-guide 277 | profile = Web Server 278 | %end 279 | """ 280 | 281 | mock_ssg_available.return_value = False 282 | check_ks_input(service, ks_in, errors=[ 283 | "SCAP Security Guide not found on the system" 284 | ]) 285 | 286 | ks_out = f""" 287 | %addon {ADDON_NAME} 288 | content-type = scap-security-guide 289 | profile = Web Server 290 | %end 291 | """ 292 | 293 | mock_ssg_available.return_value = True 294 | check_ks_input(service, ks_in, ks_out) 295 | 296 | 297 | def test_fingerprints(service): 298 | ks_template = f""" 299 | %addon {ADDON_NAME} 300 | content-url = http://example.com/test_ds.xml 301 | content-type = datastream 302 | fingerprint = {{}} 303 | %end 304 | """ 305 | 306 | # invalid character 307 | ks_in = ks_template.format("a" * 31 + "?") 308 | check_ks_input(service, ks_in, errors=[ 309 | "Unsupported or invalid fingerprint" 310 | ]) 311 | 312 | # invalid lengths (odd and even) 313 | for repetitions in (31, 41, 54, 66, 98, 124): 314 | ks_in = ks_template.format("a" * repetitions) 315 | check_ks_input(service, ks_in, errors=[ 316 | "Unsupported fingerprint" 317 | ]) 318 | 319 | # valid values 320 | for repetitions in (32, 40, 56, 64, 96, 128): 321 | ks_in = ks_template.format("a" * repetitions) 322 | check_ks_input(service, ks_in) 323 | -------------------------------------------------------------------------------- /tests/test_scap_content_handler.py: -------------------------------------------------------------------------------- 1 | from org_fedora_oscap.scap_content_handler import SCAPContentHandler 2 | from org_fedora_oscap.scap_content_handler import SCAPContentHandlerError 3 | from org_fedora_oscap.scap_content_handler import ProfileInfo 4 | import os 5 | import pytest 6 | 7 | TESTING_FILES_PATH = os.path.join( 8 | os.path.dirname(__file__), os.path.pardir, "testing_files") 9 | DS_FILEPATH = os.path.join(TESTING_FILES_PATH, "testing_ds.xml") 10 | XCCDF_FILEPATH = os.path.join(TESTING_FILES_PATH, "xccdf.xml") 11 | TAILORING_FILEPATH = os.path.join(TESTING_FILES_PATH, "tailoring.xml") 12 | OVAL_FILEPATH = os.path.join(TESTING_FILES_PATH, "scap-mycheck-oval.xml") 13 | 14 | DS_IDS = "scap_org.open-scap_datastream_tst" 15 | CHK_FIRST_ID = "scap_org.open-scap_cref_first-xccdf.xml" 16 | CHK_SECOND_ID = "scap_org.open-scap_cref_second-xccdf.xml" 17 | 18 | 19 | def test_init_invalid_file_path(): 20 | with pytest.raises(FileNotFoundError) as excinfo: 21 | SCAPContentHandler("blbl") 22 | assert "No such file or directory: 'blbl'" in str(excinfo.value) 23 | 24 | 25 | def test_init_sds(): 26 | ch = SCAPContentHandler(DS_FILEPATH) 27 | assert ch.scap_type == "SCAP_SOURCE_DATA_STREAM" 28 | 29 | 30 | def test_init_xccdf(): 31 | ch = SCAPContentHandler(XCCDF_FILEPATH) 32 | assert ch.scap_type == "XCCDF" 33 | 34 | 35 | def test_init_tailoring_of_sds(): 36 | ch = SCAPContentHandler(TAILORING_FILEPATH) 37 | assert ch.scap_type == "TAILORING" 38 | 39 | 40 | def test_init_tailoring_of_xccdf(): 41 | ch = SCAPContentHandler(TAILORING_FILEPATH) 42 | assert ch.scap_type == "TAILORING" 43 | 44 | 45 | def test_init_unsupported_scap_content_type(): 46 | # the class SCAPContentHandler shouldn't support OVAL files 47 | with pytest.raises(SCAPContentHandlerError) as excinfo: 48 | SCAPContentHandler(OVAL_FILEPATH) 49 | assert "Unsupported SCAP content type" in str(excinfo.value) 50 | 51 | 52 | def test_xccdf(): 53 | ch = SCAPContentHandler(XCCDF_FILEPATH) 54 | 55 | checklists = ch.get_data_streams_checklists() 56 | assert checklists is None 57 | 58 | profiles = ch.get_profiles() 59 | assert len(profiles) == 2 60 | pinfo1 = ProfileInfo( 61 | id="xccdf_com.example_profile_my_profile", 62 | title="My testing profile", 63 | description="A profile for testing purposes.") 64 | assert pinfo1 in profiles 65 | pinfo2 = ProfileInfo( 66 | id="xccdf_com.example_profile_my_profile2", 67 | title="My testing profile2", 68 | description="Another profile for testing purposes.") 69 | assert pinfo2 in profiles 70 | 71 | def test_xccdf_1_1(): 72 | file_path = os.path.join(TESTING_FILES_PATH, "xccdf-1.1.xml") 73 | ch = SCAPContentHandler(file_path) 74 | 75 | checklists = ch.get_data_streams_checklists() 76 | assert checklists is None 77 | 78 | profiles = ch.get_profiles() 79 | assert len(profiles) == 2 80 | pinfo1 = ProfileInfo( 81 | id="xccdf_com.example_profile_my_profile", 82 | title="My testing profile", 83 | description="A profile for testing purposes.") 84 | assert pinfo1 in profiles 85 | pinfo2 = ProfileInfo( 86 | id="xccdf_com.example_profile_my_profile2", 87 | title="My testing profile2", 88 | description="Another profile for testing purposes.") 89 | assert pinfo2 in profiles 90 | 91 | 92 | def test_xccdf_get_profiles_fails(): 93 | ch = SCAPContentHandler(XCCDF_FILEPATH) 94 | with pytest.raises(SCAPContentHandlerError) as excinfo: 95 | ch.select_checklist("", "") 96 | profiles = ch.get_profiles() 97 | assert "For XCCDF documents, the data_stream_id and " \ 98 | "checklist_id must be both None." in str(excinfo.value) 99 | 100 | 101 | def test_sds(): 102 | ch = SCAPContentHandler(DS_FILEPATH) 103 | checklists = ch.get_data_streams_checklists() 104 | assert checklists == {DS_IDS: [CHK_FIRST_ID, CHK_SECOND_ID]} 105 | 106 | ch.select_checklist(DS_IDS, CHK_FIRST_ID) 107 | profiles = ch.get_profiles() 108 | assert len(profiles) == 2 109 | pinfo1 = ProfileInfo( 110 | id="xccdf_com.example_profile_my_profile", 111 | title="My testing profile", 112 | description="A profile for testing purposes.") 113 | assert pinfo1 in profiles 114 | pinfo2 = ProfileInfo( 115 | id="xccdf_com.example_profile_my_profile2", 116 | title="My testing profile2", 117 | description="Another profile for testing purposes.") 118 | assert pinfo2 in profiles 119 | 120 | ch.select_checklist(DS_IDS, CHK_SECOND_ID) 121 | profiles2 = ch.get_profiles() 122 | assert len(profiles2) == 1 123 | pinfo3 = ProfileInfo( 124 | id="xccdf_com.example_profile_my_profile3", 125 | title="My testing profile3", 126 | description="Yet another profile for testing purposes.") 127 | 128 | 129 | def test_sds_get_profiles_fails(): 130 | ch = SCAPContentHandler(DS_FILEPATH) 131 | 132 | with pytest.raises(SCAPContentHandlerError) as excinfo: 133 | profiles = ch.get_profiles() 134 | assert "For SCAP source data streams, data_stream_id and " \ 135 | "checklist_id must be both different than None" in str(excinfo.value) 136 | 137 | with pytest.raises(SCAPContentHandlerError) as excinfo: 138 | ch.select_checklist(DS_IDS, checklist_id=None) 139 | profiles = ch.get_profiles() 140 | assert "For SCAP source data streams, data_stream_id and " \ 141 | "checklist_id must be both different than None" in str(excinfo.value) 142 | 143 | with pytest.raises(SCAPContentHandlerError) as excinfo: 144 | wrong_cref = "scap_org.open-scap_cref_seventh-xccdf.xml" 145 | ch.select_checklist(DS_IDS, wrong_cref) 146 | profiles = ch.get_profiles() 147 | assert f"Can't find ds:component-ref with id='{wrong_cref}' in " \ 148 | f"ds:datastream with id='{DS_IDS}'" in str(excinfo.value) 149 | 150 | 151 | def test_tailoring(): 152 | ch = SCAPContentHandler(DS_FILEPATH, TAILORING_FILEPATH) 153 | checklists = ch.get_data_streams_checklists() 154 | assert checklists == {DS_IDS: [CHK_FIRST_ID, CHK_SECOND_ID]} 155 | ch.select_checklist(DS_IDS, CHK_FIRST_ID) 156 | profiles = ch.get_profiles() 157 | assert len(profiles) == 4 158 | pinfo1 = ProfileInfo( 159 | id="xccdf_com.example_profile_my_profile_tailored", 160 | title="My testing profile tailored", 161 | description="") 162 | assert pinfo1 in profiles 163 | pinfo2 = ProfileInfo( 164 | id="xccdf_com.example_profile_my_profile2_tailored", 165 | title="My testing profile2 tailored", 166 | description="") 167 | assert pinfo2 in profiles 168 | # it should also include the profiles of the original benchmark 169 | pinfo3 = ProfileInfo( 170 | id="xccdf_com.example_profile_my_profile", 171 | title="My testing profile", 172 | description="A profile for testing purposes.") 173 | assert pinfo3 in profiles 174 | pinfo4 = ProfileInfo( 175 | id="xccdf_com.example_profile_my_profile2", 176 | title="My testing profile2", 177 | description="Another profile for testing purposes.") 178 | assert pinfo4 in profiles 179 | 180 | 181 | def test_default_profile(): 182 | xccdf_filepath = os.path.join(TESTING_FILES_PATH, "testing_xccdf.xml") 183 | ch = SCAPContentHandler(xccdf_filepath) 184 | checklists = ch.get_data_streams_checklists() 185 | assert checklists is None 186 | profiles = ch.get_profiles() 187 | assert len(profiles) == 1 188 | pinfo1 = ProfileInfo( 189 | id="default", 190 | title="Default", 191 | description="The implicit XCCDF profile. Usually, the default profile " 192 | "contains no rules.") 193 | assert pinfo1 in profiles 194 | -------------------------------------------------------------------------------- /tests/test_service_kickstart.py: -------------------------------------------------------------------------------- 1 | """Module with tests for the ks/oscap.py module.""" 2 | 3 | import os 4 | 5 | from pykickstart.errors import KickstartValueError 6 | import pytest 7 | 8 | try: 9 | from org_fedora_oscap.service.kickstart import OSCAPKickstartData 10 | from org_fedora_oscap import common 11 | except ImportError as exc: 12 | pytestmark = pytest.mark.skip( 13 | "Unable to import modules, possibly due to bad version of Anaconda: {error}" 14 | .format(error=str(exc))) 15 | 16 | 17 | @pytest.fixture() 18 | def blank_oscap_data(): 19 | return OSCAPKickstartData() 20 | 21 | 22 | @pytest.fixture() 23 | def filled_oscap_data(blank_oscap_data): 24 | oscap_data = blank_oscap_data 25 | for line in [ 26 | "content-type = datastream\n", 27 | "content-url = \"https://example.com/hardening.xml\"\n", 28 | "datastream-id = id_datastream_1\n", 29 | "xccdf-id = id_xccdf_new\n", 30 | "content-path = /usr/share/oscap/testing_ds.xml", 31 | "cpe-path = /usr/share/oscap/cpe.xml", 32 | "tailoring-path = /usr/share/oscap/tailoring.xml", 33 | "profile = \"Web Server\"\n", 34 | ]: 35 | oscap_data.handle_line(line) 36 | return oscap_data 37 | 38 | 39 | def test_parsing(filled_oscap_data): 40 | data = filled_oscap_data.policy_data 41 | assert data.content_type == "datastream" 42 | assert data.content_url == "https://example.com/hardening.xml" 43 | assert data.datastream_id == "id_datastream_1" 44 | assert data.xccdf_id == "id_xccdf_new" 45 | assert data.content_path == "/usr/share/oscap/testing_ds.xml" 46 | assert data.cpe_path == "/usr/share/oscap/cpe.xml" 47 | assert data.profile_id == "Web Server" 48 | assert data.tailoring_path == "/usr/share/oscap/tailoring.xml" 49 | 50 | 51 | def test_properties(filled_oscap_data): 52 | data = filled_oscap_data 53 | assert (data.preinst_content_path 54 | == common.INSTALLATION_CONTENT_DIR + data.content_name) 55 | assert (data.postinst_content_path 56 | == common.TARGET_CONTENT_DIR + data.content_name) 57 | assert (data.raw_preinst_content_path 58 | == common.INSTALLATION_CONTENT_DIR + data.content_name) 59 | assert (data.preinst_tailoring_path 60 | == os.path.normpath(common.INSTALLATION_CONTENT_DIR + data.policy_data.tailoring_path)) 61 | assert (data.postinst_tailoring_path 62 | == os.path.normpath(common.TARGET_CONTENT_DIR + data.policy_data.tailoring_path)) 63 | 64 | 65 | def test_str(filled_oscap_data): 66 | str_ret = str(filled_oscap_data) 67 | assert (str_ret == 68 | "%addon org_fedora_oscap\n" 69 | " content-type = datastream\n" 70 | " content-url = https://example.com/hardening.xml\n" 71 | " datastream-id = id_datastream_1\n" 72 | " xccdf-id = id_xccdf_new\n" 73 | " content-path = /usr/share/oscap/testing_ds.xml\n" 74 | " cpe-path = /usr/share/oscap/cpe.xml\n" 75 | " tailoring-path = /usr/share/oscap/tailoring.xml\n" 76 | " profile = Web Server\n" 77 | "%end\n\n" 78 | ) 79 | 80 | 81 | def test_str_parse(filled_oscap_data): 82 | our_oscap_data = OSCAPKickstartData() 83 | 84 | str_ret = str(filled_oscap_data) 85 | for line in str_ret.splitlines()[1:-1]: 86 | if "%end" not in line: 87 | our_oscap_data.handle_line(line) 88 | 89 | our_str_ret = str(our_oscap_data) 90 | assert str_ret == our_str_ret 91 | 92 | 93 | def test_nothing_given(blank_oscap_data): 94 | with pytest.raises(KickstartValueError): 95 | blank_oscap_data.handle_end() 96 | 97 | 98 | def test_no_content_type(blank_oscap_data): 99 | for line in ["content-url = http://example.com/test_ds.xml", 100 | "profile = Web Server", 101 | ]: 102 | blank_oscap_data.handle_line(line) 103 | 104 | with pytest.raises(KickstartValueError): 105 | blank_oscap_data.handle_end() 106 | 107 | 108 | def test_no_content_url(blank_oscap_data): 109 | for line in ["content-type = datastream", 110 | "profile = Web Server", 111 | ]: 112 | blank_oscap_data.handle_line(line) 113 | 114 | with pytest.raises(KickstartValueError): 115 | blank_oscap_data.handle_end() 116 | 117 | 118 | def test_no_profile(blank_oscap_data): 119 | for line in ["content-url = http://example.com/test_ds.xml", 120 | "content-type = datastream", 121 | ]: 122 | blank_oscap_data.handle_line(line) 123 | 124 | blank_oscap_data.handle_end() 125 | assert blank_oscap_data.policy_data.profile_id == "default" 126 | 127 | 128 | def test_rpm_without_path(blank_oscap_data): 129 | for line in ["content-url = http://example.com/oscap_content.rpm", 130 | "content-type = RPM", 131 | "profile = Web Server", 132 | ]: 133 | blank_oscap_data.handle_line(line) 134 | 135 | with pytest.raises(KickstartValueError): 136 | blank_oscap_data.handle_end() 137 | 138 | 139 | def test_rpm_with_wrong_suffix(blank_oscap_data): 140 | for line in ["content-url = http://example.com/oscap_content.xml", 141 | "content-type = RPM", 142 | "profile = Web Server", 143 | ]: 144 | blank_oscap_data.handle_line(line) 145 | 146 | with pytest.raises(KickstartValueError): 147 | blank_oscap_data.handle_end() 148 | 149 | 150 | def test_archive_without_path(blank_oscap_data): 151 | for line in ["content-url = http://example.com/oscap_content.tar", 152 | "content-type = archive", 153 | "profile = Web Server", 154 | ]: 155 | blank_oscap_data.handle_line(line) 156 | 157 | with pytest.raises(KickstartValueError): 158 | blank_oscap_data.handle_end() 159 | 160 | 161 | def test_unsupported_archive_type(blank_oscap_data): 162 | for line in ["content-url = http://example.com/oscap_content.tbz", 163 | "content-type = archive", 164 | "profile = Web Server", 165 | "xccdf-path = xccdf.xml" 166 | ]: 167 | blank_oscap_data.handle_line(line) 168 | 169 | with pytest.raises(KickstartValueError): 170 | blank_oscap_data.handle_end() 171 | 172 | 173 | def test_enough_for_ds(blank_oscap_data): 174 | for line in ["content-url = http://example.com/test_ds.xml", 175 | "content-type = datastream", 176 | "profile = Web Server", 177 | ]: 178 | blank_oscap_data.handle_line(line) 179 | 180 | blank_oscap_data.handle_end() 181 | 182 | 183 | def test_enough_for_rpm(blank_oscap_data): 184 | for line in ["content-url = http://example.com/oscap_content.rpm", 185 | "content-type = RPM", 186 | "profile = Web Server", 187 | "xccdf-path = /usr/share/oscap/xccdf.xml" 188 | ]: 189 | blank_oscap_data.handle_line(line) 190 | 191 | blank_oscap_data.handle_end() 192 | 193 | 194 | def test_enough_for_archive(blank_oscap_data): 195 | for line in ["content-url = http://example.com/oscap_content.tar", 196 | "content-type = archive", 197 | "profile = Web Server", 198 | "xccdf-path = /usr/share/oscap/xccdf.xml" 199 | ]: 200 | blank_oscap_data.handle_line(line) 201 | 202 | blank_oscap_data.handle_end() 203 | 204 | 205 | def test_archive_preinst_content_path(blank_oscap_data): 206 | for line in ["content-url = http://example.com/oscap_content.tar", 207 | "content-type = archive", 208 | "profile = Web Server", 209 | "xccdf-path = oscap/xccdf.xml" 210 | ]: 211 | blank_oscap_data.handle_line(line) 212 | 213 | blank_oscap_data.handle_end() 214 | 215 | # content_name should be the archive's name 216 | assert blank_oscap_data.content_name == "oscap_content.tar" 217 | 218 | # content path should end with the xccdf path 219 | assert blank_oscap_data.preinst_content_path.endswith("oscap/xccdf.xml") 220 | 221 | 222 | def test_ds_preinst_content_path(blank_oscap_data): 223 | for line in ["content-url = http://example.com/scap_content.xml", 224 | "content-type = datastream", 225 | "profile = Web Server", 226 | ]: 227 | blank_oscap_data.handle_line(line) 228 | 229 | blank_oscap_data.handle_end() 230 | 231 | # both content_name and content path should point to the data stream 232 | # XML 233 | assert blank_oscap_data.content_name == "scap_content.xml" 234 | assert blank_oscap_data.preinst_content_path.endswith("scap_content.xml") 235 | 236 | 237 | def test_archive_raw_content_paths(blank_oscap_data): 238 | for line in ["content-url = http://example.com/oscap_content.tar", 239 | "content-type = archive", 240 | "profile = Web Server", 241 | "xccdf-path = oscap/xccdf.xml", 242 | "tailoring-path = oscap/tailoring.xml", 243 | ]: 244 | blank_oscap_data.handle_line(line) 245 | 246 | blank_oscap_data.handle_end() 247 | 248 | # content_name should be the archive's name 249 | assert blank_oscap_data.content_name == "oscap_content.tar" 250 | 251 | # content path should end with the archive's name 252 | assert blank_oscap_data.raw_preinst_content_path.endswith("oscap_content.tar") 253 | 254 | # tailoring paths should be returned properly 255 | assert (blank_oscap_data.preinst_tailoring_path 256 | == common.INSTALLATION_CONTENT_DIR + blank_oscap_data.policy_data.tailoring_path) 257 | 258 | assert (blank_oscap_data.postinst_tailoring_path 259 | == common.TARGET_CONTENT_DIR + blank_oscap_data.policy_data.tailoring_path) 260 | 261 | 262 | def test_rpm_raw_content_paths(blank_oscap_data): 263 | for line in ["content-url = http://example.com/oscap_content.rpm", 264 | "content-type = rpm", 265 | "profile = Web Server", 266 | "xccdf-path = /usr/share/oscap/xccdf.xml", 267 | "tailoring-path = /usr/share/oscap/tailoring.xml", 268 | ]: 269 | blank_oscap_data.handle_line(line) 270 | 271 | blank_oscap_data.handle_end() 272 | 273 | # content_name should be the rpm's name 274 | assert blank_oscap_data.content_name == "oscap_content.rpm" 275 | 276 | # content path should end with the rpm's name 277 | assert blank_oscap_data.raw_preinst_content_path.endswith("oscap_content.rpm") 278 | 279 | # content paths should be returned as expected 280 | assert (blank_oscap_data.preinst_content_path 281 | == os.path.normpath(common.INSTALLATION_CONTENT_DIR + blank_oscap_data.policy_data.content_path)) 282 | 283 | # when using rpm, content_path doesn't change for the post-installation 284 | # phase 285 | assert blank_oscap_data.postinst_content_path == blank_oscap_data.policy_data.content_path 286 | 287 | 288 | def test_ds_raw_content_paths(blank_oscap_data): 289 | for line in ["content-url = http://example.com/scap_content.xml", 290 | "content-type = datastream", 291 | "profile = Web Server", 292 | ]: 293 | blank_oscap_data.handle_line(line) 294 | 295 | blank_oscap_data.handle_end() 296 | 297 | # content_name and content paths should all point to the data stream 298 | # XML 299 | assert blank_oscap_data.content_name == "scap_content.xml" 300 | assert blank_oscap_data.raw_preinst_content_path.endswith("scap_content.xml") 301 | 302 | 303 | def test_valid_fingerprints(blank_oscap_data): 304 | for repetitions in (32, 40, 56, 64, 96, 128): 305 | blank_oscap_data.handle_line("fingerprint = %s" % ("a" * repetitions)) 306 | 307 | 308 | def test_invalid_fingerprints(blank_oscap_data): 309 | # invalid character 310 | with pytest.raises(KickstartValueError, match="Unsupported or invalid fingerprint"): 311 | blank_oscap_data.handle_line("fingerprint = %s?" % ("a" * 31)) 312 | 313 | # invalid lengths (odd and even) 314 | for repetitions in (31, 41, 54, 66, 98, 124): 315 | with pytest.raises( 316 | KickstartValueError, match="Unsupported fingerprint"): 317 | blank_oscap_data.handle_line("fingerprint = %s" % ("a" * repetitions)) 318 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2013 Red Hat, Inc. 3 | # 4 | # This copyrighted material is made available to anyone wishing to use, 5 | # modify, copy, or redistribute it subject to the terms and conditions of 6 | # the GNU General Public License v.2, or (at your option) any later version. 7 | # This program is distributed in the hope that it will be useful, but WITHOUT 8 | # ANY WARRANTY expressed or implied, including the implied warranties of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 10 | # Public License for more details. You should have received a copy of the 11 | # GNU General Public License along with this program; if not, write to the 12 | # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 13 | # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the 14 | # source code or documentation are not subject to the GNU General Public 15 | # License and may only be used or replicated with the express permission of 16 | # Red Hat, Inc. 17 | # 18 | # Red Hat Author(s): Vratislav Podzimek 19 | # 20 | 21 | 22 | from unittest import mock 23 | import os 24 | from collections import namedtuple 25 | 26 | import pytest 27 | 28 | from org_fedora_oscap import utils 29 | 30 | 31 | @pytest.fixture() 32 | def mock_os(): 33 | mock_os = mock.Mock() 34 | mock_os.makedirs = mock.Mock() 35 | mock_os.path = mock.Mock() 36 | mock_os.path.isdir = mock.Mock() 37 | return mock_os 38 | 39 | 40 | def mock_utils_os(mock_os, monkeypatch): 41 | utils_module_symbols = utils.__dict__ 42 | 43 | monkeypatch.setitem(utils_module_symbols, "os", mock_os) 44 | 45 | 46 | def test_existing_dir(mock_os, monkeypatch): 47 | mock_utils_os(mock_os, monkeypatch) 48 | mock_os.path.isdir.return_value = True 49 | 50 | utils.ensure_dir_exists("/tmp/test_dir") 51 | 52 | mock_os.path.isdir.assert_called_with("/tmp/test_dir") 53 | assert not mock_os.makedirs.called 54 | 55 | 56 | def test_nonexisting_dir(mock_os, monkeypatch): 57 | mock_utils_os(mock_os, monkeypatch) 58 | mock_os.path.isdir.return_value = False 59 | 60 | utils.ensure_dir_exists("/tmp/test_dir") 61 | 62 | mock_os.path.isdir.assert_called_with("/tmp/test_dir") 63 | mock_os.makedirs.assert_called_with("/tmp/test_dir") 64 | 65 | 66 | def test_no_dir(mock_os, monkeypatch): 67 | mock_utils_os(mock_os, monkeypatch) 68 | # shouldn't raise an exception 69 | utils.ensure_dir_exists("") 70 | 71 | 72 | def test_relative_relative(): 73 | assert utils.join_paths("foo", "blah") == "foo/blah" 74 | 75 | 76 | def test_relative_absolute(): 77 | assert utils.join_paths("foo", "/blah") == "foo/blah" 78 | 79 | 80 | def test_absolute_relative(): 81 | assert utils.join_paths("/foo", "blah") == "/foo/blah" 82 | 83 | 84 | def test_absolute_absolute(): 85 | assert utils.join_paths("/foo", "/blah") == "/foo/blah" 86 | 87 | 88 | def test_dict(): 89 | dct = {"a": 1, "b": 2} 90 | 91 | mapped_dct = utils.keep_type_map(str.upper, dct) 92 | assert list(mapped_dct.keys()) == ["A", "B"] 93 | assert isinstance(mapped_dct, dict) 94 | 95 | 96 | def test_list(): 97 | lst = [1, 2, 4, 5] 98 | 99 | mapped_lst = utils.keep_type_map(lambda x: x ** 2, lst) 100 | assert mapped_lst == [1, 4, 16, 25] 101 | assert isinstance(mapped_lst, list) 102 | 103 | 104 | def test_tuple(): 105 | tpl = (1, 2, 4, 5) 106 | 107 | mapped_tpl = utils.keep_type_map(lambda x: x ** 2, tpl) 108 | assert mapped_tpl == (1, 4, 16, 25) 109 | assert isinstance(mapped_tpl, tuple) 110 | 111 | 112 | def test_namedtuple(): 113 | NT = namedtuple("TestingNT", ["a", "b"]) 114 | ntpl = NT(2, 4) 115 | 116 | mapped_tpl = utils.keep_type_map(lambda x: x ** 2, ntpl) 117 | assert mapped_tpl == NT(4, 16) 118 | assert isinstance(mapped_tpl, tuple) 119 | assert isinstance(mapped_tpl, NT) 120 | 121 | 122 | def test_set(): 123 | st = {1, 2, 4, 5} 124 | 125 | mapped_st = utils.keep_type_map(lambda x: x ** 2, st) 126 | assert mapped_st == {1, 4, 16, 25} 127 | assert isinstance(mapped_st, set) 128 | 129 | 130 | def test_str(): 131 | stri = "abcd" 132 | 133 | mapped_stri = utils.keep_type_map(lambda c: chr((ord(c) + 2) % 256), stri) 134 | assert mapped_stri == "cdef" 135 | assert isinstance(mapped_stri, str) 136 | 137 | 138 | def test_gen(): 139 | generator = (el for el in (1, 2, 4, 5)) 140 | 141 | mapped_generator = utils.keep_type_map(lambda x: x ** 2, generator) 142 | assert tuple(mapped_generator) == (1, 4, 16, 25) 143 | 144 | # any better test for this? 145 | assert "__next__" in dir(mapped_generator) 146 | 147 | 148 | def test_hash(): 149 | file_hash = '87fcda7d9e7a22412e95779e2f8e70f929106c7b27a94f5f8510553ebf4624a6' 150 | hash_obj = utils.get_hashing_algorithm(file_hash) 151 | assert hash_obj.name == "sha256" 152 | 153 | filepath = os.path.join(os.path.dirname(__file__), 'data', 'file') 154 | computed_hash = utils.get_file_fingerprint(filepath, hash_obj) 155 | 156 | assert file_hash == computed_hash 157 | -------------------------------------------------------------------------------- /tools/make-language-patch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Created by argbash-init v2.7.1 4 | # ARG_OPTIONAL_SINGLE([languages],[],[Space-separated string containing languages that should be added by the patch, e.g. "ca es en-US". If not supplied, no filtering will take place.]) 5 | # ARG_OPTIONAL_SINGLE([tmproot],[],[The root of the script's temp directory hierarchy.],[/tmp/oaa_po_$$]) 6 | # ARG_OPTIONAL_SINGLE([local-content],[l],[Don't pull translations from Zanata, but use this 'po' directory.]) 7 | # ARG_POSITIONAL_SINGLE([tarball],[Path to the tarball with upstream release that contains some translations already.]) 8 | # ARG_POSITIONAL_SINGLE([filename],[Where to save the patch.],[-]) 9 | # ARG_DEFAULTS_POS([]) 10 | # DEFINE_SCRIPT_DIR([]) 11 | # ARG_HELP([Get translations from Zanata / local 'po' directory, and craft a patch against translations in the release tarball.]) 12 | # ARGBASH_GO() 13 | # needed because of Argbash --> m4_ignore([ 14 | ### START OF CODE GENERATED BY Argbash v2.7.1 one line above ### 15 | # Argbash is a bash code generator used to get arguments parsing right. 16 | # Argbash is FREE SOFTWARE, see https://argbash.io for more info 17 | 18 | 19 | die() 20 | { 21 | local _ret=$2 22 | test -n "$_ret" || _ret=1 23 | test "$_PRINT_HELP" = yes && print_help >&2 24 | echo "$1" >&2 25 | exit ${_ret} 26 | } 27 | 28 | 29 | begins_with_short_option() 30 | { 31 | local first_option all_short_options='lh' 32 | first_option="${1:0:1}" 33 | test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0 34 | } 35 | 36 | # THE DEFAULTS INITIALIZATION - POSITIONALS 37 | _positionals=() 38 | _arg_tarball= 39 | _arg_filename="-" 40 | # THE DEFAULTS INITIALIZATION - OPTIONALS 41 | _arg_languages= 42 | _arg_tmproot="/tmp/oaa_po_$$" 43 | _arg_local_content= 44 | 45 | 46 | print_help() 47 | { 48 | printf '%s\n' "Get translations from Zanata / local 'po' directory, and craft a patch against translations in the release tarball." 49 | printf 'Usage: %s [--languages ] [--tmproot ] [-l|--local-content ] [-h|--help] []\n' "$0" 50 | printf '\t%s\n' ": Path to the tarball with upstream release that contains some translations already." 51 | printf '\t%s\n' ": Where to save the patch. (default: '-')" 52 | printf '\t%s\n' "--languages: Space-separated string containing languages that should be added by the patch, e.g. "ca es en-US". If not supplied, no filtering will take place. (no default)" 53 | printf '\t%s\n' "--tmproot: The root of the script's temp directory hierarchy. (default: '/tmp/oaa_po_$$')" 54 | printf '\t%s\n' "-l, --local-content: Don't pull translations from Zanata, but use this 'po' directory. (no default)" 55 | printf '\t%s\n' "-h, --help: Prints help" 56 | } 57 | 58 | 59 | parse_commandline() 60 | { 61 | _positionals_count=0 62 | while test $# -gt 0 63 | do 64 | _key="$1" 65 | case "$_key" in 66 | --languages) 67 | test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 68 | _arg_languages="$2" 69 | shift 70 | ;; 71 | --languages=*) 72 | _arg_languages="${_key##--languages=}" 73 | ;; 74 | --tmproot) 75 | test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 76 | _arg_tmproot="$2" 77 | shift 78 | ;; 79 | --tmproot=*) 80 | _arg_tmproot="${_key##--tmproot=}" 81 | ;; 82 | -l|--local-content) 83 | test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 84 | _arg_local_content="$2" 85 | shift 86 | ;; 87 | --local-content=*) 88 | _arg_local_content="${_key##--local-content=}" 89 | ;; 90 | -l*) 91 | _arg_local_content="${_key##-l}" 92 | ;; 93 | -h|--help) 94 | print_help 95 | exit 0 96 | ;; 97 | -h*) 98 | print_help 99 | exit 0 100 | ;; 101 | *) 102 | _last_positional="$1" 103 | _positionals+=("$_last_positional") 104 | _positionals_count=$((_positionals_count + 1)) 105 | ;; 106 | esac 107 | shift 108 | done 109 | } 110 | 111 | 112 | handle_passed_args_count() 113 | { 114 | local _required_args_string="'tarball'" 115 | test "${_positionals_count}" -ge 1 || _PRINT_HELP=yes die "FATAL ERROR: Not enough positional arguments - we require between 1 and 2 (namely: $_required_args_string), but got only ${_positionals_count}." 1 116 | test "${_positionals_count}" -le 2 || _PRINT_HELP=yes die "FATAL ERROR: There were spurious positional arguments --- we expect between 1 and 2 (namely: $_required_args_string), but got ${_positionals_count} (the last one was: '${_last_positional}')." 1 117 | } 118 | 119 | 120 | assign_positional_args() 121 | { 122 | local _positional_name _shift_for=$1 123 | _positional_names="_arg_tarball _arg_filename " 124 | 125 | shift "$_shift_for" 126 | for _positional_name in ${_positional_names} 127 | do 128 | test $# -gt 0 || break 129 | eval "$_positional_name=\${1}" || die "Error during argument parsing, possibly an Argbash bug." 1 130 | shift 131 | done 132 | } 133 | 134 | parse_commandline "$@" 135 | handle_passed_args_count 136 | assign_positional_args 1 "${_positionals[@]}" 137 | 138 | # OTHER STUFF GENERATED BY Argbash 139 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || die "Couldn't determine the script's running directory, which probably matters, bailing out" 2 140 | 141 | ### END OF CODE GENERATED BY Argbash (sortof) ### ]) 142 | # [ <-- needed because of Argbash 143 | 144 | oaa_root="$(cd "$script_dir/.." && pwd)" 145 | 146 | 147 | # $1: Where to put those translations 148 | pull_translations_locally () { 149 | (cd "$oaa_root" && make po-pull TRANSLATIONS_DIR="$1") 150 | } 151 | 152 | 153 | # $1: Location of local translations 154 | # $2: Where to put those translations 155 | copy_translations_from_directory() { 156 | cp "$1/"*.po "$2/" 157 | } 158 | 159 | 160 | # $1: OAA tarball 161 | # $2: Where to put translations (i.e. contents of the RPM's 'po' folder) 162 | extract_translations_from_tarball () { 163 | tarball_stem="$(printf "%s" "$1" | sed -e 's|.*\(oscap-anaconda-addon[^/]*\)\.tar\.gz|\1|')" 164 | tar --strip-components=2 -C "$2" -xf "$1" "$tarball_stem/po" 165 | } 166 | 167 | 168 | # $1: Directory with translation files. 169 | # $2: Space-separated list of languages that we support. 170 | filter_translations() { 171 | local regex="$(languages_to_ext_regex "$2")" 172 | for file in "$1"/*.po; do 173 | printf "%s" "$file" | grep -Eq "$regex" || rm "$file" 174 | done 175 | } 176 | 177 | 178 | languages_to_ext_regex() { 179 | local regex="" 180 | for lang in $1; do 181 | regex="$regex$(printf "%s" "$lang" | tr - _)|" 182 | done 183 | printf "%s" "(${regex%|})\\.po$" 184 | } 185 | 186 | tmpdir="$_arg_tmproot" 187 | rm -rf "$tmpdir" 188 | 189 | srcdir="$tmpdir/a/po" 190 | mkdir -p "$srcdir" 191 | 192 | destdir="$tmpdir/b/po" 193 | mkdir -p "$destdir" 194 | 195 | if test -n "$_arg_local_content"; then 196 | copy_translations_from_directory "$_arg_local_content" "$destdir" 197 | else 198 | pull_translations_locally "$destdir" 199 | fi 200 | 201 | cp "$oaa_root/po/Makefile" "$oaa_root/po/oscap-anaconda-addon.pot" "$destdir" 202 | extract_translations_from_tarball "$_arg_tarball" "$srcdir" 203 | 204 | rm -f "$srcdir/*.mo" 205 | rm -f "$destdir/*.mo" 206 | 207 | test -n "$_arg_languages" && filter_translations "$destdir" "$_arg_languages" 208 | 209 | if test "$_arg_filename" = -; then 210 | (cd "$tmpdir" && diff -U3 -N -r a b) 211 | else 212 | (cd "$tmpdir" && diff -U3 -N -r a b) > "$_arg_filename" 213 | fi 214 | 215 | rm -rf "$tmpdir" 216 | 217 | # ] <-- needed because of Argbash 218 | -------------------------------------------------------------------------------- /tools/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "***Running tests***" 4 | if make test; then 5 | echo "***Tests passed***" 6 | exit 0 7 | else 8 | echo "***TESTS FAILED***" 9 | exit 1 10 | fi 11 | --------------------------------------------------------------------------------