Blob Blame History Raw
/*
 * Copyright 2010-2011 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
 *
 * Authors:
 *      "Daniel Kopecek" <dkopecek@redhat.com>
 *      "Tomas Heinrich" <theinric@redhat.com>
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#include <errno.h>
#include <pcre.h>

#include "oscap_helpers.h"
#include "fsdev.h"
#include "_probe-api.h"
#include "probe/entcmp.h"
#include "debug_priv.h"
#include "oval_fts.h"
#if defined(OS_SOLARIS)
#include "fts_sun.h"
#include <sys/mntent.h>
#include <libzonecfg.h>
#include <sys/avl.h>
#elif defined(OS_AIX)
#include "fts_sun.h"
#else
#include <fts.h>
#endif

#undef OSCAP_FTS_DEBUG

static OVAL_FTS *OVAL_FTS_new()
{
	OVAL_FTS *ofts = calloc(1, sizeof(OVAL_FTS));

	ofts->max_depth  = -1;
	ofts->direction  = -1;
	ofts->filesystem = -1;

	return (ofts);
}

static void OVAL_FTS_free(OVAL_FTS *ofts)
{
	if (ofts->ofts_match_path_fts != NULL)
		fts_close(ofts->ofts_match_path_fts);
	if (ofts->ofts_recurse_path_fts != NULL)
		fts_close(ofts->ofts_recurse_path_fts);

	free(ofts);
	return;
}

static int pathlen_from_ftse(int fts_pathlen, int fts_namelen)
{
	int pathlen;

	if (fts_pathlen > fts_namelen) {
		pathlen = fts_pathlen - fts_namelen;
		if (pathlen > 1)
			pathlen--; /* strip last slash */
	} else {
		pathlen = fts_pathlen;
	}

	return pathlen;
}

static OVAL_FTSENT *OVAL_FTSENT_new(OVAL_FTS *ofts, FTSENT *fts_ent)
{
	OVAL_FTSENT *ofts_ent = calloc(1, sizeof(OVAL_FTSENT));

	ofts_ent->fts_info = fts_ent->fts_info;
	/* The 'shift' variable stores length of the prefix if the prefix
	 * is defined, otherwise it is set to 0. The value of 'shift' gives
	 * us information how many characters of the path string are part of
	 * the prefix and also where the actual path begins.
	 * We use it to remove the prefix from the path.
	 */
	const size_t shift = ofts->prefix ? strlen(ofts->prefix) : 0;
	if (ofts->ofts_sfilename || ofts->ofts_sfilepath) {
		ofts_ent->path_len = pathlen_from_ftse(fts_ent->fts_pathlen, fts_ent->fts_namelen) - shift;
		if (ofts_ent->path_len > 0) {
			ofts_ent->path = malloc(ofts_ent->path_len + 1);
			strncpy(ofts_ent->path, fts_ent->fts_path + shift, ofts_ent->path_len);
			ofts_ent->path[ofts_ent->path_len] = '\0';
		} else {
			ofts_ent->path_len = 1;
			ofts_ent->path = strdup("/");
		}

		ofts_ent->file_len = fts_ent->fts_namelen;
		ofts_ent->file = strdup(fts_ent->fts_name);
	} else {
		ofts_ent->path_len = fts_ent->fts_pathlen - shift;
		if (ofts_ent->path_len > 0) {
			ofts_ent->path = strdup(fts_ent->fts_path + shift);
		} else {
			ofts_ent->path_len = 1;
			ofts_ent->path = strdup("/");
		}

		ofts_ent->file_len = -1;
		ofts_ent->file = NULL;
	}

#if defined(OSCAP_FTS_DEBUG)
	dD("New OVAL_FTSENT: file: '%s', path: '%s'.", ofts_ent->file, ofts_ent->path);
#endif
	return (ofts_ent);
}

static void OVAL_FTSENT_free(OVAL_FTSENT *ofts_ent)
{
	free(ofts_ent->path);
	free(ofts_ent->file);
	free(ofts_ent);
	return;
}

#if defined(OS_SOLARIS)
#ifndef MNTTYPE_SMB
#define MNTTYPE_SMB	"smb"
#endif
#ifndef MNTTYPE_SMBFS
#define MNTTYPE_SMBFS	"smbfs"
#endif
#ifndef MNTTYPE_PROC
#define MNTTYPE_PROC	"proc"
#endif

typedef struct zone_path {
	avl_node_t avl_link_next;
	char zpath[MAXPATHLEN];
} zone_path_t;
static avl_tree_t avl_tree_list;


static bool valid_remote_fs(char *fstype)
{
	if (strcmp(fstype, MNTTYPE_NFS) == 0 ||
	    strcmp(fstype, MNTTYPE_SMBFS) == 0 ||
	    strcmp(fstype, MNTTYPE_SMB) == 0)
		return (true);
	return (false);
}

static bool valid_local_fs(char *fstype)
{
	if (strcmp(fstype, MNTTYPE_SWAP) == 0 ||
	    strcmp(fstype, MNTTYPE_MNTFS) == 0 ||
	    strcmp(fstype, MNTTYPE_CTFS) == 0 ||
	    strcmp(fstype, MNTTYPE_OBJFS) == 0 ||
	    strcmp(fstype, MNTTYPE_SHAREFS) == 0 ||
	    strcmp(fstype, MNTTYPE_PROC) == 0 ||
	    strcmp(fstype, MNTTYPE_LOFS) == 0 ||
	    strcmp(fstype, MNTTYPE_AUTOFS) == 0)
		return (false);
	return (true);
}

/* function to compare two avl nodes in the avl tree */
static int compare_zoneroot(const void *entry1, const void *entry2)
{
	zone_path_t *t1, *t2;
	int comp;

	t1 = (zone_path_t *)entry1;
	t2 = (zone_path_t *)entry2;
	if ((comp = strcmp(t1->zpath, t2->zpath)) == 0) {
		return (0);
	}
	return (comp > 0 ? 1 : -1);
}

int load_zones_path_list()
{
	FILE *cookie;
	char *name;
	zone_state_t state_num;
	zone_path_t *temp = NULL;
	avl_index_t where;
	char rpath[MAXPATHLEN];

	cookie = setzoneent();
	if (getzoneid() != GLOBAL_ZONEID)
		return (0);
	avl_create(&avl_tree_list, compare_zoneroot,
	    sizeof(zone_path_t), offsetof(zone_path_t, avl_link_next));
	while ((name = getzoneent(cookie)) != NULL) {
		if (strcmp(name, "global") == 0)
			continue;
		if (zone_get_state(name, &state_num) != Z_OK) {
			dE("Could not get zone state for %s", name);
			continue;
		} else if (state_num > ZONE_STATE_CONFIGURED) {
			temp = malloc(sizeof(zone_path_t));
			if (temp == NULL) {
				dE("Memory alloc failed");
				return(1);
			}
			if (zone_get_zonepath(name, rpath,
			    sizeof(rpath)) != Z_OK) {
				dE("Could not get zone path for %s",
				    name);
				continue;
			}
			if (oscap_realpath(rpath, temp->zpath) != NULL)
				avl_add(&avl_tree_list, temp);
		}
	}
	endzoneent(cookie);
	return (0);
}

static void free_zones_path_list()
{
	zone_path_t *temp;
	void* cookie = NULL;

	while ((temp = avl_destroy_nodes(&avl_tree_list, &cookie)) != NULL) {
		free(temp);
	}
	avl_destroy(&avl_tree_list);
}

static bool valid_local_zone(const char *path)
{
	zone_path_t temp;
	avl_index_t where;

	strlcpy(temp.zpath, path, sizeof(temp.zpath));
	if (avl_find(&avl_tree_list, &temp, &where) != NULL)
		return (true);

	return (false);
}


#endif

static bool OVAL_FTS_localp(OVAL_FTS *ofts, const char *path, void *id)
{
#if defined(OS_SOLARIS)
	if (id != NULL && (*(char*)id) != '\0') {
		/* if not a valid local fs skip */
		if (valid_local_fs((char*)id)) {
			/* if recurse is local , skip remote fs
			   and non-global zones */
			if (ofts->filesystem == OVAL_RECURSE_FS_LOCAL) {
				return (!(valid_remote_fs((char*)id) ||
				    valid_local_zone(path)));
			}
			return (true);
		}
		return (false);
	} else if (path != NULL) {
		/* id was not set, because fts_read failed to stat the node */
		struct stat sb;
		if ((stat(path, &sb) == 0) && (valid_local_fs(sb.st_fstype))) {
			/* if recurse is local , skip remote fs
			   and non-global zones */
			if (ofts->filesystem == OVAL_RECURSE_FS_LOCAL) {
				return (!(valid_remote_fs(sb.st_fstype) ||
				    valid_local_zone(path)));
			}
			return (true);
		}
		return (false);
	} else {
		return (false);
	}
#else
	if (id != NULL)
		return (fsdev_search(ofts->localdevs, id) == 1 ? true : false);
	else if (path != NULL)
		return (fsdev_path(ofts->localdevs, path) == 1 ? true : false);
	else
		return (false);
#endif
}

static char *__regex_locate(char *str)
{
    char *regex_sch = "^*?$.(["; /*<< regex start chars */
    bool  escaped = false;

    while (*str != '\0') {
	if (*str == '\\')
	    escaped = !escaped;
	else if (strchr(regex_sch, *str) != NULL) {
	    if (!escaped)
		return (str);
	    else
		escaped = false;
	}
	++str;
    }

    return (str);
}

static char *__string_unescape(char *str, size_t len)
{
    char *ret_str;
    size_t i, j;

    if (str == NULL || len == 0)
        return NULL;

    ret_str = strndup(str, len);

    if (ret_str == NULL)
        return NULL;

    for (i = j = 0; i < len && j <= i; ++i) {
        if (str[i] == '\\') {
            if (str[i+1] == '\0') {
                free(ret_str);
                return NULL;
            }
            ret_str[j] = str[i+1];
            ++j;
            ++i;
        } else {
            ret_str[j] = str[i];
            ++j;
        }
    }

    ret_str[j] = '\0';

    return ret_str;
}

static char *extract_fixed_path_prefix(char *path)
{
	char *s;

	if (path[0] == '^')
		path++;

	s = __regex_locate(path);
	if (*s != '\0')
		for (s--; s > (path + 1) && *s != '/'; s--);
	if (s > (path + 1)) {
		s = __string_unescape(path, (size_t) (s - path));
		if (s != NULL) {
			if (s[0] == '/')
				return s;
			free(s);
		}
	}

	return strdup("/");
}

static int badpartial_check_slash(const char *pattern)
{
	pcre *regex;
	const char *errptr = NULL;
	int errofs = 0, fb, ret;

	regex = pcre_compile(pattern + 1 /* skip '^' */, 0, &errptr, &errofs, NULL);
	if (regex == NULL) {
		dE("Failed to validate the pattern: pcre_compile(): "
		   "error: '%s', error offset: %d, pattern: '%s'.\n",
		   errptr, errofs, pattern);
		return -1;
	}
	ret = pcre_fullinfo(regex, NULL, PCRE_INFO_FIRSTBYTE, &fb);
	pcre_free(regex);
	regex = NULL;
	if (ret != 0) {
		dE("Failed to validate the pattern: pcre_fullinfo(): "
		   "return code: %d, pattern: '%s'.\n", ret, pattern);
		return -1;
	}
	if (fb != '/') {
		dE("Failed to validate the pattern: pcre_fullinfo(): "
		   "first byte: %d '%c', pattern: '%s' - the first "
		   "byte should be a '/'.\n", fb, fb, pattern);
		return -2;
	}

	return 0;
}

#define TEST_PATH1 "/"
#define TEST_PATH2 "x"

static int badpartial_transform_pattern(char *pattern, pcre **regex_out)
{
	/*
	  PCREPARTIAL(3)
	  http://pcre.org/pcre.txt
	  Last updated: 21 January 2012

	  For releases of PCRE prior to 8.00, because of the way
	  certain internal optimizations were implemented in the
	  pcre_exec() function, the PCRE_PARTIAL option (predecessor
	  of PCRE_PARTIAL_SOFT) could not be used with all patterns.

	  Items that were formerly restricted were repeated single
	  characters and repeated metasequences. If PCRE_PARTIAL was
	  set for a pattern that did not conform to the restrictions,
	  pcre_exec() returned the error code PCRE_ERROR_BADPARTIAL
	  (-13).
	*/

	int ret, brkt_lvl = 0, errofs = 0;
	const char *rchars = "\\[]()*+{"; /* probably incomplete */
	const char *test_path1 = TEST_PATH1;
	const char *errptr = NULL;
	char *s, *brkt_mark;
	bool bracketed = false, found_regex = false;
	pcre *regex;

	/* The processing bellow builds upon the assumption that
	   the pattern has been validated by pcre_compile() */
	for (s = brkt_mark = pattern; (s = strpbrk(s, rchars)) != NULL; s++) {
		switch (*s) {
		case '\\':
			s++;
			break;
		case '[':
			if (!bracketed) {
				bracketed = true;
				if (s[1] == ']')
					s++;
			}
			break;
		case ']':
			bracketed = false;
			break;
		case '(':
			if (!bracketed) {
				if (brkt_lvl++ == 0)
					brkt_mark = s;
			}
			break;
		case ')':
			if (!bracketed)
				brkt_lvl--;
			break;
		default:
			if (!bracketed)
				found_regex = true;
			break;
		}
		if (found_regex)
			break;
	}

	if (s == NULL) {
		dW("Nonfatal failure: can't transform the pattern for partial "
		   "match optimization: none of the suspected culprits found, "
		   "pattern: '%s'.", pattern);
		return -1;
	}

	if (brkt_lvl > 0)
		*brkt_mark = '\0';
	else
		*s = '\0';

	regex = pcre_compile(pattern, 0, &errptr, &errofs, NULL);
	if (regex == NULL) {
		dW("Nonfatal failure: can't transform the pattern for partial "
		   "match optimization, error: '%s', error offset: %d, "
		   "pattern: '%s'.", errptr, errofs, pattern);
		return -1;
	}

	ret = pcre_exec(regex, NULL, test_path1, strlen(test_path1), 0,
		PCRE_PARTIAL, NULL, 0);
	if (ret != PCRE_ERROR_PARTIAL && ret < 0) {
		pcre_free(regex);
		dW("Nonfatal failure: can't transform the pattern for partial "
		   "match optimization, pcre_exec() return code: %d, pattern: "
		   "'%s'.", ret, pattern);
		return -1;
	}

	if (regex_out != NULL)
		*regex_out = regex;

	return 0;
}

/* Verify that the path is usable and try to craft a regex to speed up
   the filesystem traversal. If the path to match is ill-designed, an
   ugly heuristic is employed to obtain something meaningfull. */
static int process_pattern_match(const char *path, pcre **regex_out)
{
	int ret, errofs = 0;
	char *pattern;
	const char *test_path1 = TEST_PATH1;
	//const char *test_path2 = TEST_PATH2;
	const char *errptr = NULL;
	pcre *regex;

	if (path[0] != '^') {
		/* Matching has to have a fixed starting point and thus
		   every pattern has to start with a caret. */
		size_t plen;

		plen = strlen(path) + 1;
		pattern = malloc(plen + 1);
		pattern[0] = '^';
		memcpy(pattern + 1, path, plen);
		dI("The pattern '%s' doesn't contain a leading caret - added. "
		   "All paths with the 'pattern match' operation must begin "
		   "with a caret.", path);
	} else {
		pattern = strdup(path);
	}

	regex = pcre_compile(pattern, 0, &errptr, &errofs, NULL);
	if (regex == NULL) {
		dE("Failed to validate the pattern: pcre_compile(): "
		   "error offset: %d, error: '%s', pattern: '%s'.\n",
		   errofs, errptr, pattern);
		free(pattern);
		return -1;
	}
	ret = pcre_exec(regex, NULL, test_path1, strlen(test_path1), 0,
		PCRE_PARTIAL, NULL, 0);

	switch (ret) {
	case PCRE_ERROR_PARTIAL:
		/* The pattern has matched a prefix of the test path
		   and probably begins with a slash. Make sure that it
		   doesn't match an arbitrary prefix. */

		/* todo:
		   Convince folks that they should really fix their
		   OVAL definitions that use ".*" as 'path' and then
		   uncomment this.

		dD("pcre_exec() returned PCRE_ERROR_PARTIAL for pattern '%s' "
		   "and test path '%s'.\n", pattern, test_path1);
		ret = pcre_exec(regex, NULL, test_path2, strlen(test_path2),
			0, PCRE_PARTIAL, NULL, 0);
		if (ret == PCRE_ERROR_PARTIAL || ret >= 0) {
			dE("Failed to validate the pattern: test path '%s' "
			   "matched by pattern '%s' - the pattern is too "
			   "general, i.e. inefficient. This could take a "
			   "lifetime to complete.\n", test_path2, pattern);
			pcre_free(regex);
			free(pattern);
			return -2;
		}
		*/
		break;
	case PCRE_ERROR_BADPARTIAL:
		dD("pcre_exec() returned PCRE_ERROR_BADPARTIAL for pattern "
		   "'%s' and a test path '%s'. Falling back to "
		   "pcre_fullinfo().\n", pattern, test_path1);
		pcre_free(regex);
		regex = NULL;

		/* Fallback to first byte check to determin if
		   the pattern begins with a slash. */
		ret = badpartial_check_slash((const char *) pattern);
		if (ret != 0) {
			free(pattern);
			return ret;
		}
		/* The pattern contains features that this version of
		   PCRE can't handle for partial matching. At least
		   try to find the longest well-bracketed prefix that
		   can be handled. */
		badpartial_transform_pattern(pattern, &regex);
		break;
	case PCRE_ERROR_NOMATCH:
		/* The pattern doesn't contain a leading slash (or
		   some part of this code is broken). Apologise to the
		   user and fail. */
		dE("Failed to validate the pattern: pcre_exec() returned "
		   "PCRE_ERROR_NOMATCH for pattern '%s' and a test path '%s'. "
		   "This indicates the pattern doesn't match a leading '/'.\n",
		   pattern, test_path1);
		pcre_free(regex);
		free(pattern);
		return -2;
	default:
		if (ret >= 0) {
			/* The pattern actually matches the test
			   path. Iteresting… Make sure that it doesn't
			   match an arbitrary prefix. */

			/* todo:
			   Convince folks that they should really fix
			   their OVAL definitions that use ".*" as
			   'path' and then uncomment this.

			ret = pcre_exec(regex, NULL, test_path2, strlen(test_path2),
					0, PCRE_PARTIAL, NULL, 0);
			if (ret == PCRE_ERROR_PARTIAL || ret >= 0) {
				dE("Failed to validate the pattern: test path '%s' "
				   "matched by pattern '%s' - the pattern is too "
				   "general, i.e. inefficient. This could take a "
				   "lifetime to complete.\n", test_path2, pattern);
				pcre_free(regex);
				free(pattern);
				return -2;
			}
			*/
			break;
		}
		/* Some other error. */
		dE("Failed to validate the pattern: pcre_exec() return "
		   "code: %d, pattern '%s', test path '%s'.\n", ret,
		   pattern, test_path1);
		pcre_free(regex);
		free(pattern);
		return -1;
	}

	if (regex == NULL) {
		dD("Disabling partial match optimization.");
	} else {
		dD("Enabling partial match optimization using "
		   "pattern: '%s'.", pattern);
		if (regex_out != NULL)
			*regex_out = regex;
	}

	free(pattern);

	return 0;
}


#undef TEST_PATH1
#undef TEST_PATH2

OVAL_FTS *oval_fts_open(SEXP_t *path, SEXP_t *filename, SEXP_t *filepath, SEXP_t *behaviors, SEXP_t* result)
{
	return oval_fts_open_prefixed(NULL, path, filename, filepath, behaviors, result);
}

OVAL_FTS *oval_fts_open_prefixed(const char *prefix, SEXP_t *path, SEXP_t *filename, SEXP_t *filepath, SEXP_t *behaviors, SEXP_t* result)
{
	OVAL_FTS *ofts;

	char cstr_path[PATH_MAX+1];
	char cstr_file[PATH_MAX+1];
	char cstr_buff[32];
	const char *paths[2] = { NULL, NULL };

	SEXP_t *r0;

	int mtc_fts_options = FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR;
	int rec_fts_options = FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR;
	int max_depth   = -1;
	int direction   = -1;
	int recurse     = -1;
	int filesystem  = -1;

	uint32_t path_op;
	bool nilfilename = false;
	pcre *regex = NULL;
	struct stat st;

	if ((path != NULL || filename != NULL || filepath == NULL)
			&& (path == NULL || filepath != NULL)) {
		return NULL;
	}
	if (behaviors == NULL) {
		return NULL;
	}

	if (path)
		PROBE_ENT_AREF(path, r0, "operation", /**/);
	else
		PROBE_ENT_AREF(filepath, r0, "operation", /**/);

	if (r0 != NULL) {
		path_op = SEXP_number_getu(r0);
		SEXP_free(r0);
	} else {
		path_op = OVAL_OPERATION_EQUALS;
	}
#if defined(OSCAP_FTS_DEBUG)
	dD("path_op: %u, '%s'.", path_op, oval_operation_get_text(path_op));
#endif
	if (path) { /* filepath == NULL */
		PROBE_ENT_STRVAL(path, cstr_path, sizeof cstr_path,
				 return NULL;, return NULL;);
		if (probe_ent_getvals(filename, NULL) == 0) {
			nilfilename = true;
		} else {
			PROBE_ENT_STRVAL(filename, cstr_file, sizeof cstr_file,
					 return NULL;, /* noop */;);
		}
#if defined(OSCAP_FTS_DEBUG)
		dD("path: '%s', filename: '%s', filename: %d.", cstr_path, nilfilename ? "" : cstr_file, nilfilename);
#endif
	} else { /* filepath != NULL */
		PROBE_ENT_STRVAL(filepath, cstr_path, sizeof cstr_path, return NULL;, return NULL;);
	}

	/* max_depth */
	PROBE_ENT_AREF(behaviors, r0, "max_depth", return NULL;);
	SEXP_string_cstr_r(r0, cstr_buff, sizeof cstr_buff - 1);
	max_depth = strtol(cstr_buff, NULL, 10);
	if (errno == EINVAL || errno == ERANGE) {
		dE("Invalid value of the `%s' attribute: %s", "recurse_direction", cstr_buff);
		SEXP_free(r0);
		return (NULL);
	}
#if defined(OSCAP_FTS_DEBUG)
	dD("bh.max_depth: %s => max_depth: %d", cstr_buff, max_depth);
#endif
	SEXP_free(r0);

	/* recurse_direction */
	PROBE_ENT_AREF(behaviors, r0, "recurse_direction", return NULL;);
	SEXP_string_cstr_r(r0, cstr_buff, sizeof cstr_buff - 1);
	/* todo: use oscap_string_to_enum() */
	if (strcmp(cstr_buff, "none") == 0) {
		direction = OVAL_RECURSE_DIRECTION_NONE;
	} else if (strcmp(cstr_buff, "down") == 0) {
		direction = OVAL_RECURSE_DIRECTION_DOWN;
	} else if (strcmp(cstr_buff, "up") == 0) {
		direction = OVAL_RECURSE_DIRECTION_UP;
	} else {
		dE("Invalid direction: %s", cstr_buff);
		SEXP_free(r0);
		return (NULL);
	}
#if defined(OSCAP_FTS_DEBUG)
	dD("bh.direction: %s => direction: %d", cstr_buff, direction);
#endif
	SEXP_free(r0);

	/* recurse */
	PROBE_ENT_AREF(behaviors, r0, "recurse", /**/);
	if (r0 != NULL) {
		SEXP_string_cstr_r(r0, cstr_buff, sizeof cstr_buff - 1);
		/* todo: use oscap_string_to_enum() */
		if (strcmp(cstr_buff, "symlinks and directories") == 0) {
			recurse = OVAL_RECURSE_SYMLINKS_AND_DIRS;
		} else if (strcmp(cstr_buff, "files and directories") == 0) {
			recurse = OVAL_RECURSE_FILES_AND_DIRS;
		} else if (strcmp(cstr_buff, "symlinks") == 0) {
			recurse = OVAL_RECURSE_SYMLINKS;
		} else if (strcmp(cstr_buff, "directories") == 0) {
			recurse = OVAL_RECURSE_DIRS;
		} else {
			dE("Invalid recurse: %s", cstr_buff);
			SEXP_free(r0);
			return (NULL);
		}
	} else {
		recurse = OVAL_RECURSE_SYMLINKS_AND_DIRS;
	}
#if defined(OSCAP_FTS_DEBUG)
	dD("bh.recurse: %s => recurse: %d", cstr_buff, recurse);
#endif
	SEXP_free(r0);

	/* recurse_file_system */
	PROBE_ENT_AREF(behaviors, r0, "recurse_file_system", /**/);

	if (r0 != NULL) {
		SEXP_string_cstr_r(r0, cstr_buff, sizeof cstr_buff - 1);
		/* todo: use oscap_string_to_enum() */
		if (strcmp(cstr_buff, "local") == 0) {
			filesystem = OVAL_RECURSE_FS_LOCAL;
		} else if (strcmp(cstr_buff, "all") == 0) {
			filesystem = OVAL_RECURSE_FS_ALL;
		} else if (strcmp(cstr_buff, "defined") == 0) {
			filesystem = OVAL_RECURSE_FS_DEFINED;
			rec_fts_options |= FTS_XDEV;
		} else {
			dE("Invalid recurse filesystem: %s", cstr_buff);
			SEXP_free(r0);
			return (NULL);
		}
	} else {
		filesystem = OVAL_RECURSE_FS_ALL;
	}
#if defined(OSCAP_FTS_DEBUG)
	dD("bh.filesystem: %s => filesystem: %d", cstr_buff, filesystem);
#endif
	SEXP_free(r0);

	/* todo:
	   Still missing is a propagation of the error to the
	   user. Currently, all the information is provided in the
	   debug log, but the oval_fts api has no way of passing this
	   information to the user.
	*/

	if (path_op == OVAL_OPERATION_EQUALS) {
		paths[0] = strdup(cstr_path);
	} else if (path_op == OVAL_OPERATION_PATTERN_MATCH) {
		if (process_pattern_match(cstr_path, &regex) != 0)
			return NULL;
		paths[0] = extract_fixed_path_prefix(cstr_path);
		dD("Extracted fixed path: '%s'.", paths[0]);
	} else {
		paths[0] = strdup("/");
	}

	if (prefix != NULL) {
		char *path_with_prefix = oscap_path_join(prefix, paths[0]);
		free((void *) paths[0]);
		paths[0] = path_with_prefix;
	}
	dI("Opening file '%s'.", paths[0]);
	/* Fail if the provided path doensn't actually exist. Symlinks
	   without targets are accepted. */
	if (lstat(paths[0], &st) == -1) {
		if (errno) {
			dD("lstat() failed: errno: %d, '%s'.",
			   errno, strerror(errno));
		}
		free((void *) paths[0]);
		return NULL;
	}

	ofts = OVAL_FTS_new();
	ofts->prefix = prefix;

	/* reset errno as fts_open() doesn't do it itself. */
	errno = 0;
	ofts->ofts_match_path_fts = fts_open((char * const *) paths, mtc_fts_options, NULL);
	free((void *) paths[0]);
	/* fts_open() doesn't return NULL for all errors (e.g. nonexistent paths),
	   so check errno to detect it. Far from being perfect. */
	if (ofts->ofts_match_path_fts == NULL || errno != 0) {
		dE("fts_open() failed, errno: %d \"%s\".", errno, strerror(errno));
		OVAL_FTS_free(ofts);
		return (NULL);
	}

	ofts->ofts_recurse_path_fts_opts = rec_fts_options;
	ofts->ofts_path_op = path_op;
	if (regex != NULL) {
		const char *errptr = NULL;

		ofts->ofts_path_regex = regex;
		ofts->ofts_path_regex_extra = pcre_study(regex, 0, &errptr);
	}

	if (filesystem == OVAL_RECURSE_FS_LOCAL) {
#if defined(OS_SOLARIS)
		ofts->localdevs = NULL;
#else
		ofts->localdevs = fsdev_init();
		if (ofts->localdevs == NULL) {
			dE("fsdev_init() failed.");
			/* One dummy read to get rid of an uninitialized
			 * value in the FTS data before calling
			 * fts_close() on it. */
			fts_read(ofts->ofts_match_path_fts);
			oval_fts_close(ofts);
			return (NULL);
		}
#endif
	} else if (filesystem == OVAL_RECURSE_FS_DEFINED) {
		/* store the device id for future comparison */
		FTSENT *fts_ent;

		fts_ent = fts_read(ofts->ofts_match_path_fts);
		if (fts_ent != NULL) {
			ofts->ofts_recurse_path_devid = fts_ent->fts_statp->st_dev;
			fts_set(ofts->ofts_match_path_fts, fts_ent, FTS_AGAIN);
		}
	}

	ofts->recurse = recurse;
	ofts->filesystem = filesystem;

	if (path) { /* filepath == NULL */
		ofts->ofts_spath = SEXP_ref(path); /* path entity */
		if (!nilfilename)
			ofts->ofts_sfilename = SEXP_ref(filename); /* filename entity */

		ofts->max_depth = max_depth;
		ofts->direction = direction;
	} else { /* filepath != NULL */
		ofts->ofts_sfilepath = SEXP_ref(filepath);
	}

#if defined(OS_SOLARIS)
	if (load_zones_path_list() != 0) {
		dE("Failed to load zones path info. Recursing non-global zones.");
		free_zones_path_list();
	}
#endif

	ofts->result = result;

	return (ofts);
}

static inline int _oval_fts_is_local(OVAL_FTS *ofts, FTSENT *fts_ent) {
# if defined(OS_SOLARIS)
	/* pseudo filesystems will be skipped */
	/* don't recurse into remote fs if local is specified */
	return ((fts_ent->fts_info == FTS_D || fts_ent->fts_info == FTS_SL)
	    && (!OVAL_FTS_localp(ofts, fts_ent->fts_path,
	    (fts_ent->fts_statp != NULL) ?
	    &fts_ent->fts_statp->st_fstype : NULL)));
#else
	/* don't recurse into non-local filesystems */
	return (ofts->filesystem == OVAL_RECURSE_FS_LOCAL
	    && (fts_ent->fts_info == FTS_D || fts_ent->fts_info == FTS_SL)
	    && (!OVAL_FTS_localp(ofts, fts_ent->fts_path,
				 (fts_ent->fts_statp != NULL) ?
				 &fts_ent->fts_statp->st_dev : NULL)));
#endif
}

/* find the first matching path or filepath */
static FTSENT *oval_fts_read_match_path(OVAL_FTS *ofts)
{
	FTSENT *fts_ent = NULL;
	SEXP_t *stmp;
	oval_result_t ores;

	/* iterate until a match is found or all elements have been traversed */
	for (;;) {
		fts_ent = fts_read(ofts->ofts_match_path_fts);
		if (fts_ent == NULL)
			return NULL;
		switch (fts_ent->fts_info) {
		case FTS_DP:
			continue;
		case FTS_DC:
			dW("Filesystem tree cycle detected at '%s'.", fts_ent->fts_path);
			fts_set(ofts->ofts_match_path_fts, fts_ent, FTS_SKIP);
			continue;
		}

#if defined(OSCAP_FTS_DEBUG)
		dD("fts_path: '%s' (l=%d)."
		   "fts_name: '%s' (l=%d).\n"
		   "fts_info: %u.\n", fts_ent->fts_path, fts_ent->fts_pathlen,
		   fts_ent->fts_name, fts_ent->fts_namelen, fts_ent->fts_info);
#endif

		if (fts_ent->fts_info == FTS_SL) {
#if defined(OSCAP_FTS_DEBUG)
			dD("Only the target of a symlink gets reported, skipping '%s'.", fts_ent->fts_path, fts_ent->fts_name);
#endif
			fts_set(ofts->ofts_match_path_fts, fts_ent, FTS_FOLLOW);
			continue;
		}
		if (_oval_fts_is_local(ofts, fts_ent)) {
			dI("Don't recurse into non-local filesystems, skipping '%s'.", fts_ent->fts_path);
			fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
			continue;
		}
		/* don't recurse beyond the initial filesystem */
		if (ofts->filesystem == OVAL_RECURSE_FS_DEFINED
		    && (fts_ent->fts_info == FTS_D || fts_ent->fts_info == FTS_SL)
		    && ofts->ofts_recurse_path_devid != fts_ent->fts_statp->st_dev) {
			fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
			continue;
		}

		const size_t shift = ofts->prefix ? strlen(ofts->prefix) : 0;
		/* partial match optimization for OVAL_OPERATION_PATTERN_MATCH operation on path and filepath */
		if (ofts->ofts_path_regex != NULL && fts_ent->fts_info == FTS_D) {
			int ret, svec[3];

			ret = pcre_exec(ofts->ofts_path_regex, ofts->ofts_path_regex_extra,
					fts_ent->fts_path+shift, fts_ent->fts_pathlen-shift, 0, PCRE_PARTIAL,
					svec, sizeof(svec) / sizeof(svec[0]));
			if (ret < 0) {
				switch (ret) {
				case PCRE_ERROR_NOMATCH:
					dD("Partial match optimization: PCRE_ERROR_NOMATCH, skipping.");
					fts_set(ofts->ofts_match_path_fts, fts_ent, FTS_SKIP);
					continue;
				case PCRE_ERROR_PARTIAL:
					dD("Partial match optimization: PCRE_ERROR_PARTIAL, continuing.");
					continue;
				default:
					dE("pcre_exec() error: %d.", ret);
					return NULL;
				}
			}
		}

		if ((ofts->ofts_sfilepath && fts_ent->fts_info == FTS_D)
		    || (!ofts->ofts_sfilepath && fts_ent->fts_info != FTS_D))
			continue;

		stmp = SEXP_string_newf("%s", fts_ent->fts_path + shift);

		if (ofts->ofts_sfilepath)
			/* try to match filepath */
			ores = probe_entobj_cmp(ofts->ofts_sfilepath, stmp);
		else
			/* try to match path */
			ores = probe_entobj_cmp(ofts->ofts_spath, stmp);
		SEXP_free(stmp);

		if (ores == OVAL_RESULT_TRUE)
			break;
		if (ofts->ofts_path_op == OVAL_OPERATION_EQUALS) {
			/* At this point the comparison result isn't OVAL_RESULT_TRUE. Since
			we passed the exact path (from filepath or path elements) to
			fts_open() we surely know that we can't find other items that would
			be equal. Therefore we can terminate the matching. This can happen
			if the filepath or path element references a variable that has
			multiple different values. */
			return NULL;
		}
	} /* for (;;) */

	/*
	 * If we know that we are not going to return anything
	 * else, then we can close the path FTS and return NULL
	 * the next time...
	 */
	if (ofts->ofts_path_op   == OVAL_OPERATION_EQUALS &&
	    ofts->direction      == OVAL_RECURSE_DIRECTION_NONE &&
	    ofts->ofts_sfilename == NULL &&
	    ofts->ofts_sfilepath == NULL)
	{
		fts_set(ofts->ofts_match_path_fts, fts_ent, FTS_SKIP);
	}

	return fts_ent;
}

/* find the first matching file or directory */
static FTSENT *oval_fts_read_recurse_path(OVAL_FTS *ofts)
{
	FTSENT *out_fts_ent = NULL;
	/* the condition below is correct because ofts_sfilepath is NULL here */
	bool collect_dirs = (ofts->ofts_sfilename == NULL);

	switch (ofts->direction) {

	case OVAL_RECURSE_DIRECTION_DOWN:
	case OVAL_RECURSE_DIRECTION_NONE:
		if (ofts->direction == OVAL_RECURSE_DIRECTION_NONE
		    && collect_dirs) {
			/* the target is the directory itself */
			out_fts_ent = ofts->ofts_match_path_fts_ent;
			ofts->ofts_match_path_fts_ent = NULL;
			break;
		}

		/* initialize separate fts for recursion */
		if (ofts->ofts_recurse_path_fts == NULL) {
			char * const paths[2] = { ofts->ofts_match_path_fts_ent->fts_path, NULL };

#if defined(OSCAP_FTS_DEBUG)
			dD("fts_open args: path: \"%s\", options: %d.",
				paths[0], ofts->ofts_recurse_path_fts_opts);
#endif
			/* reset errno as fts_open() doesn't do it itself. */
			errno = 0;
			ofts->ofts_recurse_path_fts = fts_open(paths,
				ofts->ofts_recurse_path_fts_opts, NULL);
			/* fts_open() doesn't return NULL for all errors
			   (e.g. nonexistent paths), so check errno to detect it.
			   Far from being perfect. */
			if (ofts->ofts_recurse_path_fts == NULL || errno != 0) {
				dE("fts_open() failed, errno: %d \"%s\".",
					errno, strerror(errno));
#if !defined(OSCAP_FTS_DEBUG)
				dE("fts_open args: path: \"%s\", options: %d.",
					paths[0], ofts->ofts_recurse_path_fts_opts);
#endif
				if (ofts->ofts_recurse_path_fts != NULL) {
					fts_close(ofts->ofts_recurse_path_fts);
					ofts->ofts_recurse_path_fts = NULL;
				}
				return (NULL);
			}
		}

		/* iterate until a match is found or all elements have been traversed */
		while (out_fts_ent == NULL) {
			FTSENT *fts_ent;

			fts_ent = fts_read(ofts->ofts_recurse_path_fts);
			if (fts_ent == NULL) {
				fts_close(ofts->ofts_recurse_path_fts);
				ofts->ofts_recurse_path_fts = NULL;

				return NULL;
			}

			switch (fts_ent->fts_info) {
			case FTS_DP:
				continue;
			case FTS_DC:
				dW("Filesystem tree cycle detected at '%s'.", fts_ent->fts_path);
				fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
				continue;
			}

#if defined(OSCAP_FTS_DEBUG)
			dD("fts_path: '%s' (l=%d)."
			   "fts_name: '%s' (l=%d).\n"
			   "fts_info: %u.\n", fts_ent->fts_path, fts_ent->fts_pathlen,
			   fts_ent->fts_name, fts_ent->fts_namelen, fts_ent->fts_info);
#endif

			/* collect matching target */
			if (collect_dirs) {
				if (fts_ent->fts_info == FTS_D
				    && (ofts->max_depth == -1 || fts_ent->fts_level <= ofts->max_depth))
					out_fts_ent = fts_ent;
			} else {
				if (fts_ent->fts_info != FTS_D) {
					SEXP_t *stmp;

					stmp = SEXP_string_newf("%s", fts_ent->fts_name);
					oval_result_t result = probe_entobj_cmp(ofts->ofts_sfilename, stmp);
					switch (result){
						case OVAL_RESULT_TRUE:
							out_fts_ent = fts_ent;
							break;

						case OVAL_RESULT_ERROR:
							probe_cobj_set_flag(ofts->result, SYSCHAR_FLAG_ERROR);
							break;

						default:
							break;
					}

					SEXP_free(stmp);
				}
			}

			if (fts_ent->fts_level > 0) { /* don't skip fts root */
				/* limit recursion depth */
				if (ofts->direction == OVAL_RECURSE_DIRECTION_NONE
				    || (ofts->max_depth != -1 && fts_ent->fts_level > ofts->max_depth)) {
					fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
					continue;
				}

				/* limit recursion only to selected file types */
				switch (fts_ent->fts_info) {
				case FTS_D:
					if (!(ofts->recurse & OVAL_RECURSE_DIRS)) {
						fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
						continue;
					}
					break;
				case FTS_SL:
					if (!(ofts->recurse & OVAL_RECURSE_SYMLINKS)) {
						fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
						continue;
					}
					fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_FOLLOW);
					break;
				default:
					continue;
				}
			}
			if (_oval_fts_is_local(ofts, fts_ent)) {
				fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
				continue;
			}
			/* don't recurse beyond the initial filesystem */
			if (ofts->filesystem == OVAL_RECURSE_FS_DEFINED
			    && (fts_ent->fts_info == FTS_D || fts_ent->fts_info == FTS_SL)
			    && ofts->ofts_recurse_path_devid != fts_ent->fts_statp->st_dev) {
				fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
				continue;
			}
		}

		break;
	case OVAL_RECURSE_DIRECTION_UP:
		if (ofts->ofts_recurse_path_pthcpy == NULL) {
			ofts->ofts_recurse_path_pthcpy = \
			ofts->ofts_recurse_path_curpth = strdup(ofts->ofts_match_path_fts_ent->fts_path);
			ofts->ofts_recurse_path_curdepth = 0;
		}

		while (ofts->max_depth == -1 || ofts->ofts_recurse_path_curdepth <= ofts->max_depth) {
			/* initialize separate fts for recursion */
			if (ofts->ofts_recurse_path_fts == NULL) {
				char * const paths[2] = { ofts->ofts_recurse_path_curpth, NULL };

#if defined(OSCAP_FTS_DEBUG)
				dD("fts_open args: path: \"%s\", options: %d.",
					paths[0], ofts->ofts_recurse_path_fts_opts);
#endif
				/* reset errno as fts_open() doesn't do it itself. */
				errno = 0;
				/* fts_open() doesn't return NULL for all errors
				   (e.g. nonexistent paths), so check errno to
				   detect it. Far from being perfect. */
				ofts->ofts_recurse_path_fts = fts_open(paths,
					ofts->ofts_recurse_path_fts_opts, NULL);
				if (ofts->ofts_recurse_path_fts == NULL || errno != 0) {
					dE("fts_open() failed, errno: %d \"%s\".",
						errno, strerror(errno));
#if !defined(OSCAP_FTS_DEBUG)
					dE("fts_open args: path: \"%s\", options: %d.",
						paths[0], ofts->ofts_recurse_path_fts_opts);
#endif
					if (ofts->ofts_recurse_path_fts != NULL) {
						fts_close(ofts->ofts_recurse_path_fts);
						ofts->ofts_recurse_path_fts = NULL;
					}
					return (NULL);
				}
			}

			/* iterate until a match is found or all elements have been traversed */
			while (out_fts_ent == NULL) {
				FTSENT *fts_ent;

				fts_ent = fts_read(ofts->ofts_recurse_path_fts);
				if (fts_ent == NULL)
					break;

				/*
				   it would be more accurate to obtain the device
				   id here, but for the sake of supporting the
				   comparison also in oval_fts_read_match_path(),
				   the device id is obtained in oval_fts_open_prefixed()

				if (ofts->ofts_recurse_path_curdepth == 0)
					ofts->ofts_recurse_path_devid = fts_ent->fts_statp->st_dev;
				*/
#if defined(OS_SOLARIS)
				if ((!OVAL_FTS_localp(ofts, fts_ent->fts_path,
				    (fts_ent->fts_statp != NULL) ?
				    &fts_ent->fts_statp->st_fstype : NULL)))
				       break;
#else
				if (ofts->filesystem == OVAL_RECURSE_FS_LOCAL
				    && (!OVAL_FTS_localp(ofts, fts_ent->fts_path,
						(fts_ent->fts_statp != NULL) ?
						&fts_ent->fts_statp->st_dev : NULL)))
					break;
#endif
				if (ofts->filesystem == OVAL_RECURSE_FS_DEFINED
				    && ofts->ofts_recurse_path_devid != fts_ent->fts_statp->st_dev)
					break;

				/* collect matching target */
				if (collect_dirs) {
					/* only fts root is collected */
					if (fts_ent->fts_level == 0 && fts_ent->fts_info == FTS_D) {
						out_fts_ent = fts_ent;
						fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
						break;
					}
				} else {
					if (fts_ent->fts_info != FTS_D) {
						SEXP_t *stmp;

						stmp = SEXP_string_newf("%s", fts_ent->fts_name);
						if (probe_entobj_cmp(ofts->ofts_sfilename, stmp) == OVAL_RESULT_TRUE)
							out_fts_ent = fts_ent;
						SEXP_free(stmp);
					}
				}

				if (fts_ent->fts_info == FTS_SL)
					fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_FOLLOW);
				/* limit recursion only to fts root */
				else if (fts_ent->fts_level > 0)
					fts_set(ofts->ofts_recurse_path_fts, fts_ent, FTS_SKIP);
			}

			if (out_fts_ent != NULL)
				break;

			fts_close(ofts->ofts_recurse_path_fts);
			ofts->ofts_recurse_path_fts = NULL;

			if (!strcmp(ofts->ofts_recurse_path_curpth, "/"))
				break;

			ofts->ofts_recurse_path_curpth = oscap_dirname(ofts->ofts_recurse_path_curpth);
			ofts->ofts_recurse_path_curdepth++;
		}

		if (out_fts_ent == NULL) {
			free(ofts->ofts_recurse_path_pthcpy);
			ofts->ofts_recurse_path_pthcpy = NULL;
		}

		break;
	}

	return out_fts_ent;
}

OVAL_FTSENT *oval_fts_read(OVAL_FTS *ofts)
{
	FTSENT *fts_ent;

#if defined(OSCAP_FTS_DEBUG)
	dD("ofts: %p.", ofts);
#endif

	if (ofts == NULL)
		return NULL;

	for (;;) {
		if (ofts->ofts_match_path_fts_ent == NULL) {
			ofts->ofts_match_path_fts_ent = oval_fts_read_match_path(ofts);
			if (ofts->ofts_match_path_fts_ent == NULL)
				return NULL;
		}

		if (ofts->ofts_sfilepath) {
			fts_ent = ofts->ofts_match_path_fts_ent;
			ofts->ofts_match_path_fts_ent = NULL;
			break;
		} else {
			fts_ent = oval_fts_read_recurse_path(ofts);
			if (fts_ent != NULL)
				break;

			ofts->ofts_match_path_fts_ent = NULL;

			// todo: is this true when variables are used?
			/* with 'equals', there's only one potential target */
			if (ofts->ofts_path_op == OVAL_OPERATION_EQUALS)
				return (NULL);
		}
	}

	return OVAL_FTSENT_new(ofts, fts_ent);
}

void oval_ftsent_free(OVAL_FTSENT *ofts_ent)
{
	OVAL_FTSENT_free(ofts_ent);
}

int oval_fts_close(OVAL_FTS *ofts)
{
	if (ofts->ofts_recurse_path_pthcpy != NULL)
		free(ofts->ofts_recurse_path_pthcpy);

	if (ofts->ofts_path_regex)
		pcre_free(ofts->ofts_path_regex);
	if (ofts->ofts_path_regex_extra)
		pcre_free(ofts->ofts_path_regex_extra);

	if (ofts->ofts_spath != NULL)
		SEXP_free(ofts->ofts_spath);
	if (ofts->ofts_sfilename != NULL)
		SEXP_free(ofts->ofts_sfilename);
	if (ofts->ofts_sfilepath != NULL)
		SEXP_free(ofts->ofts_sfilepath);

	fsdev_free(ofts->localdevs);

	OVAL_FTS_free(ofts);
#if defined(OS_SOLARIS)
	free_zones_path_list();
#endif

	return (0);
}