diff --git a/src/XCCDF_POLICY/xccdf_policy_remediate.c b/src/XCCDF_POLICY/xccdf_policy_remediate.c index 8c2aaf98c9..70eb188838 100644 --- a/src/XCCDF_POLICY/xccdf_policy_remediate.c +++ b/src/XCCDF_POLICY/xccdf_policy_remediate.c @@ -49,6 +49,11 @@ #include "public/xccdf_policy.h" #include "oscap_helpers.h" +struct bootc_commands { + struct oscap_list *dnf_install; + struct oscap_list *dnf_remove; +}; + static int _rule_add_info_message(struct xccdf_rule_result *rr, ...) { va_list ap; @@ -1286,6 +1291,144 @@ static int _xccdf_policy_generate_fix_other(struct oscap_list *rules_to_fix, str return ret; } +static int _parse_bootc_line(const char *line, struct bootc_commands *cmds) +{ + int ret = 0; + char *dup = strdup(line); + char **words = oscap_split(dup, " "); + enum states { + BOOTC_START, + BOOTC_DNF, + BOOTC_DNF_INSTALL, + BOOTC_DNF_REMOVE, + BOOTC_ERROR + }; + int state = BOOTC_START; + for (unsigned int i = 0; words[i] != NULL; i++) { + char *word = oscap_trim(words[i]); + if (*word == '\0') + continue; + switch (state) { + case BOOTC_START: + if (!strcmp(word, "dnf")) { + state = BOOTC_DNF; + } else { + ret = 1; + oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unsupported command keyword '%s' in command: '%s'", word, line); + goto cleanup; + } + break; + case BOOTC_DNF: + if (!strcmp(word, "install")) { + state = BOOTC_DNF_INSTALL; + } else if (!strcmp(word, "remove")) { + state = BOOTC_DNF_REMOVE; + } else { + ret = 1; + oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unsupported 'dnf' command keyword '%s' in command:'%s'", word, line); + goto cleanup; + } + break; + case BOOTC_DNF_INSTALL: + oscap_list_add(cmds->dnf_install, strdup(word)); + break; + case BOOTC_DNF_REMOVE: + oscap_list_add(cmds->dnf_remove, strdup(word)); + break; + case BOOTC_ERROR: + ret = 1; + oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unexpected string '%s' in command: '%s'", word, line); + goto cleanup; + default: + break; + } + } + +cleanup: + free(words); + free(dup); + return ret; +} + +static int _xccdf_policy_rule_generate_bootc_fix(struct xccdf_policy *policy, struct xccdf_rule *rule, const char *template, struct bootc_commands *cmds) +{ + char *fix_text = NULL; + int ret = _xccdf_policy_rule_get_fix_text(policy, rule, template, &fix_text); + if (fix_text == NULL) { + return ret; + } + char *dup = strdup(fix_text); + char **lines = oscap_split(dup, "\n"); + for (unsigned int i = 0; lines[i] != NULL; i++) { + char *line = lines[i]; + char *trim_line = oscap_trim(strdup(line)); + if (*trim_line != '#' && *trim_line != '\0') { + _parse_bootc_line(trim_line, cmds); + } + free(trim_line); + } + free(lines); + free(dup); + free(fix_text); + return ret; +} + +static int _generate_bootc_dnf(struct bootc_commands *cmds, int output_fd) +{ + struct oscap_iterator *dnf_install_it = oscap_iterator_new(cmds->dnf_install); + if (oscap_iterator_has_more(dnf_install_it)) { + _write_text_to_fd(output_fd, "dnf -y install \\\n"); + while (oscap_iterator_has_more(dnf_install_it)) { + char *package = (char *) oscap_iterator_next(dnf_install_it); + _write_text_to_fd(output_fd, " "); + _write_text_to_fd(output_fd, package); + if (oscap_iterator_has_more(dnf_install_it)) + _write_text_to_fd(output_fd, " \\\n"); + } + _write_text_to_fd(output_fd, "\n\n"); + } + oscap_iterator_free(dnf_install_it); + + struct oscap_iterator *dnf_remove_it = oscap_iterator_new(cmds->dnf_remove); + if (oscap_iterator_has_more(dnf_remove_it)) { + _write_text_to_fd(output_fd, "dnf -y remove \\\n"); + while (oscap_iterator_has_more(dnf_remove_it)) { + char *package = (char *) oscap_iterator_next(dnf_remove_it); + _write_text_to_fd(output_fd, " "); + _write_text_to_fd(output_fd, package); + if (oscap_iterator_has_more(dnf_remove_it)) + _write_text_to_fd(output_fd, " \\\n"); + } + _write_text_to_fd(output_fd, "\n"); + } + oscap_iterator_free(dnf_remove_it); + return 0; +} + +static int _xccdf_policy_generate_fix_bootc(struct oscap_list *rules_to_fix, struct xccdf_policy *policy, const char *sys, int output_fd) +{ + struct bootc_commands cmds = { + .dnf_install = oscap_list_new(), + .dnf_remove = oscap_list_new(), + }; + int ret = 0; + struct oscap_iterator *rules_to_fix_it = oscap_iterator_new(rules_to_fix); + while (oscap_iterator_has_more(rules_to_fix_it)) { + struct xccdf_rule *rule = (struct xccdf_rule *) oscap_iterator_next(rules_to_fix_it); + ret = _xccdf_policy_rule_generate_bootc_fix(policy, rule, sys, &cmds); + if (ret != 0) + break; + } + oscap_iterator_free(rules_to_fix_it); + + _write_text_to_fd(output_fd, "#!/bin/bash\n"); + _generate_bootc_dnf(&cmds, output_fd); + + oscap_list_free(cmds.dnf_install, free); + oscap_list_free(cmds.dnf_remove, free); + return ret; +} + int xccdf_policy_generate_fix(struct xccdf_policy *policy, struct xccdf_result *result, const char *sys, int output_fd) { __attribute__nonnull__(policy); @@ -1342,6 +1485,8 @@ int xccdf_policy_generate_fix(struct xccdf_policy *policy, struct xccdf_result * ret = _xccdf_policy_generate_fix_ansible(rules_to_fix, policy, sys, output_fd); } else if (strcmp(sys, "urn:redhat:osbuild:blueprint") == 0) { ret = _xccdf_policy_generate_fix_blueprint(rules_to_fix, policy, sys, output_fd); + } else if (strcmp(sys, "urn:xccdf:fix:script:bootc") == 0) { + ret = _xccdf_policy_generate_fix_bootc(rules_to_fix, policy, sys, output_fd); } else { ret = _xccdf_policy_generate_fix_other(rules_to_fix, policy, sys, output_fd); } diff --git a/tests/API/XCCDF/unittests/CMakeLists.txt b/tests/API/XCCDF/unittests/CMakeLists.txt index 7a9b3b452c..164b795e0e 100644 --- a/tests/API/XCCDF/unittests/CMakeLists.txt +++ b/tests/API/XCCDF/unittests/CMakeLists.txt @@ -110,3 +110,4 @@ add_oscap_test("test_skip_rule.sh") add_oscap_test("test_no_newline_between_select_elements.sh") add_oscap_test("test_single_line_tailoring.sh") add_oscap_test("test_reference.sh") +add_oscap_test("test_remediation_bootc.sh") diff --git a/tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml b/tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml new file mode 100644 index 0000000000..6134381d4c --- /dev/null +++ b/tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + 5.11.2 + 2021-02-01T08:07:06+01:00 + + + + + PASS + pass + + + + + + + + + + + + + + oval:org.openscap.www:var:1 + + + + + 100 + + + + + + + accepted + 1.0 + + Common hardening profile + This is a very cool profile + + + + + Rule 1: Install rsyslog package + + dnf install rsyslog + + + + Rule 2: Remove USBGuard + + dnf remove usbguard + + + + Rule 3: Install reboot package + + dnf install reboot + + + + Rule 4: Install podman package + + dnf install podman + + + + + diff --git a/tests/API/XCCDF/unittests/test_remediation_bootc.sh b/tests/API/XCCDF/unittests/test_remediation_bootc.sh new file mode 100755 index 0000000000..cf193e75a5 --- /dev/null +++ b/tests/API/XCCDF/unittests/test_remediation_bootc.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +. $builddir/tests/test_common.sh + +set -e +set -o pipefail + +name=$(basename $0 .sh) +result=$(mktemp) +stderr=$(mktemp) + +echo "Result file = $result" +echo "Stderr file = $stderr" + +$OSCAP xccdf generate fix --fix-type bootc --profile common "$srcdir/test_remediation_bootc.ds.xml" > "$result" 2> "$stderr" +[ -e $stderr ] + +diff -u "$srcdir/test_remediation_bootc_expected_output.sh" "$result" + +rm -rf "$stdout" "$stderr" "$result" diff --git a/tests/API/XCCDF/unittests/test_remediation_bootc_expected_output.sh b/tests/API/XCCDF/unittests/test_remediation_bootc_expected_output.sh new file mode 100644 index 0000000000..57a7ffb3df --- /dev/null +++ b/tests/API/XCCDF/unittests/test_remediation_bootc_expected_output.sh @@ -0,0 +1,8 @@ +#!/bin/bash +dnf -y install \ + rsyslog \ + reboot \ + podman + +dnf -y remove \ + usbguard diff --git a/utils/oscap-xccdf.c b/utils/oscap-xccdf.c index 2bcdac2e1c..54680b3595 100644 --- a/utils/oscap-xccdf.c +++ b/utils/oscap-xccdf.c @@ -285,7 +285,7 @@ static struct oscap_module XCCDF_GEN_FIX = { .help = GEN_OPTS "\nFix Options:\n" " --fix-type - Fix type. Should be one of: bash, ansible, puppet, anaconda, ignition, kubernetes,\n" - " blueprint (default: bash).\n" + " blueprint, bootc (default: bash).\n" " --output - Write the script into file.\n" " --result-id - Fixes will be generated for failed rule-results of the specified TestResult.\n" " --template - Fix template. (default: bash)\n" @@ -971,10 +971,12 @@ int app_generate_fix(const struct oscap_action *action) template = "urn:xccdf:fix:script:kubernetes"; } else if (strcmp(action->fix_type, "blueprint") == 0) { template = "urn:redhat:osbuild:blueprint"; + } else if (strcmp(action->fix_type, "bootc") == 0) { + template = "urn:xccdf:fix:script:bootc"; } else { fprintf(stderr, "Unknown fix type '%s'.\n" - "Please provide one of: bash, ansible, puppet, anaconda, ignition, kubernetes, blueprint.\n" + "Please provide one of: bash, ansible, puppet, anaconda, ignition, kubernetes, blueprint, bootc.\n" "Or provide a custom template using '--template' instead.\n", action->fix_type); return OSCAP_ERROR; @@ -984,6 +986,10 @@ int app_generate_fix(const struct oscap_action *action) } else { template = "urn:xccdf:fix:script:sh"; } + if (action->id != NULL && action->fix_type != NULL && !strcmp(action->fix_type, "bootc")) { + fprintf(stderr, "It isn't possible to generate results-oriented bootc remediations.\n"); + return OSCAP_ERROR; + } int ret = OSCAP_ERROR; struct oscap_source *source = oscap_source_new_from_file(action->f_xccdf); diff --git a/utils/oscap.8 b/utils/oscap.8 index 09da46d008..23c6c80cca 100644 --- a/utils/oscap.8 +++ b/utils/oscap.8 @@ -430,11 +430,12 @@ To use the ability to include additional information from SCE in XCCDF result fi Generate a script that shall bring the system to a state of compliance with given XCCDF Benchmark. There are 2 possibilities when generating fixes: Result-oriented fixes (--result-id) or Profile-oriented fixes (--profile). Result-oriented takes precedences over Profile-oriented, if result-id is given, oscap will ignore any profile provided. .TP Result-oriented fixes are generated using result-id provided to select only the failing rules from results in xccdf-file, it skips all other rules. +It isn't possible to generate result-oriented fixes for the bootc fix type. .TP Profile-oriented fixes are generated using all rules within the provided profile. If no result-id/profile are provided, (default) profile will be used to generate fixes. .TP \fB\-\-fix-type TYPE\fR -Specify fix type. There are multiple programming languages in which the fix script can be generated. TYPE should be one of: bash, ansible, puppet, anaconda, ignition, kubernetes, blueprint. Default is bash. This option is mutually exclusive with --template, because fix type already determines the template URN. +Specify fix type. There are multiple programming languages in which the fix script can be generated. TYPE should be one of: bash, ansible, puppet, anaconda, ignition, kubernetes, blueprint, bootc. Default is bash. This option is mutually exclusive with --template, because fix type already determines the template URN. .TP \fB\-\-output FILE\fR Write the report to this file instead of standard output.