diff --git a/src/XCCDF_POLICY/xccdf_policy_priv.h b/src/XCCDF_POLICY/xccdf_policy_priv.h index a27dcc7a03..4be667ed8e 100644 --- a/src/XCCDF_POLICY/xccdf_policy_priv.h +++ b/src/XCCDF_POLICY/xccdf_policy_priv.h @@ -95,6 +95,18 @@ struct xccdf_policy { */ int xccdf_policy_resolve_fix_substitution(struct xccdf_policy *policy, struct xccdf_fix *fix, struct xccdf_rule_result *rule_result, struct xccdf_result *test_result); +/** + * Resolve text substitution in given fix element containing Ansible remediation. Use given xccdf_policy settings + * for resolving. + * @memberof xccdf_policy + * @param policy XCCDF policy used for substitution + * @param fix a fix element to modify + * @param rule_result the rule-result for substitution instnace in fix + * @param test_result the TestResult for xccdf:fact resolution + * @returns 0 on success, 1 on failure, other value indicate warning + */ +int xccdf_policy_resolve_fix_substitution_ansible(struct xccdf_policy *policy, struct xccdf_fix *fix, struct xccdf_rule_result *rule_result, struct xccdf_result *test_result); + /** * Execute fix element for a given rule-result. Or find suitable (most appropriate) fix * in the policy, assign it to the rule-result and execute. diff --git a/src/XCCDF_POLICY/xccdf_policy_remediate.c b/src/XCCDF_POLICY/xccdf_policy_remediate.c index 5e4a0b1116..b5982b194b 100644 --- a/src/XCCDF_POLICY/xccdf_policy_remediate.c +++ b/src/XCCDF_POLICY/xccdf_policy_remediate.c @@ -48,6 +48,7 @@ #include "xccdf_policy_model_priv.h" #include "public/xccdf_policy.h" #include "oscap_helpers.h" +#include "xccdf_benchmark.h" struct kickstart_commands { struct oscap_list *package_install; @@ -764,12 +765,12 @@ static inline int _parse_blueprint_fix(const char *fix_text, struct blueprint_cu return ret; } -static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *variables, struct oscap_list *tasks) +static inline int _parse_ansible_fix(struct xccdf_policy *policy, const char *fix_text, struct oscap_list *variables, struct oscap_list *tasks) { // TODO: Tolerate different indentation styles in this regex const char *pattern = "- name: XCCDF Value [^ ]+ # promote to variable\n set_fact:\n" - " ([^:]+): (.+)\n tags:\n - always\n"; + " ([^:]+): (!!str )?(.+)\n tags:\n - always\n"; char *err; int errofs; @@ -783,11 +784,11 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va // ovector sizing: // 2 elements are used for the whole needle, - // 4 elements are used for the 2 capture groups + // 6 elements are used for the 3 capture groups // pcre documentation says we should allocate a third extra for additional // workspace. - // (2 + 4) * (3 / 2) = 9 - int ovector[9]; + // (2 + 6) * (3 / 2) = 12 + int ovector[12]; const size_t fix_text_len = strlen(fix_text); int start_offset = 0; @@ -796,8 +797,8 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va 0, ovector, sizeof(ovector) / sizeof(ovector[0])); if (match == -1) break; - if (match != 3) { - dE("Expected 2 capture group matches per XCCDF variable. Found %i!", + if (match != 4) { + dE("Expected 3 capture group matches per XCCDF variable. Found %i!", match - 1); oscap_pcre_free(re); return 1; @@ -806,18 +807,44 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va // ovector[0] and [1] hold the start and end of the whole needle match // ovector[2] and [3] hold the start and end of the first capture group // ovector[4] and [5] hold the start and end of the second capture group + // ovector[6] and [7] hold the start and end of the third capture group char *variable_name = malloc((ovector[3] - ovector[2] + 1) * sizeof(char)); memcpy(variable_name, &fix_text[ovector[2]], ovector[3] - ovector[2]); variable_name[ovector[3] - ovector[2]] = '\0'; - char *variable_value = malloc((ovector[5] - ovector[4] + 1) * sizeof(char)); - memcpy(variable_value, &fix_text[ovector[4]], ovector[5] - ovector[4]); - variable_value[ovector[5] - ovector[4]] = '\0'; + char *cast = malloc((ovector[5] - ovector[4] + 1) * sizeof(char)); + memcpy(cast, &fix_text[ovector[4]], ovector[5] - ovector[4]); + cast[ovector[5] - ovector[4]] = '\0'; - char *var_line = oscap_sprintf(" %s: %s\n", variable_name, variable_value); + char *variable_id = malloc((ovector[7] - ovector[6] + 1) * sizeof(char)); + memcpy(variable_id, &fix_text[ovector[6]], ovector[7] - ovector[6]); + variable_id[ovector[7] - ovector[6]] = '\0'; + + char *variable_value = NULL; + struct xccdf_item *item = xccdf_benchmark_get_item(xccdf_policy_get_benchmark(policy), variable_id); + if (item == NULL) { + dI("Variable not found: %s", variable_id); + variable_value = strdup(variable_id); + } else { + variable_value = strdup(xccdf_policy_get_value_of_item(policy, item)); + } + free(variable_id); + + char *var_line; + if (strchr(variable_value, '\n') != NULL) { + /* The value contains a multiline string. To ensure a valid YAML output + we need to put is as scalar block and indent it.*/ + char *indented_variable_value = oscap_indent(variable_value, 6); + const char *terminator = oscap_str_endswith(indented_variable_value, "\n") ? "" : "\n"; + var_line = oscap_sprintf(" %s: %s|\n%s%s", variable_name, cast, indented_variable_value, terminator); + free(indented_variable_value); + } else { + var_line = oscap_sprintf(" %s: %s%s\n", variable_name, cast, variable_value); + } free(variable_name); free(variable_value); + free(cast); if (!oscap_list_contains(variables, var_line, (oscap_cmp_func) oscap_streq)) { oscap_list_add(variables, var_line); @@ -829,7 +856,10 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va char *remediation_part = malloc((length_between_matches + 1) * sizeof(char)); memcpy(remediation_part, &fix_text[start_offset], length_between_matches); remediation_part[length_between_matches] = '\0'; - oscap_list_add(tasks, remediation_part); + oscap_trim(remediation_part); + if (strlen(remediation_part) > 0) { + oscap_list_add(tasks, remediation_part); + } start_offset = ovector[1]; // next time start after the entire pattern } @@ -838,7 +868,10 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va char *remediation_part = malloc((fix_text_len - start_offset + 1) * sizeof(char)); memcpy(remediation_part, &fix_text[start_offset], fix_text_len - start_offset); remediation_part[fix_text_len - start_offset] = '\0'; - oscap_list_add(tasks, remediation_part); + oscap_trim(remediation_part); + if (strlen(remediation_part) > 0) { + oscap_list_add(tasks, remediation_part); + } } oscap_pcre_free(re); @@ -863,7 +896,12 @@ static int _xccdf_policy_rule_get_fix_text(struct xccdf_policy *policy, struct x // Process Text Substitute within the fix struct xccdf_fix *cfix = xccdf_fix_clone(fix); - int res = xccdf_policy_resolve_fix_substitution(policy, cfix, NULL, NULL); + int res = 0; + if (strcmp(template, "urn:xccdf:fix:script:ansible") == 0) { + res = xccdf_policy_resolve_fix_substitution_ansible(policy, cfix, NULL, NULL); + } else { + res = xccdf_policy_resolve_fix_substitution(policy, cfix, NULL, NULL); + } if (res != 0) { oscap_seterr(OSCAP_EFAMILY_OSCAP, "A fix for Rule/@id=\"%s\" was skipped: Text substitution failed.", xccdf_rule_get_id(rule)); @@ -1128,7 +1166,7 @@ static int _xccdf_policy_rule_generate_ansible_fix(struct xccdf_policy *policy, if (fix_text == NULL) { return ret; } - ret = _parse_ansible_fix(fix_text, variables, tasks); + ret = _parse_ansible_fix(policy, fix_text, variables, tasks); free(fix_text); return ret; } diff --git a/src/XCCDF_POLICY/xccdf_policy_substitute.c b/src/XCCDF_POLICY/xccdf_policy_substitute.c index 1ea9905f3d..36078a9477 100644 --- a/src/XCCDF_POLICY/xccdf_policy_substitute.c +++ b/src/XCCDF_POLICY/xccdf_policy_substitute.c @@ -192,7 +192,34 @@ static int _xccdf_text_substitution_cb(xmlNode **node, void *user_data) } } -int xccdf_policy_resolve_fix_substitution(struct xccdf_policy *policy, struct xccdf_fix *fix, struct xccdf_rule_result *rule_result, struct xccdf_result *test_result) +static int _xccdf_text_substitution_cb_ansible(xmlNode **node, void *user_data) +{ + if (node == NULL || *node == NULL || user_data == NULL) + return 1; + + xmlNode *cur = *node; + if (!oscap_streq((const char *) cur->name, "sub") || !xccdf_is_supported_namespace(cur->ns)) + return 0; + + if (cur->children != NULL) + dW("The xccdf:sub element SHALL NOT have any content."); + + char *sub_idref = (char *) xmlGetProp(cur, BAD_CAST "idref"); + if (sub_idref == NULL || *sub_idref == '\0') { + oscap_seterr(OSCAP_EFAMILY_XCCDF, "The xccdf:sub MUST have a single @idref attribute."); + free(sub_idref); + return 2; + } + + xmlNode *new_node = xmlNewText(BAD_CAST sub_idref); + xmlReplaceNode(cur, new_node); + xmlFreeNode(cur); + *node = new_node; + free(sub_idref); + return 0; +} + +static int _xccdf_policy_resolve_fix_substitution_impl(struct xccdf_policy *policy, struct xccdf_fix *fix, struct xccdf_rule_result *rule_result, struct xccdf_result *test_result, int (*callback)(xmlNode **, void *)) { struct _xccdf_text_substitution_data data; data.policy = policy; @@ -200,13 +227,23 @@ int xccdf_policy_resolve_fix_substitution(struct xccdf_policy *policy, struct xc data.rule_result = rule_result; char *result = NULL; - int res = xml_iterate_dfs(xccdf_fix_get_content(fix), &result, _xccdf_text_substitution_cb, &data); + int res = xml_iterate_dfs(xccdf_fix_get_content(fix), &result, callback, &data); if (res == 0) xccdf_fix_set_content(fix, result); free(result); return res; } +int xccdf_policy_resolve_fix_substitution(struct xccdf_policy *policy, struct xccdf_fix *fix, struct xccdf_rule_result *rule_result, struct xccdf_result *test_result) +{ + return _xccdf_policy_resolve_fix_substitution_impl(policy, fix, rule_result, test_result, _xccdf_text_substitution_cb); +} + +int xccdf_policy_resolve_fix_substitution_ansible(struct xccdf_policy *policy, struct xccdf_fix *fix, struct xccdf_rule_result *rule_result, struct xccdf_result *test_result) +{ + return _xccdf_policy_resolve_fix_substitution_impl(policy, fix, rule_result, test_result, _xccdf_text_substitution_cb_ansible); +} + char* xccdf_policy_substitute(const char *text, struct xccdf_policy *policy) { struct _xccdf_text_substitution_data data; data.policy = policy; diff --git a/tests/API/XCCDF/unittests/CMakeLists.txt b/tests/API/XCCDF/unittests/CMakeLists.txt index 674e2b29b1..8c33c52b5a 100644 --- a/tests/API/XCCDF/unittests/CMakeLists.txt +++ b/tests/API/XCCDF/unittests/CMakeLists.txt @@ -115,3 +115,4 @@ add_oscap_test("test_single_line_tailoring.sh") add_oscap_test("test_reference.sh") add_oscap_test("test_remediation_bootc.sh") add_oscap_test("openscap_2289_regression.sh") +add_oscap_test("test_multiline_string_in_ansible_remediation.sh") diff --git a/tests/API/XCCDF/unittests/test_ansible_yaml_block_scalar.playbook.yml b/tests/API/XCCDF/unittests/test_ansible_yaml_block_scalar.playbook.yml index dd02767390..095abaf4c7 100644 --- a/tests/API/XCCDF/unittests/test_ansible_yaml_block_scalar.playbook.yml +++ b/tests/API/XCCDF/unittests/test_ansible_yaml_block_scalar.playbook.yml @@ -34,4 +34,3 @@ - CCE-82462-3 - NIST-800-53-AU-2(a) - diff --git a/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation.sh b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation.sh new file mode 100755 index 0000000000..fb51197018 --- /dev/null +++ b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +. $builddir/tests/test_common.sh + +# Test XCCDF values with multiline strings are correctly processed when generating Ansible remediation Playbooks + +set -e +set -o pipefail + +ds="$srcdir/test_multiline_string_in_ansible_remediation_ds.xml" + +function test_oscap() { + local variant="$1" + local raw_output="$(mktemp)" + local no_header_output="$(mktemp)" + local stdout="$(mktemp)" + local stderr="$(mktemp)" + $OSCAP xccdf generate fix --profile "xccdf_com.example.www_profile_test_$variant" --fix-type ansible --output "$raw_output" "$ds" >"$stdout" 2>"$stderr" + [ -f "$stdout" ] + [ ! -s "$stdout" ] + [ -f "$stderr" ] + [ ! -s "$stderr" ] + sed '/^#/d' "$raw_output" > "$no_header_output" + diff -u "$no_header_output" "$srcdir/test_multiline_string_in_ansible_remediation_playbook_$variant.yml" + rm "$raw_output" + rm "$no_header_output" + rm "$stdout" + rm "$stderr" +} + +test_oscap "single_line_string" +test_oscap "multi_line_string" diff --git a/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_ds.xml b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_ds.xml new file mode 100644 index 0000000000..ec453fc34b --- /dev/null +++ b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_ds.xml @@ -0,0 +1,160 @@ + + + + 5.11 + 2009-01-12T10:41:00-05:00 + + + + + + PASS + pass + + + + + + + + FAIL + conditional fail + + + + + + + + + + + + + + + + + + + + + oval:x:var:1 + + + + + + + + + + + + 100 + + + + + + + + + accepted + 1.0 + + + xccdf_test_profile + This profile is for testing. + + + + + + xccdf_test_profile_multi_line_string + This profile is for testing multiline string in ansible remediation. + + + + + + + test value + foo + This is a test string without newlines. + This is a test string +with newlines + +and empty lines + +useful for things like: +- banners +- bash commands + +or other things that need to be indented. + + + + test value + foo + Very long string that should be converted to a multiline string. It's so long that it doesn't fit here. + Another very long string. +But this time it's a multiline string. + It looks much nicer + more readable + and easier to write. +That's it! + + + + Rule rule1 to test ansible variable www_value_val1 + - name: XCCDF Value www_value_val1 # promote to variable + set_fact: + www_value_val1: + tags: + - always + +- name: rule1 + ansible.builtin.copy: + dest: /foo/bar + content: "bar {{ www_value_val1 }}" + tags: + - foo + - bar + +- name: XCCDF Value www_value_nonexistent # promote to variable + set_fact: + www_value_nonexistent: "dummy value because the variable does not exist" + tags: + - always + + + + + + + + + Rule rule2 to test ansible variable www_value_val2 + - name: XCCDF Value www_value_val2 # promote to variable + set_fact: + www_value_val2: !!str + tags: + - always +- name: rule2 + ansible.builtin.copy: + dest: /foo/baz + content: "baz {{ www_value_val2 }}" + tags: + - foo + - baz + + + + + + + + diff --git a/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_playbook_multi_line_string.yml b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_playbook_multi_line_string.yml new file mode 100644 index 0000000000..092edd221e --- /dev/null +++ b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_playbook_multi_line_string.yml @@ -0,0 +1,41 @@ +--- + + +- hosts: all + vars: + www_value_val1: | + This is a test string + with newlines + + and empty lines + + useful for things like: + - banners + - bash commands + + or other things that need to be indented. + www_value_nonexistent: "dummy value because the variable does not exist" + www_value_val2: !!str | + Another very long string. + But this time it's a multiline string. + It looks much nicer + more readable + and easier to write. + That's it! + tasks: + - name: rule1 + ansible.builtin.copy: + dest: /foo/bar + content: "bar {{ www_value_val1 }}" + tags: + - foo + - bar + + - name: rule2 + ansible.builtin.copy: + dest: /foo/baz + content: "baz {{ www_value_val2 }}" + tags: + - foo + - baz + diff --git a/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_playbook_single_line_string.yml b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_playbook_single_line_string.yml new file mode 100644 index 0000000000..b415090eb1 --- /dev/null +++ b/tests/API/XCCDF/unittests/test_multiline_string_in_ansible_remediation_playbook_single_line_string.yml @@ -0,0 +1,25 @@ +--- + + +- hosts: all + vars: + www_value_val1: This is a test string without newlines. + www_value_nonexistent: "dummy value because the variable does not exist" + www_value_val2: !!str Very long string that should be converted to a multiline string. It's so long that it doesn't fit here. + tasks: + - name: rule1 + ansible.builtin.copy: + dest: /foo/bar + content: "bar {{ www_value_val1 }}" + tags: + - foo + - bar + + - name: rule2 + ansible.builtin.copy: + dest: /foo/baz + content: "baz {{ www_value_val2 }}" + tags: + - foo + - baz + diff --git a/xsl/xccdf-share.xsl b/xsl/xccdf-share.xsl index fb10132f43..c3aa3aacb7 100644 --- a/xsl/xccdf-share.xsl +++ b/xsl/xccdf-share.xsl @@ -358,10 +358,11 @@ Authors:

-            
+            
                 
                 
                 
+                
             
         
@@ -473,4 +474,116 @@ Authors: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (N/A) + + + + + + + + + + + + + | + # block scalar value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +