From 5a421dfd3f783e5f317ff6764b1203e08bb0ff4e Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Dec 07 2021 17:39:17 +0000 Subject: import openscap-1.3.5-13.el9 --- diff --git a/SOURCES/openscap-1.3.6-alternative-hostname-pr-1806.patch b/SOURCES/openscap-1.3.6-alternative-hostname-pr-1806.patch new file mode 100644 index 0000000..1d6b269 --- /dev/null +++ b/SOURCES/openscap-1.3.6-alternative-hostname-pr-1806.patch @@ -0,0 +1,72 @@ +From d97687c12ba6cbca1d732534ff7394bd14547d92 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jan=20=C4=8Cern=C3=BD?= +Date: Fri, 10 Sep 2021 14:53:42 +0200 +Subject: [PATCH] Add an alternative source of hostname + +If /etc/hostname can't be read, we will try to open /proc/sys/kernel/hostname instead. + +Resolves: rhbz#1977668 +--- + src/XCCDF/result.c | 5 ++++ + tests/API/XCCDF/unittests/CMakeLists.txt | 1 + + .../XCCDF/unittests/test_results_hostname.sh | 26 +++++++++++++++++++ + 3 files changed, 32 insertions(+) + create mode 100755 tests/API/XCCDF/unittests/test_results_hostname.sh + +diff --git a/src/XCCDF/result.c b/src/XCCDF/result.c +index 91fcc6041d..c0ad4b926f 100644 +--- a/src/XCCDF/result.c ++++ b/src/XCCDF/result.c +@@ -271,6 +271,11 @@ static char *_get_etc_hostname(const char *oscap_probe_root) + + fp = oscap_fopen_with_prefix(oscap_probe_root, "/etc/hostname"); + ++ if (fp == NULL) { ++ dD("Trying to use /proc/sys/kernel/hostname instead of /etc/hostname"); ++ fp = oscap_fopen_with_prefix(oscap_probe_root, "/proc/sys/kernel/hostname"); ++ } ++ + if (fp == NULL) + goto fail; + +diff --git a/tests/API/XCCDF/unittests/CMakeLists.txt b/tests/API/XCCDF/unittests/CMakeLists.txt +index 52645834c4..6549538440 100644 +--- a/tests/API/XCCDF/unittests/CMakeLists.txt ++++ b/tests/API/XCCDF/unittests/CMakeLists.txt +@@ -101,3 +101,4 @@ add_oscap_test("test_fix_arf.sh") + add_oscap_test("test_fix_resultid_by_suffix.sh") + add_oscap_test("test_generate_fix_ansible_vars.sh") + add_oscap_test("test_xccdf_requires_conflicts.sh") ++add_oscap_test("test_results_hostname.sh") +diff --git a/tests/API/XCCDF/unittests/test_results_hostname.sh b/tests/API/XCCDF/unittests/test_results_hostname.sh +new file mode 100755 +index 0000000000..c4408affbb +--- /dev/null ++++ b/tests/API/XCCDF/unittests/test_results_hostname.sh +@@ -0,0 +1,26 @@ ++#!/usr/bin/env bash ++. $builddir/tests/test_common.sh ++ ++set -e ++set -o pipefail ++ ++result=$(mktemp) ++tmpdir=$(mktemp -d) ++ ++export OSCAP_PROBE_ROOT="$tmpdir" ++ ++mkdir -p "$tmpdir/etc" ++echo "hostname_defined_in_etc_hostname" > "$tmpdir/etc/hostname" ++$OSCAP xccdf eval --results "$result" "$srcdir/test_single_rule.ds.xml" || ret=$? ++assert_exists 1 '/Benchmark/TestResult/target[text()="hostname_defined_in_etc_hostname"]' ++assert_exists 0 '/Benchmark/TestResult/target[text()="hostname_defined_in_proc_sys_kernel"]' ++ ++rm -rf "$tmpdir/etc/hostname" ++mkdir -p "$tmpdir/proc/sys/kernel/" ++echo "hostname_defined_in_proc_sys_kernel" > "$tmpdir/proc/sys/kernel/hostname" ++$OSCAP xccdf eval --results "$result" "$srcdir/test_single_rule.ds.xml" || ret=$? ++assert_exists 0 '/Benchmark/TestResult/target[text()="hostname_defined_in_etc_hostname"]' ++assert_exists 1 '/Benchmark/TestResult/target[text()="hostname_defined_in_proc_sys_kernel"]' ++ ++rm -f "$result" ++rm -rf "$tmpdir" diff --git a/SOURCES/openscap-1.3.6-blueprint-fix-pr-1749.patch b/SOURCES/openscap-1.3.6-blueprint-fix-pr-1749.patch new file mode 100644 index 0000000..0e44989 --- /dev/null +++ b/SOURCES/openscap-1.3.6-blueprint-fix-pr-1749.patch @@ -0,0 +1,64 @@ +From 5f0a9033b466d929613a2a55a1524ec75c09b5b0 Mon Sep 17 00:00:00 2001 +From: Evgeny Kolesnikov +Date: Thu, 6 May 2021 08:14:12 +0200 +Subject: [PATCH] Introduce OSBuild Blueprint fix type + +--- + utils/oscap-xccdf.c | 7 +++++-- + utils/oscap.8 | 2 +- + xsl/xccdf-share.xsl | 1 + + 3 files changed, 7 insertions(+), 3 deletions(-) + +diff --git a/utils/oscap-xccdf.c b/utils/oscap-xccdf.c +index 95c1c7658d..801e54fa35 100644 +--- a/utils/oscap-xccdf.c ++++ b/utils/oscap-xccdf.c +@@ -275,7 +275,8 @@ static struct oscap_module XCCDF_GEN_FIX = { + .usage = "[options] xccdf-file.xml", + .help = GEN_OPTS + "\nFix Options:\n" +- " --fix-type - Fix type. Should be one of: bash, ansible, puppet, anaconda (default: bash).\n" ++ " --fix-type - Fix type. Should be one of: bash, ansible, puppet, anaconda, ignition, kubernetes,\n" ++ " blueprint (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" +@@ -887,10 +888,12 @@ int app_generate_fix(const struct oscap_action *action) + template = "urn:xccdf:fix:script:ignition"; + } else if (strcmp(action->fix_type, "kubernetes") == 0) { + template = "urn:xccdf:fix:script:kubernetes"; ++ } else if (strcmp(action->fix_type, "blueprint") == 0) { ++ template = "urn:redhat:osbuild:blueprint"; + } else { + fprintf(stderr, + "Unknown fix type '%s'.\n" +- "Please provide one of: bash, ansible, puppet, anaconda, ignition, kubernetes.\n" ++ "Please provide one of: bash, ansible, puppet, anaconda, ignition, kubernetes, blueprint.\n" + "Or provide a custom template using '--template' instead.\n", + action->fix_type); + return OSCAP_ERROR; +diff --git a/utils/oscap.8 b/utils/oscap.8 +index 240b829d7b..6cae0ffe8a 100644 +--- a/utils/oscap.8 ++++ b/utils/oscap.8 +@@ -395,7 +395,7 @@ Result-oriented fixes are generated using result-id provided to select only the + 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. 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. 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. +diff --git a/xsl/xccdf-share.xsl b/xsl/xccdf-share.xsl +index 9f8e587676..d7a9f3b7e2 100644 +--- a/xsl/xccdf-share.xsl ++++ b/xsl/xccdf-share.xsl +@@ -295,6 +295,7 @@ Authors: + Puppet snippet + Anaconda snippet + Kubernetes snippet ++ OSBuild Blueprint snippet + script + + diff --git a/SOURCES/openscap-1.3.6-blueprint-toml-pr-1810.patch b/SOURCES/openscap-1.3.6-blueprint-toml-pr-1810.patch new file mode 100644 index 0000000..ec31a3d --- /dev/null +++ b/SOURCES/openscap-1.3.6-blueprint-toml-pr-1810.patch @@ -0,0 +1,583 @@ +From b0b7626dca08acd4563ae42c1c27ccc0777b5357 Mon Sep 17 00:00:00 2001 +From: Evgeny Kolesnikov +Date: Thu, 23 Sep 2021 00:58:29 +0200 +Subject: [PATCH] Add proper Blueprint's remediation snippets handling for + generation of the final TOML document. + +As the final Blueprint could not be created by just gluing up all +the snippets together we have to get a bit more creative. +--- + docs/manual/manual.adoc | 15 ++ + src/XCCDF_POLICY/xccdf_policy_remediate.c | 216 ++++++++++++++++-- + src/common/list.c | 19 ++ + src/common/list.h | 1 + + tests/API/XCCDF/unittests/CMakeLists.txt | 1 + + .../unittests/test_remediation_blueprint.sh | 27 +++ + .../unittests/test_remediation_blueprint.toml | 45 ++++ + .../test_remediation_blueprint.xccdf.xml | 102 +++++++++ + 8 files changed, 405 insertions(+), 21 deletions(-) + create mode 100755 tests/API/XCCDF/unittests/test_remediation_blueprint.sh + create mode 100644 tests/API/XCCDF/unittests/test_remediation_blueprint.toml + create mode 100644 tests/API/XCCDF/unittests/test_remediation_blueprint.xccdf.xml + +diff --git a/docs/manual/manual.adoc b/docs/manual/manual.adoc +index e8664eb920..90e2cc2c63 100644 +--- a/docs/manual/manual.adoc ++++ b/docs/manual/manual.adoc +@@ -1084,6 +1084,21 @@ scanned during this command. If you want to generate remediation only for the + failed rules based on scan results, refer to <<_reviewing_remediations,Reviewing + remediations>>. + ++=== Generating Image Builder Blueprints ++ ++OpenSCAP can also create a remediation in form of Image Builder (OSBuild) Blueprint. This remeditaion ++is intendeded to be used as a bootstrap for image creation and usually it will contain only essential ++elements of the configuration, elements that would be hard or impossible to change after the image ++is created, like partitioning or set of installed packages. ++ ++It is recommended to combine this type of remediation with other types, executed on the running system. ++ ++For example, to generate a blueprint remediation for RHEL 8 OSPP profile, run: ++ ++---- ++$ oscap xccdf generate fix --profile ospp --fix-type blueprint /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml > blueprint.toml ++---- ++ + == Details on SCAP conformance + + === Check Engines +diff --git a/src/XCCDF_POLICY/xccdf_policy_remediate.c b/src/XCCDF_POLICY/xccdf_policy_remediate.c +index 0b3a037a5f..6033c3b54b 100644 +--- a/src/XCCDF_POLICY/xccdf_policy_remediate.c ++++ b/src/XCCDF_POLICY/xccdf_policy_remediate.c +@@ -656,6 +656,78 @@ static int _write_fix_missing_warning_to_fd(const char *sys, int output_fd, stru + } + } + ++struct blueprint_entries { ++ const char *pattern; ++ struct oscap_list *list; ++ pcre *re; ++}; ++ ++static inline int _parse_blueprint_fix(const char *fix_text, struct oscap_list *generic, struct oscap_list *services_enable, struct oscap_list *services_disable, struct oscap_list *kernel_append) ++{ ++ const char *err; ++ int errofs; ++ int ret = 0; ++ ++ struct blueprint_entries tab[] = { ++ {"\\[customizations\\.services\\]\\s+enabled[=\\s]+\\[([^\\]]+)\\]\\s+", services_enable, NULL}, ++ {"\\[customizations\\.services\\]\\s+disabled[=\\s]+\\[([^\\]]+)\\]\\s+", services_disable, NULL}, ++ {"\\[customizations\\.kernel\\]\\s+append[=\\s\"]+([^\"]+)[\\s\"]+", kernel_append, NULL}, ++ // We do this only to pop the 'distro' entry to the top of the generic list, ++ // effectively placing it to the root of the TOML document. ++ {"\\s+(distro[=\\s\"]+[^\"]+[\\s\"]+)", generic, NULL}, ++ {NULL, NULL, NULL} ++ }; ++ ++ for (int i = 0; tab[i].pattern != NULL; i++) { ++ tab[i].re = pcre_compile(tab[i].pattern, PCRE_UTF8, &err, &errofs, NULL); ++ if (tab[i].re == NULL) { ++ dE("Unable to compile /%s/ regex pattern, pcre_compile() returned error (offset: %d): '%s'.\n", tab[i].pattern, errofs, err); ++ ret = 1; ++ goto exit; ++ } ++ } ++ ++ const size_t fix_text_len = strlen(fix_text); ++ size_t start_offset = 0; ++ int ovector[6] = {0}; ++ ++ for (int i = 0; tab[i].pattern != NULL; i++) { ++ while (true) { ++ const int match = pcre_exec(tab[i].re, NULL, fix_text, fix_text_len, start_offset, ++ 0, ovector, sizeof(ovector) / sizeof(ovector[0])); ++ if (match == -1) ++ break; ++ ++ if (match != 2) { ++ dE("Expected 1 capture group matches per entry. Found %i!", match - 1); ++ ret = 1; ++ goto exit; ++ } ++ ++ char *val = malloc((ovector[3] - ovector[2] + 1) * sizeof(char)); ++ memcpy(val, &fix_text[ovector[2]], ovector[3] - ovector[2]); ++ val[ovector[3] - ovector[2]] = '\0'; ++ ++ if (!oscap_list_contains(kernel_append, val, (oscap_cmp_func) oscap_streq)) { ++ oscap_list_prepend(tab[i].list, val); ++ } else { ++ free(val); ++ } ++ ++ start_offset = ovector[1]; ++ } ++ } ++ ++ if (start_offset < fix_text_len-1) { ++ oscap_list_add(generic, strdup(fix_text + start_offset)); ++ } ++ ++exit: ++ for (int i = 0; tab[i].pattern != NULL; i++) ++ pcre_free(tab[i].re); ++ ++ return ret; ++} + + static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *variables, struct oscap_list *tasks) + { +@@ -793,6 +865,18 @@ static int _xccdf_policy_rule_generate_fix(struct xccdf_policy *policy, struct x + return ret; + } + ++static int _xccdf_policy_rule_generate_blueprint_fix(struct xccdf_policy *policy, struct xccdf_rule *rule, const char *template, struct oscap_list *generic, struct oscap_list *services_enable, struct oscap_list *services_disable, struct oscap_list *kernel_append) ++{ ++ char *fix_text = NULL; ++ int ret = _xccdf_policy_rule_get_fix_text(policy, rule, template, &fix_text); ++ if (fix_text == NULL) { ++ return ret; ++ } ++ ret = _parse_blueprint_fix(fix_text, generic, services_enable, services_disable, kernel_append); ++ free(fix_text); ++ return ret; ++} ++ + static int _xccdf_policy_rule_generate_ansible_fix(struct xccdf_policy *policy, struct xccdf_rule *rule, const char *template, struct oscap_list *variables, struct oscap_list *tasks) + { + char *fix_text = NULL; +@@ -914,25 +998,45 @@ static char *_comment_multiline_text(char *text) + static int _write_script_header_to_fd(struct xccdf_policy *policy, struct xccdf_result *result, const char *sys, int output_fd) + { + if (!(oscap_streq(sys, "") || oscap_streq(sys, "urn:xccdf:fix:script:sh") || oscap_streq(sys, "urn:xccdf:fix:commands") || +- oscap_streq(sys, "urn:xccdf:fix:script:ansible"))) ++ oscap_streq(sys, "urn:xccdf:fix:script:ansible") || oscap_streq(sys, "urn:redhat:osbuild:blueprint"))) + return 0; // no header required + +- const bool ansible_script = strcmp(sys, "urn:xccdf:fix:script:ansible") == 0; +- const char *how_to_apply = ansible_script ? +- "# $ ansible-playbook -i \"localhost,\" -c local playbook.yml\n" +- "# $ ansible-playbook -i \"192.168.1.155,\" playbook.yml\n" +- "# $ ansible-playbook -i inventory.ini playbook.yml" : +- "# $ sudo ./remediation-script.sh"; + const char *oscap_version = oscap_get_version(); +- const char *format = ansible_script ? "ansible" : "bash"; +- const char *remediation_type = ansible_script ? "Ansible Playbook" : "Bash Remediation Script"; +- const char *shebang_with_newline = ansible_script ? "" : "#!/usr/bin/env bash\n"; ++ char *how_to_apply = ""; ++ char *format = (char *)sys; ++ char *remediation_type = "Unknown"; ++ char *shebang_with_newline = ""; ++ ++ if (oscap_streq(sys, "urn:xccdf:fix:script:ansible")) { ++ how_to_apply = "# $ ansible-playbook -i \"localhost,\" -c local playbook.yml\n" ++ "# $ ansible-playbook -i \"192.168.1.155,\" playbook.yml\n" ++ "# $ ansible-playbook -i inventory.ini playbook.yml"; ++ format = "ansible"; ++ remediation_type = "Ansible Playbook"; ++ } ++ ++ if (oscap_streq(sys, "urn:redhat:osbuild:blueprint")) { ++ how_to_apply = "# composer-cli blueprints push blueprint.toml"; ++ format = "blueprint"; ++ remediation_type = "Blueprint"; ++ } ++ ++ if (oscap_streq(sys, "") || oscap_streq(sys, "urn:xccdf:fix:script:sh") || oscap_streq(sys, "urn:xccdf:fix:commands")) { ++ how_to_apply = "# $ sudo ./remediation-script.sh"; ++ format = "bash"; ++ remediation_type = "Bash Remediation Script"; ++ shebang_with_newline = "#!/usr/bin/env bash\n"; ++ } + + char *fix_header; + + struct xccdf_profile *profile = xccdf_policy_get_profile(policy); + const char *profile_id = xccdf_profile_get_id(profile); + ++ struct xccdf_benchmark *benchmark = xccdf_policy_get_benchmark(policy); ++ const char *benchmark_version_info = benchmark ? xccdf_benchmark_get_version(benchmark) : "Unknown"; ++ const char *benchmark_id = benchmark ? xccdf_benchmark_get_id(benchmark) : "Unknown"; ++ + // Title + struct oscap_text_iterator *title_iterator = xccdf_profile_get_title(profile); + char *raw_profile_title = oscap_textlist_get_preferred_plaintext(title_iterator, NULL); +@@ -942,11 +1046,6 @@ static int _write_script_header_to_fd(struct xccdf_policy *policy, struct xccdf_ + + if (result == NULL) { + // Profile-based remediation fix +- struct xccdf_benchmark *benchmark = xccdf_policy_get_benchmark(policy); +- if (benchmark == NULL) { +- free(profile_title); +- return 1; +- } + // Description + struct oscap_text_iterator *description_iterator = xccdf_profile_get_description(profile); + char *profile_description = description_iterator != NULL ? +@@ -955,10 +1054,8 @@ static int _write_script_header_to_fd(struct xccdf_policy *policy, struct xccdf_ + char *commented_profile_description = _comment_multiline_text(profile_description); + free(profile_description); + +- const char *benchmark_version_info = xccdf_benchmark_get_version(benchmark); +- const char *benchmark_id = xccdf_benchmark_get_id(benchmark); +- const struct xccdf_version_info *xccdf_version = xccdf_benchmark_get_schema_version(benchmark); +- const char *xccdf_version_name = xccdf_version_info_get_version(xccdf_version); ++ const struct xccdf_version_info *xccdf_version = benchmark ? xccdf_benchmark_get_schema_version(benchmark) : NULL; ++ const char *xccdf_version_name = xccdf_version ? xccdf_version_info_get_version(xccdf_version) : "Unknown"; + + fix_header = oscap_sprintf( + "%s" +@@ -1026,9 +1123,8 @@ static int _write_script_header_to_fd(struct xccdf_policy *policy, struct xccdf_ + result_id, format, remediation_type, remediation_type, how_to_apply + ); + } +- free(profile_title); + +- if (ansible_script) { ++ if (oscap_streq(sys, "urn:xccdf:fix:script:ansible")) { + char *ansible_fix_header = oscap_sprintf( + "---\n" + "%s\n" +@@ -1036,9 +1132,85 @@ static int _write_script_header_to_fd(struct xccdf_policy *policy, struct xccdf_ + fix_header); + free(fix_header); + return _write_text_to_fd_and_free(output_fd, ansible_fix_header); ++ } else if (oscap_streq(sys, "urn:redhat:osbuild:blueprint")) { ++ char *blueprint_fix_header = oscap_sprintf( ++ "%s" ++ "name = \"%s\"\n" ++ "description = \"%s\"\n" ++ "version = \"%s\"\n", ++ fix_header, profile_id, profile_title, benchmark_version_info); ++ free(fix_header); ++ return _write_text_to_fd_and_free(output_fd, blueprint_fix_header); + } else { + return _write_text_to_fd_and_free(output_fd, fix_header); + } ++ ++ free(profile_title); ++} ++ ++static int _xccdf_policy_generate_fix_blueprint(struct oscap_list *rules_to_fix, struct xccdf_policy *policy, const char *sys, int output_fd) ++{ ++ int ret = 0; ++ struct oscap_list *generic = oscap_list_new(); ++ struct oscap_list *services_enable = oscap_list_new(); ++ struct oscap_list *services_disable = oscap_list_new(); ++ struct oscap_list *kernel_append = oscap_list_new(); ++ 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_blueprint_fix(policy, rule, sys, generic, services_enable, services_disable, kernel_append); ++ if (ret != 0) ++ break; ++ } ++ oscap_iterator_free(rules_to_fix_it); ++ ++ struct oscap_iterator *generic_it = oscap_iterator_new(generic); ++ while(oscap_iterator_has_more(generic_it)) { ++ char *var_line = (char *) oscap_iterator_next(generic_it); ++ _write_text_to_fd(output_fd, var_line); ++ } ++ _write_text_to_fd(output_fd, "\n"); ++ oscap_iterator_free(generic_it); ++ oscap_list_free(generic, free); ++ ++ _write_text_to_fd(output_fd, "[customizations.kernel]\nappend = \""); ++ struct oscap_iterator *kernel_append_it = oscap_iterator_new(kernel_append); ++ while(oscap_iterator_has_more(kernel_append_it)) { ++ char *var_line = (char *) oscap_iterator_next(kernel_append_it); ++ _write_text_to_fd(output_fd, var_line); ++ if (oscap_iterator_has_more(kernel_append_it)) ++ _write_text_to_fd(output_fd, " "); ++ } ++ _write_text_to_fd(output_fd, "\"\n\n"); ++ oscap_iterator_free(kernel_append_it); ++ oscap_list_free(kernel_append, free); ++ ++ _write_text_to_fd(output_fd, "[customizations.services]\n"); ++ _write_text_to_fd(output_fd, "enabled = ["); ++ struct oscap_iterator *services_enable_it = oscap_iterator_new(services_enable); ++ while(oscap_iterator_has_more(services_enable_it)) { ++ char *var_line = (char *) oscap_iterator_next(services_enable_it); ++ _write_text_to_fd(output_fd, var_line); ++ if (oscap_iterator_has_more(services_enable_it)) ++ _write_text_to_fd(output_fd, ","); ++ } ++ _write_text_to_fd(output_fd, "]\n"); ++ oscap_iterator_free(services_enable_it); ++ oscap_list_free(services_enable, free); ++ ++ _write_text_to_fd(output_fd, "disabled = ["); ++ struct oscap_iterator *services_disable_it = oscap_iterator_new(services_disable); ++ while(oscap_iterator_has_more(services_disable_it)) { ++ char *var_line = (char *) oscap_iterator_next(services_disable_it); ++ _write_text_to_fd(output_fd, var_line); ++ if (oscap_iterator_has_more(services_disable_it)) ++ _write_text_to_fd(output_fd, ","); ++ } ++ _write_text_to_fd(output_fd, "]\n\n"); ++ oscap_iterator_free(services_disable_it); ++ oscap_list_free(services_disable, free); ++ ++ return ret; + } + + static int _xccdf_policy_generate_fix_ansible(struct oscap_list *rules_to_fix, struct xccdf_policy *policy, const char *sys, int output_fd) +@@ -1145,6 +1317,8 @@ int xccdf_policy_generate_fix(struct xccdf_policy *policy, struct xccdf_result * + // in Ansible we have to generate variables first and then tasks + if (strcmp(sys, "urn:xccdf:fix:script:ansible") == 0) { + 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 { + ret = _xccdf_policy_generate_fix_other(rules_to_fix, policy, sys, output_fd); + } +diff --git a/src/common/list.c b/src/common/list.c +index 2516d0f2f0..90381069f8 100644 +--- a/src/common/list.c ++++ b/src/common/list.c +@@ -66,6 +66,25 @@ bool oscap_list_add(struct oscap_list * list, void *value) + return true; + } + ++bool oscap_list_prepend(struct oscap_list * list, void *value) ++{ ++ __attribute__nonnull__(list); ++ if (value == NULL) return false; ++ ++ struct oscap_list_item *item = malloc(sizeof(struct oscap_list_item)); ++ item->next = NULL; ++ item->data = value; ++ ++list->itemcount; ++ ++ if (list->first == NULL) { ++ list->last = list->first = item; ++ } else { ++ item->next = list->first; ++ list->first = item; ++ } ++ return true; ++} ++ + bool oscap_list_push(struct oscap_list *list, void *value) + { + return oscap_list_add(list,value); +diff --git a/src/common/list.h b/src/common/list.h +index 7a0694dc8a..3179c514f0 100644 +--- a/src/common/list.h ++++ b/src/common/list.h +@@ -62,6 +62,7 @@ struct oscap_list *oscap_list_new(void); + void oscap_create_lists(struct oscap_list **first, ...); + bool oscap_list_add(struct oscap_list *list, void *value); + bool oscap_list_push(struct oscap_list *list, void *value); ++bool oscap_list_prepend(struct oscap_list *list, void *value); + bool oscap_list_pop(struct oscap_list *list, oscap_destruct_func destructor); + bool oscap_list_remove(struct oscap_list *list, void *value, oscap_cmp_func compare, oscap_destruct_func destructor); + struct oscap_list *oscap_list_clone(const struct oscap_list * list, oscap_clone_func cloner); +diff --git a/tests/API/XCCDF/unittests/CMakeLists.txt b/tests/API/XCCDF/unittests/CMakeLists.txt +index 52645834c4..9c17ebb78a 100644 +--- a/tests/API/XCCDF/unittests/CMakeLists.txt ++++ b/tests/API/XCCDF/unittests/CMakeLists.txt +@@ -75,6 +75,7 @@ add_oscap_test("test_single_rule_stigw.sh") + add_oscap_test("test_remediation_simple.sh") + add_oscap_test("test_remediation_offline.sh") + add_oscap_test("test_remediation_metadata.sh") ++add_oscap_test("test_remediation_blueprint.sh") + add_oscap_test("test_remediation_bad_fix.sh") + add_oscap_test("test_remediation_subs_plain_text.sh") + add_oscap_test("test_remediation_subs_plain_text_empty.sh") +diff --git a/tests/API/XCCDF/unittests/test_remediation_blueprint.sh b/tests/API/XCCDF/unittests/test_remediation_blueprint.sh +new file mode 100755 +index 0000000000..7c79822529 +--- /dev/null ++++ b/tests/API/XCCDF/unittests/test_remediation_blueprint.sh +@@ -0,0 +1,27 @@ ++#!/usr/bin/env bash ++. $builddir/tests/test_common.sh ++ ++set -e ++set -o pipefail ++ ++name=$(basename $0 .sh) ++result=$(make_temp_file /tmp ${name}.out) ++stderr=$(make_temp_file /tmp ${name}.out) ++ ++ret=0 ++ ++input_xml="$srcdir/${name}.xccdf.xml" ++valid_toml="$srcdir/${name}.toml" ++ ++echo "Stderr file = $stderr" ++echo "Result file = $result" ++[ -f $stderr ]; [ ! -s $stderr ]; :> $stderr ++ ++# The $valid_toml file was generated without ' # This file was generated by OpenSCAP 1.3.5 using:' line ++# to make the test independent from the scanner version. We have to filter this line from the output as well. ++ ++$OSCAP xccdf generate fix --fix-type blueprint --profile 'common' "$input_xml" | grep -v "OpenSCAP" > "$result" ++ ++diff $valid_toml $result ++ ++rm "$result" +diff --git a/tests/API/XCCDF/unittests/test_remediation_blueprint.toml b/tests/API/XCCDF/unittests/test_remediation_blueprint.toml +new file mode 100644 +index 0000000000..e189adca9d +--- /dev/null ++++ b/tests/API/XCCDF/unittests/test_remediation_blueprint.toml +@@ -0,0 +1,45 @@ ++############################################################################### ++# ++# Blueprint for Profile title on one line ++# ++# Profile Description: ++# Profile description ++# ++# Profile ID: xccdf_moc.elpmaxe.www_profile_common ++# Benchmark ID: xccdf_moc.elpmaxe.www_benchmark_test ++# Benchmark Version: 1.0 ++# XCCDF Version: 1.2 ++# ++# $ oscap xccdf generate fix --profile xccdf_moc.elpmaxe.www_profile_common --fix-type blueprint xccdf-file.xml ++# ++# It attempts to fix every selected rule, even if the system is already compliant. ++# ++# How to apply this Blueprint: ++# composer-cli blueprints push blueprint.toml ++# ++############################################################################### ++ ++name = "xccdf_moc.elpmaxe.www_profile_common" ++description = "Profile title on one line" ++version = "1.0" ++distro = rhel-80 ++ ++[[packages]] ++name = "aide" ++version = "*" ++ ++[[customizations.filesystem]] ++mountpoint = "/home" ++size = 1 ++ ++[[customizations.filesystem]] ++mountpoint = "/tmp" ++size = 2 ++ ++[customizations.kernel] ++append = "foo=bar audit=1" ++ ++[customizations.services] ++enabled = ["sshd","usbguard"] ++disabled = ["kdump"] ++ +diff --git a/tests/API/XCCDF/unittests/test_remediation_blueprint.xccdf.xml b/tests/API/XCCDF/unittests/test_remediation_blueprint.xccdf.xml +new file mode 100644 +index 0000000000..e685620dac +--- /dev/null ++++ b/tests/API/XCCDF/unittests/test_remediation_blueprint.xccdf.xml +@@ -0,0 +1,102 @@ ++ ++ ++ accepted ++ 1.0 ++ ++ Profile title on one line ++ Profile description ++ ++