From 7ba95a91dc1123f09ebcd0f7db2bfddfb0f12074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C4=8Cern=C3=BD?= Date: Fri, 11 Oct 2024 17:07:06 +0200 Subject: [PATCH 1/3] Introduce generating bootc remediation --- src/XCCDF_POLICY/xccdf_policy_remediate.c | 145 ++++++++++++++++++++++ utils/oscap-xccdf.c | 10 +- utils/oscap.8 | 3 +- 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/src/XCCDF_POLICY/xccdf_policy_remediate.c b/src/XCCDF_POLICY/xccdf_policy_remediate.c index 8c2aaf98c9..592114a8e3 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 *package_install; + struct oscap_list *package_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_PACKAGE, + BOOTC_PACKAGE_INSTALL, + BOOTC_PACKAGE_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, "package")) { + state = BOOTC_PACKAGE; + } else { + ret = 1; + oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unsupported command keyword '%s' in command: '%s'", word, line); + goto cleanup; + } + break; + case BOOTC_PACKAGE: + if (!strcmp(word, "install")) { + state = BOOTC_PACKAGE_INSTALL; + } else if (!strcmp(word, "remove")) { + state = BOOTC_PACKAGE_REMOVE; + } else { + ret = 1; + oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unsupported 'package' command keyword '%s' in command:'%s'", word, line); + goto cleanup; + } + break; + case BOOTC_PACKAGE_INSTALL: + oscap_list_add(cmds->package_install, strdup(word)); + break; + case BOOTC_PACKAGE_REMOVE: + oscap_list_add(cmds->package_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_packages(struct bootc_commands *cmds, int output_fd) +{ + struct oscap_iterator *package_install_it = oscap_iterator_new(cmds->package_install); + if (oscap_iterator_has_more(package_install_it)) { + _write_text_to_fd(output_fd, "dnf -y install \\\n"); + while (oscap_iterator_has_more(package_install_it)) { + char *package = (char *) oscap_iterator_next(package_install_it); + _write_text_to_fd(output_fd, " "); + _write_text_to_fd(output_fd, package); + if (oscap_iterator_has_more(package_install_it)) + _write_text_to_fd(output_fd, " \\\n"); + } + _write_text_to_fd(output_fd, "\n\n"); + } + oscap_iterator_free(package_install_it); + + struct oscap_iterator *package_remove_it = oscap_iterator_new(cmds->package_remove); + if (oscap_iterator_has_more(package_remove_it)) { + _write_text_to_fd(output_fd, "dnf -y remove \\\n"); + while (oscap_iterator_has_more(package_remove_it)) { + char *package = (char *) oscap_iterator_next(package_remove_it); + _write_text_to_fd(output_fd, " "); + _write_text_to_fd(output_fd, package); + if (oscap_iterator_has_more(package_remove_it)) + _write_text_to_fd(output_fd, " \\\n"); + } + _write_text_to_fd(output_fd, "\n"); + } + oscap_iterator_free(package_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 = { + .package_install = oscap_list_new(), + .package_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_packages(&cmds, output_fd); + + oscap_list_free(cmds.package_install, free); + oscap_list_free(cmds.package_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/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. From c2c3a9f2c1488ea0028a79422ff06762f0f2ebf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C4=8Cern=C3=BD?= Date: Fri, 11 Oct 2024 17:52:50 +0200 Subject: [PATCH 2/3] Add a simple test --- tests/API/XCCDF/unittests/CMakeLists.txt | 1 + .../unittests/test_remediation_bootc.ds.xml | 87 +++++++++++++++++++ .../XCCDF/unittests/test_remediation_bootc.sh | 19 ++++ .../test_remediation_bootc_expected_output.sh | 8 ++ 4 files changed, 115 insertions(+) create mode 100644 tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml create mode 100755 tests/API/XCCDF/unittests/test_remediation_bootc.sh create mode 100644 tests/API/XCCDF/unittests/test_remediation_bootc_expected_output.sh 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..311068bf8d --- /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 + + package install rsyslog + + + + Rule 2: Remove USBGuard + + package remove usbguard + + + + Rule 3: Install reboot package + + package install reboot + + + + Rule 4: Install podman package + + package 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 From 4b6d0f2a6d34f6bfb380401b23d7dfa884a7e4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C4=8Cern=C3=BD?= Date: Thu, 17 Oct 2024 14:48:38 +0200 Subject: [PATCH 3/3] Rename "package" keyword to "dnf" This will allow us in future to extend the code to support remediations for systems where a different package manager than "dnf" is used. --- src/XCCDF_POLICY/xccdf_policy_remediate.c | 66 +++++++++---------- .../unittests/test_remediation_bootc.ds.xml | 8 +-- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/XCCDF_POLICY/xccdf_policy_remediate.c b/src/XCCDF_POLICY/xccdf_policy_remediate.c index 592114a8e3..70eb188838 100644 --- a/src/XCCDF_POLICY/xccdf_policy_remediate.c +++ b/src/XCCDF_POLICY/xccdf_policy_remediate.c @@ -50,8 +50,8 @@ #include "oscap_helpers.h" struct bootc_commands { - struct oscap_list *package_install; - struct oscap_list *package_remove; + struct oscap_list *dnf_install; + struct oscap_list *dnf_remove; }; static int _rule_add_info_message(struct xccdf_rule_result *rr, ...) @@ -1298,9 +1298,9 @@ static int _parse_bootc_line(const char *line, struct bootc_commands *cmds) char **words = oscap_split(dup, " "); enum states { BOOTC_START, - BOOTC_PACKAGE, - BOOTC_PACKAGE_INSTALL, - BOOTC_PACKAGE_REMOVE, + BOOTC_DNF, + BOOTC_DNF_INSTALL, + BOOTC_DNF_REMOVE, BOOTC_ERROR }; int state = BOOTC_START; @@ -1310,30 +1310,30 @@ static int _parse_bootc_line(const char *line, struct bootc_commands *cmds) continue; switch (state) { case BOOTC_START: - if (!strcmp(word, "package")) { - state = BOOTC_PACKAGE; + 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_PACKAGE: + case BOOTC_DNF: if (!strcmp(word, "install")) { - state = BOOTC_PACKAGE_INSTALL; + state = BOOTC_DNF_INSTALL; } else if (!strcmp(word, "remove")) { - state = BOOTC_PACKAGE_REMOVE; + state = BOOTC_DNF_REMOVE; } else { ret = 1; - oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unsupported 'package' command keyword '%s' in command:'%s'", word, line); + oscap_seterr(OSCAP_EFAMILY_OSCAP, "Unsupported 'dnf' command keyword '%s' in command:'%s'", word, line); goto cleanup; } break; - case BOOTC_PACKAGE_INSTALL: - oscap_list_add(cmds->package_install, strdup(word)); + case BOOTC_DNF_INSTALL: + oscap_list_add(cmds->dnf_install, strdup(word)); break; - case BOOTC_PACKAGE_REMOVE: - oscap_list_add(cmds->package_remove, strdup(word)); + case BOOTC_DNF_REMOVE: + oscap_list_add(cmds->dnf_remove, strdup(word)); break; case BOOTC_ERROR: ret = 1; @@ -1373,43 +1373,43 @@ static int _xccdf_policy_rule_generate_bootc_fix(struct xccdf_policy *policy, st return ret; } -static int _generate_bootc_packages(struct bootc_commands *cmds, int output_fd) +static int _generate_bootc_dnf(struct bootc_commands *cmds, int output_fd) { - struct oscap_iterator *package_install_it = oscap_iterator_new(cmds->package_install); - if (oscap_iterator_has_more(package_install_it)) { + 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(package_install_it)) { - char *package = (char *) oscap_iterator_next(package_install_it); + 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(package_install_it)) + 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(package_install_it); + oscap_iterator_free(dnf_install_it); - struct oscap_iterator *package_remove_it = oscap_iterator_new(cmds->package_remove); - if (oscap_iterator_has_more(package_remove_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(package_remove_it)) { - char *package = (char *) oscap_iterator_next(package_remove_it); + 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(package_remove_it)) + 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(package_remove_it); + 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 = { - .package_install = oscap_list_new(), - .package_remove = oscap_list_new(), + .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); @@ -1422,10 +1422,10 @@ static int _xccdf_policy_generate_fix_bootc(struct oscap_list *rules_to_fix, str oscap_iterator_free(rules_to_fix_it); _write_text_to_fd(output_fd, "#!/bin/bash\n"); - _generate_bootc_packages(&cmds, output_fd); + _generate_bootc_dnf(&cmds, output_fd); - oscap_list_free(cmds.package_install, free); - oscap_list_free(cmds.package_remove, free); + oscap_list_free(cmds.dnf_install, free); + oscap_list_free(cmds.dnf_remove, free); return ret; } diff --git a/tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml b/tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml index 311068bf8d..6134381d4c 100644 --- a/tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml +++ b/tests/API/XCCDF/unittests/test_remediation_bootc.ds.xml @@ -61,25 +61,25 @@ Rule 1: Install rsyslog package - package install rsyslog + dnf install rsyslog Rule 2: Remove USBGuard - package remove usbguard + dnf remove usbguard Rule 3: Install reboot package - package install reboot + dnf install reboot Rule 4: Install podman package - package install podman + dnf install podman