Blob Blame History Raw
/*
 * Copyright 2013 Red Hat Inc., Durham, North Carolina.
 * All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>

#ifdef OSCAP_UNIX
#include <sys/wait.h>
#endif

#ifdef OS_WINDOWS
#include <io.h>
#else
#include <unistd.h>
#endif

#include <libxml/tree.h>
#include <pcre.h>

#include "XCCDF/item.h"
#include "common/_error.h"
#include "common/debug_priv.h"
#include "common/oscap_acquire.h"
#include "xccdf_policy_priv.h"
#include "xccdf_policy_model_priv.h"
#include "public/xccdf_policy.h"
#include "oscap_helpers.h"

static int _rule_add_info_message(struct xccdf_rule_result *rr, ...)
{
	va_list ap;
	const char *fmt;
	char *text;
	struct xccdf_message *msg;

	va_start(ap, rr);
	fmt = va_arg(ap, const char *);
	text = oscap_vsprintf(fmt, ap);
	va_end(ap);

	msg = xccdf_message_new();
	xccdf_message_set_content(msg, text);
	dI("[%s]->msg: %s", xccdf_rule_result_get_idref(rr), text);
	free(text);
	xccdf_message_set_severity(msg, XCCDF_MSG_INFO);
	xccdf_rule_result_add_message(rr, msg);
	return 0;
}

static inline bool _file_exists(const char *file)
{
	struct stat sb;
	return file != NULL && stat(file, &sb) == 0;
}

static int _write_text_to_fd(int output_fd, const char* text) {

	ssize_t written = 0;
	const ssize_t length = strlen(text);

	while (written < length) {
		ssize_t w = write(output_fd, text + written, length - written);
		if (w < 0)
			break;
		written += w;
	}

	return written != length;

}

static int _write_text_to_fd_and_free(int output_fd, char *text)
{
	const int ret = _write_text_to_fd(output_fd, text);
	free(text);
	return ret;
}

static int _write_remediation_to_fd_and_free(int output_fd, const char* template, char* text)
{
	if (oscap_streq(template, "urn:xccdf:fix:script:ansible")) {
		// Add required indentation in front of every single line

		const char delim = '\n';
		const char *indentation = "    ";

		char *current = text;
		char *next_delim = NULL;
		char *end = NULL;

		do {
			next_delim = strchr(current, delim);
			if (next_delim != NULL) {
				*next_delim = '\0';
			}

			// remove all trailing whitespaces
			size_t len = strlen(current);
			if (len > 0) {
				end = current + len - 1;
				while (isspace(*end)) {
					*end = '\0';
					if (end == current)
						break;
					end--;
				}
			}

			if (strlen(current) > 0) {
				// write indentation
				if (_write_text_to_fd(output_fd, indentation) != 0) {
					free(text);
					return 1;
				}
				if (_write_text_to_fd(output_fd, current) != 0) {
					free(text);
					return 1;
				}
			}
			if (_write_text_to_fd(output_fd, "\n") != 0) {
				free(text);
				return 1;
			}

			if (next_delim != NULL) {
				// text is NULL terminated to this is guaranteed to point to valid memory
				current = next_delim + 1;
			}
		} while (next_delim != NULL);

		if (_write_text_to_fd(output_fd, "\n") != 0) {
			free(text);
			return 1;
		}

		free(text);
		return 0;

	} else {
		// no extra processing is needed
		return _write_text_to_fd_and_free(output_fd, text);
	}
}
struct _interpret_map {
	const char *sys;
	const char *interpret;
};

typedef const char * (*_search_interpret_map_fn) (const char *, const struct _interpret_map *);

static const char *_search_interpret_map(const char *sys, const struct _interpret_map *map)
{
	const struct _interpret_map *mapptr;
	for (mapptr = map; mapptr->sys != NULL; ++mapptr)
		if (oscap_streq(mapptr->sys, sys))
			return mapptr->interpret;
	return NULL;
}

static const char *_get_supported_interpret(const char *sys, const struct _interpret_map *unused)
{
	static const struct _interpret_map _openscap_supported_interprets[] = {
		{"urn:xccdf:fix:commands",		"/bin/bash"},
		{"urn:xccdf:fix:script:sh",		"/bin/bash"},
		{"urn:xccdf:fix:script:perl",		"/usr/bin/perl"},
#ifdef PREFERRED_PYTHON_PATH
		{"urn:xccdf:fix:script:python",		PREFERRED_PYTHON_PATH},
#endif
#ifdef PYTHON2_PATH
		{"urn:xccdf:fix:script:python2",	PYTHON2_PATH},
#endif
#ifdef PYTHON3_PATH
		{"urn:xccdf:fix:script:python3",	PYTHON3_PATH},
#endif
		{"urn:xccdf:fix:script:csh",		"/bin/csh"},
		{"urn:xccdf:fix:script:tclsh",		"/usr/bin/tclsh"},
		{"urn:xccdf:fix:script:javascript",	"/usr/bin/js"},

		// Current Ansible remediations are only Ansible snippets and are
		// not runnable without header.
		// {"urn:xccdf:fix:script:ansible",	"/usr/bin/ansible-playbook"},
		{NULL,					NULL}
	};
	const char *interpret = _search_interpret_map(sys, _openscap_supported_interprets);
	return _file_exists(interpret) ? interpret : NULL;
}

static inline struct xccdf_rule *_lookup_rule_by_rule_result(const struct xccdf_policy *policy, const struct xccdf_rule_result *rr)
{
	const struct xccdf_benchmark *benchmark = xccdf_policy_get_benchmark(policy);
	if (benchmark == NULL)
		return NULL;
	return (struct xccdf_rule *) xccdf_benchmark_get_item(benchmark, xccdf_rule_result_get_idref(rr));
}

static inline bool _is_platform_applicable(struct xccdf_policy *policy, const char *platform)
{
	if (oscap_streq("", platform))
		return true;
	struct oscap_stringlist *platform_list = oscap_stringlist_new();
	oscap_stringlist_add_string(platform_list, platform);
	struct oscap_string_iterator *platform_it = oscap_stringlist_get_strings(platform_list);
	bool ret = xccdf_policy_model_platforms_are_applicable(xccdf_policy_get_model(policy), platform_it);
	oscap_string_iterator_free(platform_it);
	oscap_stringlist_free(platform_list);
	return ret;
}

static struct oscap_list *_get_fixes(struct xccdf_policy *policy, const struct xccdf_rule *rule)
{
	struct oscap_list *result = oscap_list_new();

	struct xccdf_fix_iterator *fix_it = xccdf_rule_get_fixes(rule);
	while (xccdf_fix_iterator_has_more(fix_it)) {
		struct xccdf_fix *fix = xccdf_fix_iterator_next(fix_it);
		oscap_list_add(result, fix);
	}
	xccdf_fix_iterator_free(fix_it);
	return result;
}

static struct oscap_list *_filter_fixes_by_applicability(struct xccdf_policy *policy, const struct xccdf_rule *rule)
{
	/* Filters out the fixes which are not applicable */
	struct oscap_list *result = oscap_list_new();
	if (!xccdf_policy_model_item_is_applicable(xccdf_policy_get_model(policy), (struct xccdf_item *) rule))
		/* The fix element is applicable only when the all the parent elements are. */
		return result;
	struct xccdf_fix_iterator *fix_it = xccdf_rule_get_fixes(rule);
	while (xccdf_fix_iterator_has_more(fix_it)) {
		struct xccdf_fix *fix = xccdf_fix_iterator_next(fix_it);
		const char *platform = xccdf_fix_get_platform(fix);
		if (_is_platform_applicable(policy, platform))
			oscap_list_add(result, fix);
	}
	xccdf_fix_iterator_free(fix_it);
	return result;
}

static struct oscap_list *_filter_fixes_by_system(struct oscap_list *fixes, _search_interpret_map_fn filter, const struct _interpret_map *allowed_systems)
{
	struct oscap_iterator *fix_it = oscap_iterator_new(fixes);
	while (oscap_iterator_has_more(fix_it)) {
		struct xccdf_fix *fix = (struct xccdf_fix *) oscap_iterator_next(fix_it);
		const char *sys = xccdf_fix_get_system(fix);
		if (sys == NULL)
			sys = "";
		if (filter(sys, allowed_systems) == NULL)
			oscap_iterator_detach(fix_it);
	}
	oscap_iterator_free(fix_it);
	return fixes;
}

static struct oscap_list *_filter_fixes_by_distruption_and_reboot(struct oscap_list *fixes)
{
	bool reboot = true;	// Let's assuming worse case and flip when fix/@rebot=false is found
	xccdf_level_t disruption = XCCDF_HIGH;

	struct oscap_iterator *fix_it = oscap_iterator_new(fixes);
	while (oscap_iterator_has_more(fix_it)) {
		struct xccdf_fix *fix = (struct xccdf_fix *) oscap_iterator_next(fix_it);
		if (!xccdf_fix_get_reboot(fix))
			reboot = false;
	}
	oscap_iterator_reset(fix_it);
	while (oscap_iterator_has_more(fix_it)) {
		struct xccdf_fix *fix = (struct xccdf_fix *) oscap_iterator_next(fix_it);
		if (reboot == false && xccdf_fix_get_reboot(fix)) {
			oscap_iterator_detach(fix_it);
		} else {
			xccdf_level_t dis = xccdf_fix_get_disruption(fix);
			if (dis == XCCDF_MEDIUM || dis == XCCDF_LOW)
				// Preferring "medium" and "low" over any other
				disruption = dis;
		}
	}
	if (disruption == XCCDF_MEDIUM || disruption == XCCDF_LOW) {
		oscap_iterator_reset(fix_it);
		while (oscap_iterator_has_more(fix_it)) {
			struct xccdf_fix *fix = (struct xccdf_fix *) oscap_iterator_next(fix_it);
			if (disruption != xccdf_fix_get_disruption(fix))
				oscap_iterator_detach(fix_it);
		}
	}
	oscap_iterator_free(fix_it);
	return fixes;
}

static inline struct xccdf_fix *_find_suitable_fix(struct xccdf_policy *policy, struct xccdf_rule_result *rr)
{
	/* In XCCDF 1.2, there is nothing like a default fix. However we use
	 * the following heuristics to find out some suitable fix:
	 * 	- remove fixes which are not appplicable (CPE)
	 * 	- remove fixes we cannot execute
	 * 	- choose fixes with the least disruption
	 * 	- choose fixes which do not require reboot
	 * 	- choose the first fix
	 */
	struct xccdf_fix *fix = NULL;
	const struct xccdf_rule *rule = _lookup_rule_by_rule_result(policy, rr);
	if (rule == NULL)
		return NULL;
	struct oscap_list *fixes = _filter_fixes_by_applicability(policy, rule);
	fixes = _filter_fixes_by_system(fixes, _get_supported_interpret, NULL);
	fixes = _filter_fixes_by_distruption_and_reboot(fixes);
	struct xccdf_fix_iterator *fix_it = oscap_iterator_new(fixes);
	if (xccdf_fix_iterator_has_more(fix_it))
		fix = xccdf_fix_iterator_next(fix_it);
	xccdf_fix_iterator_free(fix_it);
	oscap_list_free0(fixes);
	return fix;
}

static inline int _xccdf_fix_decode_xml(struct xccdf_fix *fix, char **result)
{
	/* We need to decode &amp; and similar sequences. That is a process reverse
	 * to the xmlEncodeSpecialChars()). Further we need to drop XML commentaries
	 * and expand CDATA blobs.
	 */
	*result = NULL;
	char *str = oscap_sprintf("<x xmlns:xhtml='http://www.w3.org/1999/xhtml'>%s</x>",
		xccdf_fix_get_content(fix));
        xmlDoc *doc = xmlReadMemory(str, strlen(str), NULL, NULL, XML_PARSE_RECOVER |
		XML_PARSE_NOERROR | XML_PARSE_NOWARNING | XML_PARSE_NONET | XML_PARSE_NSCLEAN);
	dI("Following script will be executed: '''%s'''", str);
	free(str);

        xmlBuffer *buff = xmlBufferCreate();
	xmlNodePtr child = xmlDocGetRootElement(doc)->children;
	while (child != NULL) {
		switch (child->type) {
		case XML_ELEMENT_NODE:{
			/* Remaining child elements are suspicious. Perhaps it is an unresolved
			 * substitution element The execution would be dangerous, i.e. bash could
			 * interpret < and > characters of the element as pipe commands. */
			xmlFreeDoc(doc);
			xmlBufferFree(buff);
			return 1;
			}; break;
		case XML_TEXT_NODE:
		case XML_CDATA_SECTION_NODE:{
			xmlNodeBufGetContent(buff, child);
			}; break;
		default:
			break;
		}
		child = child->next;
        }
	xmlFreeDoc(doc);
	*result = oscap_strdup((char *)xmlBufferContent(buff));
	xmlBufferFree(buff);
	return 0;
}

#if defined(unix) || defined(__unix__) || defined(__unix)
static inline int _xccdf_fix_execute(struct xccdf_rule_result *rr, struct xccdf_fix *fix)
{
	if (rr == NULL) {
		return 1;
	}

	if (fix == NULL || oscap_streq(xccdf_fix_get_content(fix), NULL)) {
		_rule_add_info_message(rr, "No fix available.");
		return 1;
	}
	
	const char *interpret = NULL;
	if ((interpret = _get_supported_interpret(xccdf_fix_get_system(fix), NULL)) == NULL) {
		_rule_add_info_message(rr, "Not supported xccdf:fix/@system='%s' or missing interpreter.",
				xccdf_fix_get_system(fix) == NULL ? "" : xccdf_fix_get_system(fix));
		return 1;
	}

	char *fix_text = NULL;
	if (_xccdf_fix_decode_xml(fix, &fix_text) != 0) {
		_rule_add_info_message(rr, "Fix element contains unresolved child elements.");
		return 1;
	}

	int result = 1;

	char *temp_dir = oscap_acquire_temp_dir();
	if (temp_dir == NULL)
		goto cleanup;
	// TODO: Directory and files shall be labeled with SELinux to prevent
	// confined processes with less priviledges to transit to oscap domain
	// and become basically unconfined.
	char *temp_file = NULL;
	int fd = oscap_acquire_temp_file(temp_dir, "fix-XXXXXXXX", &temp_file);
	if (fd == -1) {
		_rule_add_info_message(rr, "mkstemp failed: %s", strerror(errno));
		goto cleanup;
	}

	if (_write_text_to_fd(fd, fix_text) != 0) {
		_rule_add_info_message(rr, "Could not write to the temp file: %s", strerror(errno));
		(void) close(fd);
		goto cleanup;
	}

	if (close(fd) != 0)
		_rule_add_info_message(rr, "Could not close temp file: %s", strerror(errno));

	int pipefd[2];
	if (pipe(pipefd) == -1) {
		_rule_add_info_message(rr, "Could not create pipe: %s", strerror(errno));
		goto cleanup;
	}

	int fork_result = fork();
	if (fork_result >= 0) {
		/* fork succeded */
		if (fork_result == 0) {
			/* Execute fix and forward output to the parrent. */
			close(pipefd[0]);
			dup2(pipefd[1], fileno(stdout));
			dup2(pipefd[1], fileno(stderr));
			close(pipefd[1]);

			char *const argvp[3] = {
				(char *)interpret,
				temp_file,
				NULL
			};

			char *const envp[2] = {
				"PATH=/bin:/sbin:/usr/bin:/usr/sbin",
				NULL
			};

			execve(interpret, argvp, envp);
			/* Wow, execve returned. In this special case, we failed to execute the fix
			 * and we return 0 from function. At least the following error message will
			 * indicate the problem in xccdf:message. */
			printf("Error while executing fix script: execve returned: %s\n", strerror(errno));
			exit(42);
		} else {
			free(temp_file);
			close(pipefd[1]);
			char *stdout_buff = oscap_acquire_pipe_to_string(pipefd[0]);
			int wstatus;
			waitpid(fork_result, &wstatus, 0);
			_rule_add_info_message(rr, "Fix execution completed and returned: %d", WEXITSTATUS(wstatus));
			if (stdout_buff != NULL && stdout_buff[0] != '\0')
				_rule_add_info_message(rr, stdout_buff);
			free(stdout_buff);
			/* We return zero to indicate success. Rather than returning the exit code. */
			result = 0;
		}
	} else {
		_rule_add_info_message(rr, "Failed to fork. %s", strerror(errno));
		free(temp_file);
	}

cleanup:
	oscap_acquire_cleanup_dir(&temp_dir);
	free(fix_text);
	return result;
}
#else
static inline int _xccdf_fix_execute(struct xccdf_rule_result *rr, struct xccdf_fix *fix)
{
	if (rr == NULL) {
		return 1;
	}

	if (fix == NULL || oscap_streq(xccdf_fix_get_content(fix), NULL)) {
		_rule_add_info_message(rr, "No fix available.");
		return 1;
	} else {
		_rule_add_info_message(rr, "Cannot execute the fix script: not implemented");
	}
	
	return 1;
}
#endif

int xccdf_policy_rule_result_remediate(struct xccdf_policy *policy, struct xccdf_rule_result *rr, struct xccdf_fix *fix, struct xccdf_result *test_result)
{
	if (policy == NULL || rr == NULL)
		return 1;
	if (xccdf_rule_result_get_result(rr) != XCCDF_RESULT_FAIL)
		return 0;

	// if a miscellaneous error happens (fix unsuitable or if we want to skip it for any reason
	// we set misc_error to one, and the fix will be reported as error (and not skipped without log like before)
	int misc_error=0;

	if (fix == NULL) {
		fix = _find_suitable_fix(policy, rr);
		if (fix == NULL) {
			// We want to append xccdf:message about missing fix.
			_rule_add_info_message(rr, "No suitable fix found.");
			xccdf_rule_result_set_result(rr, XCCDF_RESULT_FAIL);
			misc_error=1;
		}
	}

	struct xccdf_check *check = NULL;
	struct xccdf_check_iterator *check_it = xccdf_rule_result_get_checks(rr);
	while (xccdf_check_iterator_has_more(check_it))
		check = xccdf_check_iterator_next(check_it);
	xccdf_check_iterator_free(check_it);

	if(misc_error == 0){
		/* Initialize the fix. */
		struct xccdf_fix *cfix = xccdf_fix_clone(fix);
		int res = xccdf_policy_resolve_fix_substitution(policy, cfix, rr, test_result);
		xccdf_rule_result_add_fix(rr, cfix);
		if (res != 0) {
			_rule_add_info_message(rr, "Fix execution was aborted: Text substitution failed.");
			xccdf_rule_result_set_result(rr, XCCDF_RESULT_ERROR);
			misc_error=1;
		}else{

			/* Execute the fix. */
			res = _xccdf_fix_execute(rr, cfix);
			if (res != 0) {
				_rule_add_info_message(rr, "Fix was not executed. Execution was aborted.");
				xccdf_rule_result_set_result(rr, XCCDF_RESULT_ERROR);
				misc_error=1;
			}
		}
	}

	/* We report rule during remediation even if fix isn't executed due to a miscellaneous error */
	int report = 0;
	struct xccdf_rule *rule = _lookup_rule_by_rule_result(policy, rr);
	if (rule == NULL) {
		// Sadly, we cannot handle this since b9d123d53140c6e369b7f2206e4e3e63dc556fd1.
		oscap_seterr(OSCAP_EFAMILY_OSCAP, "Could not find xccdf:Rule/@id=%s.", xccdf_rule_result_get_idref(rr));
	}
	else {
		report = xccdf_policy_report_cb(policy, XCCDF_POLICY_OUTCB_START, (void *) rule);
		if (report != 0)
			return report;
	}

	if(misc_error == 0){
		/* Verify fix if applied by calling OVAL again */
		if (check == NULL) {
			xccdf_rule_result_set_result(rr, XCCDF_RESULT_ERROR);
			_rule_add_info_message(rr, "Failed to verify applied fix: Missing xccdf:check.");
		} else {
			int new_result = xccdf_policy_check_evaluate(policy, check);
			if (new_result == XCCDF_RESULT_PASS)
				xccdf_rule_result_set_result(rr, XCCDF_RESULT_FIXED);
			else {
				xccdf_rule_result_set_result(rr, XCCDF_RESULT_ERROR);
				_rule_add_info_message(rr, "Failed to verify applied fix: Checking engine returns: %s",
					new_result <= 0 ? "internal error" : xccdf_test_result_type_get_text(new_result));
			}
		}
	}

	xccdf_rule_result_set_time_current(rr);
	return rule == NULL ? 0 : xccdf_policy_report_cb(policy, XCCDF_POLICY_OUTCB_END, (void *) rr);
}

int xccdf_policy_remediate(struct xccdf_policy *policy, struct xccdf_result *result)
{
	__attribute__nonnull__(result);
	struct xccdf_rule_result_iterator *rr_it = xccdf_result_get_rule_results(result);
	while (xccdf_rule_result_iterator_has_more(rr_it)) {
		struct xccdf_rule_result *rr = xccdf_rule_result_iterator_next(rr_it);
		xccdf_policy_rule_result_remediate(policy, rr, NULL, result);
	}
	xccdf_rule_result_iterator_free(rr_it);
	xccdf_result_set_end_time_current(result);
	return 0;
}

/* --- Follows functions for generating XCCDF:Fix script --- */
static const struct xccdf_fix *_find_fix_for_template(struct xccdf_policy *policy, struct xccdf_rule *rule, const char *template)
{
	struct xccdf_fix *fix = NULL;
	struct oscap_list *fixes = _get_fixes(policy, rule);

	if (template) {
		const struct _interpret_map map[] = {	{template, "Cloud!"},
							{NULL, NULL}};
		fixes = _filter_fixes_by_system(fixes, _search_interpret_map, map);
	}
	fixes = _filter_fixes_by_distruption_and_reboot(fixes);
	struct xccdf_fix_iterator *fix_it = oscap_iterator_new(fixes);
	if (xccdf_fix_iterator_has_more(fix_it))
		fix = xccdf_fix_iterator_next(fix_it);
	xccdf_fix_iterator_free(fix_it);
	oscap_list_free0(fixes);
	return fix;
}

static int _write_fix_header_to_fd(const char *sys, int output_fd, struct xccdf_rule *rule, unsigned int current, unsigned int total)
{
	if (oscap_streq(sys, "") || oscap_streq(sys, "urn:xccdf:fix:script:sh") || oscap_streq(sys, "urn:xccdf:fix:commands")) {
		char *fix_header = oscap_sprintf(
				"###############################################################################\n"
				"# BEGIN fix (%i / %i) for '%s'\n"
				"###############################################################################\n"
				"(>&2 echo \"Remediating rule %i/%i: '%s'\")\n",
				current, total, xccdf_rule_get_id(rule), current, total, xccdf_rule_get_id(rule));
		return _write_text_to_fd_and_free(output_fd, fix_header);
	} else {
		return 0;
	}
}

static int _write_fix_footer_to_fd(const char *sys, int output_fd, struct xccdf_rule *rule)
{
	if (oscap_streq(sys, "") || oscap_streq(sys, "urn:xccdf:fix:script:sh") || oscap_streq(sys, "urn:xccdf:fix:commands")) {
		char *fix_footer = oscap_sprintf("\n# END fix for '%s'\n\n", xccdf_rule_get_id(rule));
		return _write_text_to_fd_and_free(output_fd, fix_footer);
	} else {
		return 0;
	}
}

static int _write_fix_missing_warning_to_fd(const char *sys, int output_fd, struct xccdf_rule *rule)
{
	if (oscap_streq(sys, "") || oscap_streq(sys, "urn:xccdf:fix:script:sh") || oscap_streq(sys, "urn:xccdf:fix:commands")) {
		char *fix_footer = oscap_sprintf("(>&2 echo \"FIX FOR THIS RULE '%s' IS MISSING!\")\n", xccdf_rule_get_id(rule));
		return _write_text_to_fd_and_free(output_fd, fix_footer);
	} else {
		return 0;
	}
}


static inline int _parse_ansible_fix(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";
	const char *err;
	int errofs;

	pcre *re = pcre_compile(pattern, PCRE_UTF8, &err, &errofs, NULL);
	if (re == NULL) {
		dE("Unable to compile regex pattern, "
				"pcre_compile() returned error (offset: %d): '%s'.\n", errofs, err);
		return 1;
	}

	// ovector sizing:
	// 2 elements are used for the whole needle,
	// 4 elements are used for the 2 capture groups
	// pcre documentation says we should allocate a third extra for additional
	// workspace.
	// (2 + 4) * (3 / 2) = 9
	int ovector[9];

	const size_t fix_text_len = strlen(fix_text);
	int start_offset = 0;
	while (true) {
		const int match = pcre_exec(re, NULL, fix_text, fix_text_len, start_offset,
				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!",
				match - 1);
			pcre_free(re);
			return 1;
		}

		// 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
		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 *var_line = oscap_sprintf("    %s: %s\n", variable_name, variable_value);

		free(variable_name);
		free(variable_value);

		if (!oscap_list_contains(variables, var_line, (oscap_cmp_func) oscap_streq)) {
			oscap_list_add(variables, var_line);
		}

		// Remarks: ovector doesn't contain values relative to start_offset, it contains
		// absolute indices of fix_text.
		const int length_between_matches = ovector[0] - start_offset;
		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);

		start_offset = ovector[1]; // next time start after the entire pattern
	}

	if (fix_text_len - start_offset > 0) {
		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);
	}

	pcre_free(re);
	return 0;
}

static int _xccdf_policy_rule_get_fix_text(struct xccdf_policy *policy, struct xccdf_rule *rule, const char *template, char **fix_text)
{
	// Ensure that given Rule is selected and applicable (CPE).
	const bool is_selected = xccdf_policy_is_item_selected(policy, xccdf_rule_get_id(rule));
	if (!is_selected) {
		dI("Skipping unselected Rule/@id=\"%s\"", xccdf_rule_get_id(rule));
		return 0;
	}
	// Find the most suitable fix.
	const struct xccdf_fix *fix = _find_fix_for_template(policy, rule, template);
	if (fix == NULL) {
		dI("No fix element was found for Rule/@id=\"%s\"", xccdf_rule_get_id(rule));
		return 0;
	}
	dI("Processing a fix for Rule/@id=\"%s\"", xccdf_rule_get_id(rule));

	// 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);
	if (res != 0) {
		oscap_seterr(OSCAP_EFAMILY_OSCAP, "A fix for Rule/@id=\"%s\" was skipped: Text substitution failed.",
				xccdf_rule_get_id(rule));
		xccdf_fix_free(cfix);
		return res == 1; // Value 2 indicates warning.
	}
	// Refine. Resolve XML comments, CDATA and remaining elements
	if (_xccdf_fix_decode_xml(cfix, fix_text) != 0) {
		oscap_seterr(OSCAP_EFAMILY_OSCAP, "A fix element for Rule/@id=\"%s\" contains unresolved child elements.",
				xccdf_rule_get_id(rule));
		xccdf_fix_free(cfix);
		return 1;
	}
	xccdf_fix_free(cfix);
	return 0;
}

static int _xccdf_policy_rule_generate_fix(struct xccdf_policy *policy, struct xccdf_rule *rule, const char *template, int output_fd, unsigned int current, unsigned int total)
{
	int ret = _write_fix_header_to_fd(template, output_fd, rule, current, total);
	if (ret != 0) {
		return ret;
	}
	char *fix_text = NULL;
	ret = _xccdf_policy_rule_get_fix_text(policy, rule, template, &fix_text);
	if (fix_text == NULL || ret != 0) {
		ret = _write_fix_missing_warning_to_fd(template, output_fd, rule);
	} else {
		ret = _write_remediation_to_fd_and_free(output_fd, template, fix_text);
	}
	if (ret != 0) {
		return ret;
	}
	ret = _write_fix_footer_to_fd(template, output_fd, rule);
	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;
	int ret = _xccdf_policy_rule_get_fix_text(policy, rule, template, &fix_text);
	if (fix_text == NULL) {
		return ret;
	}
	ret = _parse_ansible_fix(fix_text, variables, tasks);
	free(fix_text);
	return ret;
}

static int _xccdf_item_recursive_gather_selected_rules(struct xccdf_policy *policy, struct xccdf_item *item, struct oscap_list *rule_list)
{
	int ret = 0;
	const bool is_selected = xccdf_policy_is_item_selected(policy, xccdf_item_get_id(item));
	if (!is_selected) {
		return ret;
	}
	switch (xccdf_item_get_type(item)) {
	case XCCDF_GROUP:{
		struct xccdf_item_iterator *child_it = xccdf_group_get_content((struct xccdf_group *) item);
		while (xccdf_item_iterator_has_more(child_it)) {
			struct xccdf_item *child = xccdf_item_iterator_next(child_it);
			ret = _xccdf_item_recursive_gather_selected_rules(policy, child, rule_list);
			if (ret != 0)
				break;
		}
		xccdf_item_iterator_free(child_it);
		} break;
	case XCCDF_RULE:{
		oscap_list_add(rule_list, (struct xccdf_rule *) item);
		} break;
	default:
		assert(false);
		break;
	}
	return ret;
}

static void _trim_trailing_whitespace(char *str, size_t str_len)
{
	char *last_char = str + str_len - 1;
	while (isspace(*last_char)) {
		*last_char = '\0';
		last_char--;
	}
}

/* Handles multiline strings in profile title and description.
 * Puts a '#' at the beginning of each line.
 * Also removes trailing and leading whitespaces on each line.
 */
static char *_comment_multiline_text(char *text)
{
	if (text == NULL) {
		return oscap_strdup("Not available");
	}
	const char *filler = "\n# ";
	size_t buffer_size = strlen(text) + 1; // +1 for terminating '\0'
	char *buffer = malloc(buffer_size);
	char *saveptr;
	size_t filler_len = strlen(filler);
	size_t result_len = 0;
	bool first = true;
	char *str = text;
	while (true) {
		char *token = oscap_strtok_r(str, "\n", &saveptr);
		if (token == NULL) {
			break;
		}
		/* Strip leading whitespace */
		while (isspace(*token)) {
			token++;
		}
		size_t token_len = strlen(token);
		if (token_len > 0) {
			/* Strip trailing whitespace */
			_trim_trailing_whitespace(token, token_len);
			token_len = strlen(token);
		}
		if (token_len > 0) {
			/* Copy filler to output buffer */
			if (!first) {
				if (buffer_size < result_len + filler_len + 1) {
					buffer_size += filler_len;
					buffer = realloc(buffer, buffer_size);
				}
				strncpy(buffer + result_len, filler, filler_len + 1);
				result_len += filler_len;
			}
			if (buffer_size < result_len + token_len + 1) {
					buffer_size += token_len;
					buffer = realloc(buffer, buffer_size);
			}
			/* Copy token to output buffer */
			strncpy(buffer + result_len, token, token_len + 1);
			result_len += token_len;
			first = false;
		}
		str = NULL;
	}
	*(buffer + result_len) = '\0';
	return buffer;
}

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")))
		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 ? "" : "#!/bin/bash\n";

	char *fix_header;

	struct xccdf_profile *profile = xccdf_policy_get_profile(policy);
	const char *profile_id = xccdf_profile_get_id(profile);

	// Title
	struct oscap_text_iterator *title_iterator = xccdf_profile_get_title(profile);
	char *raw_profile_title = oscap_textlist_get_preferred_plaintext(title_iterator, NULL);
	oscap_text_iterator_free(title_iterator);
	char *profile_title = _comment_multiline_text(raw_profile_title);
	free(raw_profile_title);

	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 ?
				oscap_textlist_get_preferred_plaintext(description_iterator, NULL) : NULL;
		oscap_text_iterator_free(description_iterator);
		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);

		fix_header = oscap_sprintf(
			"%s"
			"###############################################################################\n"
			"#\n"
			"# %s for %s\n"
			"#\n"
			"# Profile Description:\n"
			"# %s\n"
			"#\n"
			"# Profile ID:  %s\n"
			"# Benchmark ID:  %s\n"
			"# Benchmark Version:  %s\n"
			"# XCCDF Version:  %s\n"
			"#\n"
			"# This file was generated by OpenSCAP %s using:\n"
			"# $ oscap xccdf generate fix --profile %s --fix-type %s xccdf-file.xml\n"
			"#\n"
			"# This %s is generated from an OpenSCAP profile without preliminary evaluation.\n"
			"# It attempts to fix every selected rule, even if the system is already compliant.\n"
			"#\n"
			"# How to apply this %s:\n"
			"%s\n"
			"#\n"
			"###############################################################################\n\n",
			shebang_with_newline, remediation_type, profile_title,
			commented_profile_description,
			profile_id, benchmark_id, benchmark_version_info, xccdf_version_name,
			oscap_version, profile_id, format, remediation_type,
			remediation_type, how_to_apply
		);

		free(commented_profile_description);

	} else {
		// Results-based remediation fix
		const char *start_time = xccdf_result_get_start_time(result);
		const char *end_time = xccdf_result_get_end_time(result);
		const char *result_id = xccdf_result_get_id(result);
		const struct xccdf_version_info *xccdf_version = xccdf_result_get_schema_version(result);
		const char *xccdf_version_name = xccdf_version_info_get_version(xccdf_version);

		fix_header = oscap_sprintf(
			"%s"
			"###############################################################################\n"
			"#\n"
			"# %s generated from evaluation of %s\n"
			"#\n"
			"# Profile ID: %s\n"
			"# XCCDF Version:  %s\n#\n"
			"# Evaluation Start Time:  %s\n"
			"# Evaluation End Time:  %s\n#\n"
			"# This file was generated by OpenSCAP %s using:\n"
			"# $ oscap xccdf generate fix --result-id %s --fix-type %s xccdf-results.xml\n"
			"#\n"
			"# This %s is generated from the results of a profile evaluation.\n"
			"# It attempts to remediate all issues from the selected rules that failed the test.\n"
			"#\n"
			"# How to apply this %s:\n"
			"%s\n"
			"#\n"
			"###############################################################################\n\n",
			shebang_with_newline, remediation_type, profile_title, profile_id, xccdf_version_name,
			start_time != NULL ? start_time : "Unknown", end_time, oscap_version,
			result_id, format, remediation_type, remediation_type, how_to_apply
		);
	}
	free(profile_title);

	if (ansible_script) {
		char *ansible_fix_header = oscap_sprintf(
			"---\n"
			"%s\n"
			"- hosts: all\n",
			fix_header);
		free(fix_header);
		return _write_text_to_fd_and_free(output_fd, ansible_fix_header);
	} else {
		return _write_text_to_fd_and_free(output_fd, fix_header);
	}
}

static int _xccdf_policy_generate_fix_ansible(struct oscap_list *rules_to_fix, struct xccdf_policy *policy, const char *sys, int output_fd)
{
	int ret = 0;
	struct oscap_list *variables = oscap_list_new();
	struct oscap_list *tasks = 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_ansible_fix(policy, rule, sys, variables, tasks);
		if (ret != 0)
			break;
	}
	oscap_iterator_free(rules_to_fix_it);

	_write_text_to_fd(output_fd, "  vars:\n");
	struct oscap_iterator *variables_it = oscap_iterator_new(variables);
	while(oscap_iterator_has_more(variables_it)) {
		char *var_line = (char *) oscap_iterator_next(variables_it);
		_write_text_to_fd(output_fd, var_line);
	}
	oscap_iterator_free(variables_it);
	oscap_list_free(variables, free);

	_write_text_to_fd(output_fd, "  tasks:\n");
	struct oscap_iterator *tasks_it = oscap_iterator_new(tasks);
	while(oscap_iterator_has_more(tasks_it)) {
		char *var_line = strdup((char *) oscap_iterator_next(tasks_it));
		_write_remediation_to_fd_and_free(output_fd, sys, var_line);
	}
	oscap_iterator_free(tasks_it);
	oscap_list_free(tasks, free);
	return ret;
}

static int _xccdf_policy_generate_fix_other(struct oscap_list *rules_to_fix, struct xccdf_policy *policy, const char *sys, int output_fd)
{
	int ret = 0;
	const unsigned int total = oscap_list_get_itemcount(rules_to_fix);
	unsigned int current = 1;
	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_fix(policy, rule, sys, output_fd, current++, total);
		if (ret != 0)
			break;
	}
	oscap_iterator_free(rules_to_fix_it);
	return ret;
}

int xccdf_policy_generate_fix(struct xccdf_policy *policy, struct xccdf_result *result, const char *sys, int output_fd)
{
	__attribute__nonnull__(policy);
	int ret = 0;

	struct oscap_list *rules_to_fix = oscap_list_new();
	if (result == NULL) {
		// No TestResult is available. Generate fix from the stock profile.
		dI("Generating profile-oriented fixes for policy(profile/@id=%s)", xccdf_policy_get_id(policy));
		struct xccdf_benchmark *benchmark = xccdf_policy_get_benchmark(policy);

		if (benchmark == NULL) {
			oscap_seterr(OSCAP_EFAMILY_OSCAP, "Could not find benchmark model for policy id='%s' when generating fixes.", xccdf_policy_get_id(policy));
			oscap_list_free(rules_to_fix, NULL);
			return 1;
		}

		if (_write_script_header_to_fd(policy, result, sys, output_fd) != 0) {
			oscap_list_free(rules_to_fix, NULL);
			return 1;
		}

		struct xccdf_item_iterator *item_it = xccdf_benchmark_get_content(benchmark);
		while (xccdf_item_iterator_has_more(item_it)) {
			struct xccdf_item *item = xccdf_item_iterator_next(item_it);
			ret = _xccdf_item_recursive_gather_selected_rules(policy, item, rules_to_fix);
			if (ret != 0)
				break;
		}
		xccdf_item_iterator_free(item_it);
	}
	else {
		dI("Generating result-oriented fixes for policy(result/@id=%s)", xccdf_result_get_id(result));

		if (_write_script_header_to_fd(policy, result, sys, output_fd) != 0) {
			oscap_list_free(rules_to_fix, NULL);
			return 1;
		}

		struct xccdf_rule_result_iterator *rr_it = xccdf_result_get_rule_results(result);
		while (xccdf_rule_result_iterator_has_more(rr_it)) {
			struct xccdf_rule_result *rr = xccdf_rule_result_iterator_next(rr_it);
			if (xccdf_rule_result_get_result(rr) != XCCDF_RESULT_FAIL)
				continue;
			struct xccdf_rule *rule = _lookup_rule_by_rule_result(policy, rr);
			oscap_list_add(rules_to_fix, rule);
		}
		xccdf_rule_result_iterator_free(rr_it);
	}

	// Ansible Playbooks are generated using a different function because
	// 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 {
		ret =  _xccdf_policy_generate_fix_other(rules_to_fix, policy, sys, output_fd);
	}

	oscap_list_free(rules_to_fix, NULL);

	return ret;
}