Skip to content

Commit 9af9a7e

Browse files
committed
Support XCCDF Values containing newlines in Ansible Playbooks
When generating Ansible Playbooks from SCAP content, the values of XCCDF Values are extracted from Ansible code and added to the `vars` key in the generated Playbook. However, if the value contains newlines, the generated Playbook is invalid, because the inserted string isn't properly indented. This patch adds support for multi-line values. If the value will contain newlines, it will be indented and inserted as a YAML scalar block.
1 parent 4c34b12 commit 9af9a7e

9 files changed

Lines changed: 362 additions & 18 deletions

src/XCCDF_POLICY/xccdf_policy_priv.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ struct xccdf_policy {
9595
*/
9696
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);
9797

98+
/**
99+
* Resolve text substitution in given fix element containing Ansible remediation. Use given xccdf_policy settings
100+
* for resolving.
101+
* @memberof xccdf_policy
102+
* @param policy XCCDF policy used for substitution
103+
* @param fix a fix element to modify
104+
* @param rule_result the rule-result for substitution instnace in fix
105+
* @param test_result the TestResult for xccdf:fact resolution
106+
* @returns 0 on success, 1 on failure, other value indicate warning
107+
*/
108+
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);
109+
98110
/**
99111
* Execute fix element for a given rule-result. Or find suitable (most appropriate) fix
100112
* in the policy, assign it to the rule-result and execute.

src/XCCDF_POLICY/xccdf_policy_remediate.c

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "xccdf_policy_model_priv.h"
4949
#include "public/xccdf_policy.h"
5050
#include "oscap_helpers.h"
51+
#include "xccdf_benchmark.h"
5152

5253
struct kickstart_commands {
5354
struct oscap_list *package_install;
@@ -764,12 +765,12 @@ static inline int _parse_blueprint_fix(const char *fix_text, struct blueprint_cu
764765
return ret;
765766
}
766767

767-
static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *variables, struct oscap_list *tasks)
768+
static inline int _parse_ansible_fix(struct xccdf_policy *policy, const char *fix_text, struct oscap_list *variables, struct oscap_list *tasks)
768769
{
769770
// TODO: Tolerate different indentation styles in this regex
770771
const char *pattern =
771772
"- name: XCCDF Value [^ ]+ # promote to variable\n set_fact:\n"
772-
" ([^:]+): (.+)\n tags:\n - always\n";
773+
" ([^:]+): (!!str )?(.+)\n tags:\n - always\n";
773774
char *err;
774775
int errofs;
775776

@@ -783,11 +784,11 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va
783784

784785
// ovector sizing:
785786
// 2 elements are used for the whole needle,
786-
// 4 elements are used for the 2 capture groups
787+
// 6 elements are used for the 3 capture groups
787788
// pcre documentation says we should allocate a third extra for additional
788789
// workspace.
789-
// (2 + 4) * (3 / 2) = 9
790-
int ovector[9];
790+
// (2 + 6) * (3 / 2) = 12
791+
int ovector[12];
791792

792793
const size_t fix_text_len = strlen(fix_text);
793794
int start_offset = 0;
@@ -796,8 +797,8 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va
796797
0, ovector, sizeof(ovector) / sizeof(ovector[0]));
797798
if (match == -1)
798799
break;
799-
if (match != 3) {
800-
dE("Expected 2 capture group matches per XCCDF variable. Found %i!",
800+
if (match != 4) {
801+
dE("Expected 3 capture group matches per XCCDF variable. Found %i!",
801802
match - 1);
802803
oscap_pcre_free(re);
803804
return 1;
@@ -806,18 +807,44 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va
806807
// ovector[0] and [1] hold the start and end of the whole needle match
807808
// ovector[2] and [3] hold the start and end of the first capture group
808809
// ovector[4] and [5] hold the start and end of the second capture group
810+
// ovector[6] and [7] hold the start and end of the third capture group
809811
char *variable_name = malloc((ovector[3] - ovector[2] + 1) * sizeof(char));
810812
memcpy(variable_name, &fix_text[ovector[2]], ovector[3] - ovector[2]);
811813
variable_name[ovector[3] - ovector[2]] = '\0';
812814

813-
char *variable_value = malloc((ovector[5] - ovector[4] + 1) * sizeof(char));
814-
memcpy(variable_value, &fix_text[ovector[4]], ovector[5] - ovector[4]);
815-
variable_value[ovector[5] - ovector[4]] = '\0';
815+
char *cast = malloc((ovector[5] - ovector[4] + 1) * sizeof(char));
816+
memcpy(cast, &fix_text[ovector[4]], ovector[5] - ovector[4]);
817+
cast[ovector[5] - ovector[4]] = '\0';
816818

817-
char *var_line = oscap_sprintf(" %s: %s\n", variable_name, variable_value);
819+
char *variable_id = malloc((ovector[7] - ovector[6] + 1) * sizeof(char));
820+
memcpy(variable_id, &fix_text[ovector[6]], ovector[7] - ovector[6]);
821+
variable_id[ovector[7] - ovector[6]] = '\0';
822+
823+
char *variable_value = NULL;
824+
struct xccdf_item *item = xccdf_benchmark_get_item(xccdf_policy_get_benchmark(policy), variable_id);
825+
if (item == NULL) {
826+
dI("Variable not found: %s", variable_id);
827+
variable_value = strdup(variable_id);
828+
} else {
829+
variable_value = strdup(xccdf_policy_get_value_of_item(policy, item));
830+
}
831+
free(variable_id);
832+
833+
char *var_line;
834+
if (strchr(variable_value, '\n') != NULL) {
835+
/* The value contains a multiline string. To ensure a valid YAML output
836+
we need to put is as scalar block and indent it.*/
837+
char *indented_variable_value = oscap_indent(variable_value, 6);
838+
const char *terminator = oscap_str_endswith(indented_variable_value, "\n") ? "" : "\n";
839+
var_line = oscap_sprintf(" %s: %s|\n%s%s", variable_name, cast, indented_variable_value, terminator);
840+
free(indented_variable_value);
841+
} else {
842+
var_line = oscap_sprintf(" %s: %s%s\n", variable_name, cast, variable_value);
843+
}
818844

819845
free(variable_name);
820846
free(variable_value);
847+
free(cast);
821848

822849
if (!oscap_list_contains(variables, var_line, (oscap_cmp_func) oscap_streq)) {
823850
oscap_list_add(variables, var_line);
@@ -829,7 +856,10 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va
829856
char *remediation_part = malloc((length_between_matches + 1) * sizeof(char));
830857
memcpy(remediation_part, &fix_text[start_offset], length_between_matches);
831858
remediation_part[length_between_matches] = '\0';
832-
oscap_list_add(tasks, remediation_part);
859+
oscap_trim(remediation_part);
860+
if (strlen(remediation_part) > 0) {
861+
oscap_list_add(tasks, remediation_part);
862+
}
833863

834864
start_offset = ovector[1]; // next time start after the entire pattern
835865
}
@@ -838,7 +868,10 @@ static inline int _parse_ansible_fix(const char *fix_text, struct oscap_list *va
838868
char *remediation_part = malloc((fix_text_len - start_offset + 1) * sizeof(char));
839869
memcpy(remediation_part, &fix_text[start_offset], fix_text_len - start_offset);
840870
remediation_part[fix_text_len - start_offset] = '\0';
841-
oscap_list_add(tasks, remediation_part);
871+
oscap_trim(remediation_part);
872+
if (strlen(remediation_part) > 0) {
873+
oscap_list_add(tasks, remediation_part);
874+
}
842875
}
843876

844877
oscap_pcre_free(re);
@@ -863,7 +896,12 @@ static int _xccdf_policy_rule_get_fix_text(struct xccdf_policy *policy, struct x
863896

864897
// Process Text Substitute within the fix
865898
struct xccdf_fix *cfix = xccdf_fix_clone(fix);
866-
int res = xccdf_policy_resolve_fix_substitution(policy, cfix, NULL, NULL);
899+
int res = 0;
900+
if (strcmp(template, "urn:xccdf:fix:script:ansible") == 0) {
901+
res = xccdf_policy_resolve_fix_substitution_ansible(policy, cfix, NULL, NULL);
902+
} else {
903+
res = xccdf_policy_resolve_fix_substitution(policy, cfix, NULL, NULL);
904+
}
867905
if (res != 0) {
868906
oscap_seterr(OSCAP_EFAMILY_OSCAP, "A fix for Rule/@id=\"%s\" was skipped: Text substitution failed.",
869907
xccdf_rule_get_id(rule));
@@ -1128,7 +1166,7 @@ static int _xccdf_policy_rule_generate_ansible_fix(struct xccdf_policy *policy,
11281166
if (fix_text == NULL) {
11291167
return ret;
11301168
}
1131-
ret = _parse_ansible_fix(fix_text, variables, tasks);
1169+
ret = _parse_ansible_fix(policy, fix_text, variables, tasks);
11321170
free(fix_text);
11331171
return ret;
11341172
}

src/XCCDF_POLICY/xccdf_policy_substitute.c

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,21 +192,58 @@ static int _xccdf_text_substitution_cb(xmlNode **node, void *user_data)
192192
}
193193
}
194194

195-
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)
195+
static int _xccdf_text_substitution_cb_ansible(xmlNode **node, void *user_data)
196+
{
197+
if (node == NULL || *node == NULL || user_data == NULL)
198+
return 1;
199+
200+
xmlNode *cur = *node;
201+
if (!oscap_streq((const char *) cur->name, "sub") || !xccdf_is_supported_namespace(cur->ns))
202+
return 0;
203+
204+
if (cur->children != NULL)
205+
dW("The xccdf:sub element SHALL NOT have any content.");
206+
207+
char *sub_idref = (char *) xmlGetProp(cur, BAD_CAST "idref");
208+
if (sub_idref == NULL || *sub_idref == '\0') {
209+
oscap_seterr(OSCAP_EFAMILY_XCCDF, "The xccdf:sub MUST have a single @idref attribute.");
210+
free(sub_idref);
211+
return 2;
212+
}
213+
214+
xmlNode *new_node = xmlNewText(BAD_CAST sub_idref);
215+
xmlReplaceNode(cur, new_node);
216+
xmlFreeNode(cur);
217+
*node = new_node;
218+
free(sub_idref);
219+
return 0;
220+
}
221+
222+
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 *))
196223
{
197224
struct _xccdf_text_substitution_data data;
198225
data.policy = policy;
199226
data.processing_type = _DOCUMENT_GENERATION_TYPE | _ASSESSMENT_TYPE;
200227
data.rule_result = rule_result;
201228

202229
char *result = NULL;
203-
int res = xml_iterate_dfs(xccdf_fix_get_content(fix), &result, _xccdf_text_substitution_cb, &data);
230+
int res = xml_iterate_dfs(xccdf_fix_get_content(fix), &result, callback, &data);
204231
if (res == 0)
205232
xccdf_fix_set_content(fix, result);
206233
free(result);
207234
return res;
208235
}
209236

237+
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)
238+
{
239+
return _xccdf_policy_resolve_fix_substitution_impl(policy, fix, rule_result, test_result, _xccdf_text_substitution_cb);
240+
}
241+
242+
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)
243+
{
244+
return _xccdf_policy_resolve_fix_substitution_impl(policy, fix, rule_result, test_result, _xccdf_text_substitution_cb_ansible);
245+
}
246+
210247
char* xccdf_policy_substitute(const char *text, struct xccdf_policy *policy) {
211248
struct _xccdf_text_substitution_data data;
212249
data.policy = policy;

tests/API/XCCDF/unittests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,4 @@ add_oscap_test("test_single_line_tailoring.sh")
115115
add_oscap_test("test_reference.sh")
116116
add_oscap_test("test_remediation_bootc.sh")
117117
add_oscap_test("openscap_2289_regression.sh")
118+
add_oscap_test("test_multiline_string_in_ansible_remediation.sh")

tests/API/XCCDF/unittests/test_ansible_yaml_block_scalar.playbook.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,3 @@
3434
- CCE-82462-3
3535
- NIST-800-53-AU-2(a)
3636

37-
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
. $builddir/tests/test_common.sh
3+
4+
# Test XCCDF values with multiline strings are correctly processed when generating Ansible remediation Playbooks
5+
6+
set -e
7+
set -o pipefail
8+
9+
ds="$srcdir/test_multiline_string_in_ansible_remediation_ds.xml"
10+
11+
function test_oscap() {
12+
local variant="$1"
13+
local raw_output="$(mktemp)"
14+
local no_header_output="$(mktemp)"
15+
local stdout="$(mktemp)"
16+
local stderr="$(mktemp)"
17+
$OSCAP xccdf generate fix --profile "xccdf_com.example.www_profile_test_$variant" --fix-type ansible --output "$raw_output" "$ds" >"$stdout" 2>"$stderr"
18+
[ -f "$stdout" ]
19+
[ ! -s "$stdout" ]
20+
[ -f "$stderr" ]
21+
[ ! -s "$stderr" ]
22+
sed '/^#/d' "$raw_output" > "$no_header_output"
23+
diff -u "$no_header_output" "$srcdir/test_multiline_string_in_ansible_remediation_playbook_$variant.yml"
24+
rm "$raw_output"
25+
rm "$no_header_output"
26+
rm "$stdout"
27+
rm "$stderr"
28+
}
29+
30+
test_oscap "single_line_string"
31+
test_oscap "multi_line_string"

0 commit comments

Comments
 (0)