/*
 * fapolicy-cli.c - CLI tool for fapolicyd
 * Copyright (c) 2019-2022 Red Hat Inc.
 * All Rights Reserved.
 *
 * This software may be freely redistributed and/or modified under the
 * terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING. If not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor
 * Boston, MA 02110-1335, USA.
 *
 * Authors:
 *   Radovan Sroka <rsroka@redhat.com>
 *   Steve Grubb <sgrubb@redhat.com>
 *   Zoltan Fridrich <zfridric@redhat.com>
 */

#include "config.h"
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <magic.h>
#include <stdbool.h>
#include <stdlib.h>
#include <getopt.h>
#include <stdatomic.h>
#include <lmdb.h>
#include <limits.h>
#include <signal.h>
#include <ftw.h>
#include <libgen.h>	// basename
#include "policy.h"
#include "rules.h"
#include "database.h"
#include "file-cli.h"
#include "file.h"
#include "fapolicyd-backend.h"
#include "string-util.h"
#include "daemon-config.h"
#include "decision-config.h"
#include "gcc-attributes.h"
#include "message.h"
#include "llist.h"
#include "avl.h"
#include "fd-fgets.h"
#include "paths.h"
#include "filter.h"
#include "file.h"
#include "ignore-mounts.h"
#include "rule-lint.h"
#include "cli-privilege.h"

bool verbose = false;
static bool lint_rules = false;
static bool assume_yes = false;

static int send_state_report_signal(unsigned int pid, report_intent_t intent,
		char *reason, size_t reason_len)
	__attr_access ((__write_only__, 3, 4));
static int send_timing_signal(unsigned int pid, report_intent_t intent,
		char *reason, size_t reason_len)
	__attr_access ((__write_only__, 3, 4));
static int get_daemon_pid(unsigned int *pid, char *reason, size_t reason_len)
	__attr_access ((__write_only__, 2, 3));

static const char *usage =
"Fapolicyd CLI Tool\n\n"
"--check-config        Check the daemon config for syntax errors\n"
"--check-path          Check files in $PATH against the trustdb for problems\n"
"--check-status        Dump daemon health and configuration state\n"
"--check-metrics       Dump daemon runtime counters and cache metrics\n"
"--check-trustdb       Check the trustdb against files on disk for problems\n"
"--compact-trustdb     Rebuild and compact the daemon trustdb LMDB environment\n"
"--check-watch_fs      Check watch_fs against currently mounted file systems\n"
"--check-ignore_mounts [path] Scan ignored mounts for risky content\n"
"--check-rules [path]  Validate rules file syntax without loading\n"
"--lint                Enable policy lint warnings with --check-rules\n"
"--reset-metrics       Dump metrics and reset counters when daemon allows it\n"
"--timing-start        Start a manual decision timing run\n"
"--timing-stop         Stop manual decision timing and dump timing report\n"
"--timer-start         Alias for --timing-start\n"
"--timer-stop          Alias for --timing-stop\n"
"--verbose             Enable verbose output for select commands\n"
"-d, --delete-db       Delete the trust database\n"
"-D, --dump-db         Dump the trust database contents\n"
"-f, --file cmd path   Manage the file trust database\n"
"-h, --help            Prints this help message\n"
"-t, --ftype file-path Prints out the mime type of a file\n"
"-T, --ftype-ext file-path Emit NUL-delimited path and mime type\n"
"-l, --list            Prints a list of the daemon's rules with numbers\n"
"-r, --reload-rules    Notifies fapolicyd to perform reload of rules\n"
"-y, --yes             Do not prompt before a manual metrics reset\n"
#ifdef HAVE_LIBRPM
"--test-filter path    Test FILTER_FILE against path and trace to stdout\n"
#endif
"--filter              Use FILTER_FILE for --file add or update\n"
"--trust-file file     Use after --file to specify trust file\n"
"-u, --update          Notifies fapolicyd to perform update of database\n"
;

static struct option long_opts[] =
{
	{"check-config",0, NULL,  1 },
	{"check-watch_fs",0, NULL, 2 },
	{"check-ignore_mounts", 2, NULL, 7 },
	{"verbose",     0, NULL, 8 },
	{"check-trustdb",0, NULL,  3 },
	{"check-status",0, NULL,  4 },
	{"check-path",  0, NULL,  5 },
	{"check-metrics",0, NULL, 14 },
	{"compact-trustdb", 0, NULL, 15 },
	{"check-rules",  2, NULL, 9 },
	{"lint",	0, NULL, 10 },
	{"reset-metrics", 0, NULL, 11 },
	{"timing-start",	0, NULL, 12 },
	{"timing-stop",	0, NULL, 13 },
	{"timer-start",	0, NULL, 12 },
	{"timer-stop",	0, NULL, 13 },
	{"yes",		0, NULL, 'y'},
	{"delete-db",	0, NULL, 'd'},
	{"dump-db",	0, NULL, 'D'},
	{"file",	1, NULL, 'f'},
	{"help",	0, NULL, 'h'},
	{"ftype",	1, NULL, 't'},
	{"ftype-ext",	1, NULL, 'T'},
	{"list",	0, NULL, 'l'},
	{"update",	0, NULL, 'u'},
	{"reload-rules",	0, NULL, 'r'},
#ifdef HAVE_LIBRPM
	{"test-filter", 1, NULL, 6 },
#endif
	{ NULL,		0, NULL, 0 }
};

atomic_bool stop = 0;  // Library needs this
unsigned int debug_mode = 0;			// Library needs this
conf_t config;				// Library needs this

static void reset_config(void)
{
	decision_config_destroy();
	free_daemon_config(&config);
	memset(&config, 0, sizeof(config));
}

typedef enum _reload_code { DB, RULES, COMPACT } reload_code;

static char *get_line(FILE *f)
{
	char *line = NULL;
	size_t len = 0;

	if (getline(&line, &len, f) != -1) {
		/* remove newline */
		char *ptr = strchr(line, 0x0a);
		if (ptr)
			*ptr = 0;
		return line;
	}
	free(line);
	return NULL;
}


static int do_delete_db(void)
{
	if (unlink_db())
		return CLI_EXIT_DB_ERROR;
	return CLI_EXIT_SUCCESS;
}

// This function opens the trust db and iterates over the entries.
// It returns CLI_EXIT_SUCCESS on success and CLI_EXIT_DB_ERROR on failure
static int verify_file(const char *path, off_t size, const char *sha,
		        unsigned int tsource);
static int do_dump_db(void)
{
	int rc, exit_rc = CLI_EXIT_SUCCESS;
	MDB_env *env;
	MDB_txn *txn;
	MDB_dbi dbi;
	MDB_stat status;
	MDB_cursor *cursor;
	MDB_val key, val;
	char active_name[TRUST_DB_GENERATION_NAME_SIZE];

	rc = mdb_env_create(&env);
	if (rc) {
		fprintf(stderr, "mdb_env_create failed, error %d %s\n", rc,
							mdb_strerror(rc));
		return CLI_EXIT_DB_ERROR;
	}
	rc = mdb_env_set_maxdbs(env, 128);
	if (rc) {
		fprintf(stderr, "mdb_env_set_maxdbs failed, error %d %s\n", rc,
				mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto env_close;
	}
	/*
	 * Dumping walks the live database while the daemon may reload trust DB
	 * generations, so this read-only transaction must enter LMDB's reader
	 * table instead of bypassing locking.
	 */
	rc = mdb_env_open(env, DB_DIR, MDB_RDONLY, 0660);
	if (rc) {
		fprintf(stderr, "mdb_env_open failed, error %d %s\n", rc,
							mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto env_close;
	}
	rc = mdb_env_stat(env, &status);
	if (rc) {
		fprintf(stderr, "mdb_env_stat failed, error %d %s\n", rc,
							mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto env_close;
	}
	if (status.ms_entries == 0) {
		printf("Trust database is empty\n");
		goto env_close; // Note: rc is 0 to get here
	}
	rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn);
	if (rc) {
		fprintf(stderr, "mdb_txn_begin failed, error %d %s\n", rc,
							mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto env_close;
	}
	/*
	 * The daemon publishes trust DB generations as named databases and
	 * records the active name in metadata. Older databases did not have this
	 * metadata, so fall back to DB_NAME for compatibility.
	 */
	rc = database_read_active_name(txn, active_name, sizeof(active_name));
	if (rc) {
		fprintf(stderr, "trust metadata read failed, error %d %s\n",
							rc, mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto txn_abort;
	}
	rc = mdb_dbi_open(txn, active_name, MDB_DUPSORT, &dbi);
	if (rc) {
		fprintf(stderr, "mdb_open failed, error %d %s\n", rc,
							mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto txn_abort;
	}
	rc = mdb_stat(txn, dbi, &status);
	if (rc) {
		fprintf(stderr, "mdb_stat failed, error %d %s\n", rc,
							mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto dbi_close;
	}
	if (status.ms_entries == 0) {
		printf("Trust database is empty\n");
		goto dbi_close;
	}
	rc = mdb_cursor_open(txn, dbi, &cursor);
	if (rc) {
		fprintf(stderr, "mdb_cursor_open failed, error %d %s\n", rc,
							mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto dbi_close;
	}
	rc = mdb_cursor_get(cursor, &key, &val, MDB_FIRST);
	if (rc) {
		if (rc == MDB_NOTFOUND) {
			printf("Trust database is empty\n");
			goto cursor_close;
		}
		fprintf(stderr, "mdb_cursor_get failed, error %d %s\n", rc,
							mdb_strerror(rc));
		exit_rc = CLI_EXIT_DB_ERROR;
		goto cursor_close;
	}
	do {
		char *path = NULL, *data = NULL, sha[FILE_DIGEST_STRING_MAX];
		unsigned int tsource;
		trustdb_size_t size;
		const char *source;

		path = malloc(key.mv_size + 1);
		if (!path)
			goto next_record;

		memcpy(path, key.mv_data, key.mv_size);
		path[key.mv_size] = 0;
		data = malloc(val.mv_size + 1);

		if (!data)
			goto next_record;

		memcpy(data, val.mv_data, val.mv_size);
		data[val.mv_size] = 0;

		if (sscanf(data, DATA_FORMAT_IN, &tsource, &size, sha) != 3)
			goto next_record;

		source = lookup_tsource(tsource);
		printf("%s %s %" PRIu64 " %s\n", source, path, size, sha);

next_record:
		free(data);
		free(path);
		// Try to get the duplicate. If it doesn't exist, get the next one
		rc = mdb_cursor_get(cursor, &key, &val, MDB_NEXT_DUP);
		if (rc == MDB_NOTFOUND)
			rc = mdb_cursor_get(cursor, &key, &val, MDB_NEXT_NODUP);
	} while (rc == 0);

	if (rc != MDB_NOTFOUND)
		exit_rc = CLI_EXIT_DB_ERROR;
cursor_close:
	mdb_cursor_close(cursor);
dbi_close:
	mdb_close(env, dbi);
txn_abort:
	mdb_txn_abort(txn);
env_close:
	mdb_env_close(env);

	return exit_rc;
}

static int parse_file_args(int argc, char * const argv[],
			    const char **path, const char **trust_file,
			    bool *use_filter, bool path_optional)
{
	*path = NULL;
	*trust_file = NULL;
	*use_filter = false;

	for (int i = 0; i < argc; i++) {
		if (!strcmp(argv[i], "--filter")) {
			if (*use_filter)
				return CLI_EXIT_USAGE;
			*use_filter = true;
			continue;
		}
		if (!strcmp(argv[i], "--trust-file")) {
			if (*trust_file || i + 1 >= argc)
				return CLI_EXIT_USAGE;
			*trust_file = argv[++i];
			continue;
		}
		if (*path == NULL) {
			*path = argv[i];
			continue;
		}
		return CLI_EXIT_USAGE;
	}

	if (!path_optional && *path == NULL)
		return CLI_EXIT_USAGE;

	return CLI_EXIT_SUCCESS;
}

static int do_file_add(int argc, char * const argv[])
{
	char full_path[PATH_MAX] = { 0 };
	const char *path = NULL;
	const char *trust_file = NULL;
	bool use_filter = false;

	int rc = parse_file_args(argc, argv, &path, &trust_file,
				 &use_filter, false);
	if (rc)
		return rc;

	if (!realpath(path, full_path))
		return CLI_EXIT_PATH_CONFIG;

	return file_append(full_path, trust_file, use_filter);
}

static int do_file_delete(int argc, char * const argv[])
{
	char full_path[PATH_MAX] = { 0 };

	if (argc == 1) {
		if (!realpath(argv[0], full_path))
			return CLI_EXIT_PATH_CONFIG;
		return file_delete(full_path, NULL);
	}
	if (argc == 3) {
		if (!realpath(argv[0], full_path))
			return CLI_EXIT_PATH_CONFIG;
		if (strcmp("--trust-file", argv[1]))
			return CLI_EXIT_USAGE;
		return file_delete(full_path, argv[2]);
	}
	return CLI_EXIT_USAGE;
}

static int do_file_update(int argc, char * const argv[])
{
	char full_path[PATH_MAX] = { 0 };
	const char *path = NULL;
	const char *trust_file = NULL;
	bool use_filter = false;

	int rc = parse_file_args(argc, argv, &path, &trust_file,
				 &use_filter, true);
	if (rc)
		return rc;

	if (path) {
		if (!realpath(path, full_path))
			return CLI_EXIT_PATH_CONFIG;
		path = full_path;
	} else {
		path = "/";
	}

	return file_update(path, trust_file, use_filter);
}

static int do_manage_files(int argc, char * const argv[])
{
	int rc = CLI_EXIT_SUCCESS;

	if (argc < 1 || argc > 5) {
		fprintf(stderr, "Wrong number of arguments\n");
		fprintf(stderr, "\n%s", usage);
		return CLI_EXIT_USAGE;
	}

	if (!strcmp("add", argv[0]))
		rc = do_file_add(argc - 1, argv + 1);
	else if (!strcmp("delete", argv[0]))
		rc = do_file_delete(argc - 1, argv + 1);
	else if (!strcmp("update", argv[0]))
		rc = do_file_update(argc - 1, argv + 1);
	else {
		fprintf(stderr, "%s is not a valid option, choose one of add|delete|update\n", argv[0]);
		fprintf(stderr, "\n%s", usage);
		return CLI_EXIT_USAGE;
	}

	switch (rc) {
	case CLI_EXIT_SUCCESS: // no error
		return CLI_EXIT_SUCCESS;
	case CLI_EXIT_USAGE: // args error
		fprintf(stderr, "Wrong number of arguments\n");
                fprintf(stderr, "\n%s", usage);
                return rc;
	case CLI_EXIT_PATH_CONFIG: // realpath error
		fprintf(stderr, "Can't obtain realpath from: %s\n", argv[1]);
		fprintf(stderr, "\n%s", usage);
		return rc;
	default: // file function errors
		break;
	}
	return rc;
}


static int do_ftype(const char *path, bool print_path)
{
	int fd;
	const char *ptr = NULL;
	char buf[80];
	struct stat sb;
	struct file_info i;

	// We need to open in non-blocking mode because if its a
	// fifo, it will hang the program.
	fd = open(path, O_RDONLY|O_NONBLOCK);
	if (fd < 0) {
		fprintf(stderr, "Cannot open %s - %s\n", path, strerror(errno));
		return CLI_EXIT_IO;
	}
	if (fstat(fd, &sb) != 0) {
		fprintf(stderr, "Cannot stat %s - %s\n", path, strerror(errno));
		close(fd);
		return CLI_EXIT_IO;
	}

	// Setup file info with bare essentials
	i.device = sb.st_dev;
	i.mode = sb.st_mode;
	i.size = sb.st_size;

	if (file_init()) {
		fprintf(stderr, "Cannot initialize file helper libraries\n");
		close(fd);
		return CLI_EXIT_INTERNAL;
	}
	ptr = get_file_type_from_fd(fd, &i, path, sizeof(buf), buf);
	file_close();
	close(fd);

	if (print_path) {
		/* Keep pathname bytes from creating synthetic output records. */
		fputs(path, stdout);
		fputc('\0', stdout);
		fputs(ptr ? ptr : "unknown", stdout);
		fputc('\0', stdout);
	}
	else if (ptr)
		printf("%s\n", ptr);
	else
		printf("unknown\n");

	return CLI_EXIT_SUCCESS;
}

static int do_list(void)
{
	unsigned count = 1;
	FILE *f = fopen(OLD_RULES_FILE, "rm");
	char *buf;

	if (f == NULL) {
		f = fopen(RULES_FILE, "rm");
		if (f == NULL) {
			fprintf(stderr, "Cannot open rules file (%s)\n",
						strerror(errno));
			return CLI_EXIT_IO;
		}
	} else {
		FILE *t = fopen(RULES_FILE, "rm");
		if (t) {
			fclose(t);
			fclose(f);
			fprintf(stderr,
				"Error - old and new rules file detected. "
				"Delete one or the other.\n");
			return CLI_EXIT_PATH_CONFIG;
		}
	}

	while ((buf = get_line(f))) {
		char *str = buf;
		while (*str) {
			if (!isblank((unsigned char)*str))
				break;
			str++;
		}
		if (*str == 0) // blank line
			goto next_iteration;
		if (*str == '#') //comment line
			goto next_iteration;
		if (*str == '%') {
			printf("-> %s\n", buf);
			goto next_iteration;
		}

		printf("%u. %s\n", count, buf);
		count++;
next_iteration:
		free(buf);
	}
	fclose(f);
	return CLI_EXIT_SUCCESS;
}


static int do_reload(int code)
{
	int fd = -1;
	struct stat s;
	char command;
	char str[2];
	ssize_t ret;

	switch (code) {
	case DB:
		command = RELOAD_TRUSTDB_COMMAND;
		break;
	case RULES:
		command = RELOAD_RULES_COMMAND;
		break;
	case COMPACT:
		command = COMPACT_TRUSTDB_COMMAND;
		break;
	default:
		return CLI_EXIT_INTERNAL;
	}
	str[0] = command;
	str[1] = '\n';

	fd = open(fifo_path, O_WRONLY);
	if (fd == -1) {
		fprintf(stderr, "Open: %s -> %s\n", fifo_path, strerror(errno));
		return CLI_EXIT_DAEMON_IPC;
	}

	if (fstat(fd, &s) == -1) {
		fprintf(stderr, "Stat: %s -> %s\n", fifo_path, strerror(errno));
		close(fd);
		return CLI_EXIT_DAEMON_IPC;
	} else {
		if (!S_ISFIFO(s.st_mode)) {
			fprintf(stderr,
				"File: %s exists but it is not a pipe!\n",
				fifo_path);
			close(fd);
			return CLI_EXIT_DAEMON_IPC;
		}
		// we will require pipe to have 0660 permissions
		mode_t mode = s.st_mode & ~S_IFMT;
		if (mode != 0660) {
			fprintf(stderr,
				"File: %s has 0%o instead of 0660 \n",
				fifo_path,
				mode);
			close(fd);
			return CLI_EXIT_DAEMON_IPC;
		}
	}

	do {
		ret = write(fd, str, sizeof(str));
	} while (ret == -1 && errno == EINTR);

	if (ret != (ssize_t)sizeof(str)) {
		if (ret < 0)
			fprintf(stderr, "Write: %s -> %s\n", fifo_path,
				strerror(errno));
		else
			fprintf(stderr, "Write: %s -> short write (%zd of %zu bytes)\n",
				fifo_path, ret, sizeof(str));
		close(fd);
		return CLI_EXIT_DAEMON_IPC;
	}

	if (close(fd)) {
		fprintf(stderr,"Close: %s -> %s\n", fifo_path, strerror(errno));
		return CLI_EXIT_DAEMON_IPC;
	}

	printf("Fapolicyd was notified\n");
	return CLI_EXIT_SUCCESS;
}

static const char *bad_filesystems[] = {
	"autofs",
	"bdev",
	"binder",
	"binfmt_misc",
	"bpf",
	"cgroup",
	"cgroup2",
	"configfs",
	"cpuset",
	"debugfs",
	"devpts",
	"devtmpfs",
	"efivarfs",
	"fusectl",
	"fuse.gvfsd-fuse",
	"fuse.portal",
	"hugetlbfs",
	"mqueue",
	"nsfs",
	"overlay", // No source of trust for what's in this
	"pipefs",
	"proc",
	"pstore",
	"resctrl",
	"rpc_pipefs",
	"securityfs",
	"selinuxfs",
	"sockfs",
	"sysfs",
	"tracefs"
};
#define FS_NAMES (sizeof(bad_filesystems)/sizeof(bad_filesystems[0]))

// Returns 1 if not a real file system and 0 if its a file system we can watch
static int not_watchable(const char *type)
{
	unsigned int i;

	for (i = 0; i < FS_NAMES; i++)
		if (strcmp(bad_filesystems[i], type) == 0)
			return 1;

	return 0;
}

// Returns CLI_EXIT_SUCCESS on success or other CLI_EXIT_* codes on failure.
// Finding unwatched file systems is not considered an error
static int check_watch_fs(void)
{
	char buf[PATH_MAX * 2], device[1025], point[4097];
	char type[32], mntops[128];
	int fs_req, fs_passno, fd, found = 0, alloc_err = 0, read_err = 0;
	list_t fs, mnt;
	char *ptr, *saved, *tmp;

	set_message_mode(MSG_STDERR, DBG_YES);
	reset_config();
	if (load_daemon_config(&config)) {
		reset_config();
		return CLI_EXIT_PATH_CONFIG;
	}
	if (config.watch_fs == NULL) {
		fprintf(stderr, "File systems to watch is empty");
		reset_config();
		return CLI_EXIT_PATH_CONFIG;
	}
	tmp = strdup(config.watch_fs);
	if (tmp == NULL) {
		reset_config();
		return CLI_EXIT_INTERNAL;
	}

	list_init(&fs);
	ptr = strtok_r(tmp, ",", &saved);
	while (ptr) {
		char *index = strdup(ptr);
		char *data = strdup("0");
		if (!index || !data || list_append(&fs, index, data)) {
			free(index);
			free(data);
			alloc_err = 1;
		}
		ptr = strtok_r(NULL, ",", &saved);
	}
	free(tmp);

	fd = open("/proc/mounts", O_RDONLY);
	if (fd < 0) {
		fprintf(stderr, "Unable to open mounts\n");
		reset_config();
		list_empty(&fs);
		return CLI_EXIT_IO;
	}

	fd_fgets_state_t *st = fd_fgets_init();
	if (!st) {
		fprintf(stderr, "Failed fd_fgets_init\n");
		reset_config();
		list_empty(&fs);
		close(fd);
		return CLI_EXIT_INTERNAL;
	}

	// Build the list of mount point types
	list_init(&mnt);
	do {
		int read_rc = fd_fgets_r(st, buf, sizeof(buf), fd);

		/* A read error does not initialize buf or mark the state at EOF. */
		if (read_rc < 0) {
			fprintf(stderr, "Unable to read mounts\n");
			read_err = 1;
			break;
		}
		if (read_rc > 0) {
			sscanf(buf, "%1024s %4096s %31s %127s %d %d\n",
			       device,point, type, mntops, &fs_req, &fs_passno);
			// Some file systems are not watchable
			if (not_watchable(type))
				continue;
			char *index = strdup(type);
			char *data = strdup("0");
			if (!index || !data || list_append(&mnt, index, data)) {
				free(index);
				free(data);
				alloc_err = 1;
			}
		}
	} while (!fd_fgets_eof_r(st));
	fd_fgets_destroy(st);
	close(fd);


	// Now search the list we just built
	for (list_item_t *lptr = mnt.first; lptr; ) {
		// See if the file system is watched
		if (list_contains(&fs, lptr->index) == 0) {
			found = 1;
			printf("%s not watched\n", (char *)lptr->index);

			// Remove the file system so that we get 1 report
			char *tmpfs = strdup(lptr->index);
			if (tmpfs == NULL) {
				alloc_err = 1;
				break;
			}
			while (list_remove(&mnt, tmpfs))
                                ;
			free(tmpfs);

			// Start from the beginning
			lptr = mnt.first;
			continue;
		}
		lptr = lptr->next;
	}

	reset_config();
	list_empty(&fs);
	list_empty(&mnt);
	if (read_err)
		return CLI_EXIT_IO;
	if (alloc_err)
		return CLI_EXIT_INTERNAL;
	if (found == 0)
		printf("Nothing appears missing\n");
	return CLI_EXIT_SUCCESS;
}

// Returns 0 = everything is OK, 1 = there is a problem
static int verify_file(const char *path, off_t size, const char *sha,
		        unsigned int tsource)
{
	int fd, warn_sha = 0;
	struct stat sb;
	file_hash_alg_t alg;
	size_t digest_len, expected_len;
	const char *alg_name;

	digest_len = strlen(sha);
	alg = file_hash_alg(digest_len);
	expected_len = file_hash_length(alg) * 2;

	/*
	 * Non-RPM trust fragments historically used SHA256, but newer stores
	 * may contain longer digests (for example SHA512).  Fall back to
	 * SHA256 only when the digest length cannot be mapped to a known
	 * algorithm so legacy entries keep working.
	 */
	if (expected_len == 0)
		expected_len = file_hash_length(FILE_HASH_ALG_SHA256) * 2;
	if (alg == FILE_HASH_ALG_NONE)
		alg = FILE_HASH_ALG_SHA256;

	if (digest_len != expected_len) {
		printf("%s miscompares: cannot infer digest algorithm\n", path);
		return 1;
	}

	fd = open(path, O_RDONLY);
	if (fd < 0) {
		printf("Can't open %s (%s)\n", path, strerror(errno));
		return 1;
	}
	if (fstat(fd, &sb)) {
		printf("Can't stat %s (%s)\n", path, strerror(errno));
		close(fd);
		return 1;
	}
	if (sb.st_size != size) {
		printf("%s miscompares: file size\n", path);
		close(fd);
		return 1;
	}

	char *sha_buf = get_hash_from_fd2(fd, sb.st_size, alg);
	close(fd);

	if (sha_buf == NULL || strcmp(sha, sha_buf))
		warn_sha = 1;
	free(sha_buf);

	if (warn_sha) {
		alg_name = file_hash_alg_name(alg);
		printf("%s miscompares: %s\n", path,
		       alg_name ? alg_name : "digest");
		return 1;
	}
	return 0;
}

static int check_trustdb(void)
{
	int verification_failed = 0, exit_rc = CLI_EXIT_SUCCESS;
	unsigned long unverifiable = 0;

	set_message_mode(MSG_STDERR, DBG_NO);
	reset_config();
	if (load_daemon_config(&config)) {
		reset_config();
		return CLI_EXIT_PATH_CONFIG;
	}
	set_message_mode(MSG_QUIET, DBG_NO);
	if (decision_config_publish(&config)) {
		set_message_mode(MSG_STDERR, DBG_NO);
		fprintf(stderr, "Unable to publish decision configuration\n");
		reset_config();
		return CLI_EXIT_INTERNAL;
	}
	int rc = walk_database_start(&config);
	reset_config();
	if (rc == WALK_DATABASE_EMPTY)
		return CLI_EXIT_SUCCESS;
	if (rc)
		return CLI_EXIT_DB_ERROR;

	do {
		unsigned int tsource;
		trustdb_size_t stored_size;
		off_t size;
		char sha[FILE_DIGEST_STRING_MAX];
		char data[TRUSTDB_DATA_BUFSZ];
		char *path;

		// Get the entry and format it for use.
		walkdb_entry_t *entry = walk_database_get_entry();
		if (entry->path_is_hashed) {
			/* A SHA-512 key is not evidence that its record miscompares. */
			unverifiable++;
			continue;
		}

		path = malloc(entry->path.mv_size + 1);
		if (path == NULL) {
			fprintf(stderr, "Out of memory while verifying trust database\n");
			exit_rc = CLI_EXIT_INTERNAL;
			break;
		}
		memcpy(path, entry->path.mv_data, entry->path.mv_size);
		path[entry->path.mv_size] = '\0';
		snprintf(data, sizeof(data), "%.*s", (int) entry->data.mv_size,
			(char *) entry->data.mv_data);
		if (sscanf(data, DATA_FORMAT_IN, &tsource, &stored_size, sha) != 3 ||
		    trustdb_size_to_off_t(stored_size, &size)) {
			fprintf(stderr, "%s data entry is corrupted\n", path);
			verification_failed = 1;
			free(path);
			continue;
		}
		if (verify_file(path, size, sha, tsource))
			verification_failed = 1;
		free(path);
	} while ((rc = walk_database_next()) == WALK_DATABASE_NEXT);

	walk_database_finish();

	if (rc == WALK_DATABASE_NEXT_ERROR)
		exit_rc = CLI_EXIT_DB_ERROR;
	if (unverifiable) {
		fprintf(stderr,
			"Warning: %lu hashed trust database %s could not be "
			"verified: the original %s %s unavailable\n",
			unverifiable, unverifiable == 1 ? "entry" : "entries",
			unverifiable == 1 ? "path" : "paths",
			unverifiable == 1 ? "is" : "are");
	}
	if (verification_failed && exit_rc == CLI_EXIT_SUCCESS)
		exit_rc = CLI_EXIT_GENERIC;
	if (!verification_failed && unverifiable == 0 &&
	    exit_rc == CLI_EXIT_SUCCESS)
		puts("No problems found");

	return exit_rc;
}

static int is_link(const char *path)
{
	struct stat sb;

	if (lstat(path, &sb)) {
		fprintf(stderr, "Can't stat %s\n", path);
		return -1;
	}
	if (S_ISLNK(sb.st_mode))
		return 1;

	return 0;
}

// Check that the file is in the trust db
static int path_found = 0;
static int path_scan_error = 0;
static int check_file(const char *fpath,
		const struct stat *sb,
		int typeflag,
		struct FTW *s_unused __attribute__ ((unused)))
{
	int ret = FTW_CONTINUE;
	struct file_info info = { 0 };
	struct stat opened;
	int fd;

	/* nftw() leaves sb undefined for FTW_NS entries. */
	if (typeflag == FTW_NS || typeflag == FTW_DNR) {
		fprintf(stderr, "Unable to inspect %s during PATH scan\n", fpath);
		path_scan_error = 1;
		return ret;
	}
	if (typeflag != FTW_F || S_ISREG(sb->st_mode) == 0)
		return ret;

	fd = open_verified_regular_file(fpath, sb, &opened);
	if (fd < 0) {
		fprintf(stderr, "Unable to safely open %s (%s)\n", fpath,
			strerror(errno));
		path_scan_error = 1;
		return ret;
	}

	info.device = opened.st_dev;
	info.inode = opened.st_ino;
	info.mode = opened.st_mode;
	info.size = opened.st_size;
	info.time = opened.st_mtim;
	if (check_trust_database_readonly(fpath, &info, fd) != 1) {
		path_found = 1;
		fprintf(stderr, "%s is not trusted\n", fpath);
	}

	close(fd);
	return ret;
}

static int check_path(void)
{
	char *ptr, *saved;
	int link, walk_rc;
	const char *env_path = getenv("PATH");
	if (env_path == NULL) {
		puts("PATH not found");
		return CLI_EXIT_PATH_CONFIG;
	}

	set_message_mode(MSG_STDERR, DBG_NO);
	reset_config();
	if (load_daemon_config(&config)) {
		reset_config();
		return CLI_EXIT_PATH_CONFIG;
	}
	set_message_mode(MSG_QUIET, DBG_NO);
	if (decision_config_publish(&config)) {
		set_message_mode(MSG_STDERR, DBG_NO);
		fprintf(stderr, "Unable to publish decision configuration\n");
		reset_config();
		return CLI_EXIT_INTERNAL;
	}
	int rc = database_readonly_lookup_start();
	if (rc) {
		set_message_mode(MSG_STDERR, DBG_NO);
		fprintf(stderr, "Cannot initialize trust database (%d)\n", rc);
		reset_config();
		return CLI_EXIT_DB_ERROR;
	}
	path_found = 0;
	path_scan_error = 0;
	char *path = strdup(env_path);
	if (path == NULL) {
		fprintf(stderr, "Out of memory while reading PATH\n");
		database_readonly_lookup_finish();
		reset_config();
		return CLI_EXIT_INTERNAL;
	}
	ptr = strtok_r(path, ":", &saved);
	while (ptr) {
		link = is_link(ptr);
		if (link) {
			if (link < 0)
				path_scan_error = 1;
			goto next;
		}

		walk_rc = nftw(ptr, check_file, 1024, FTW_PHYS);
		if (walk_rc) {
			fprintf(stderr, "Unable to scan %s (%s)\n", ptr,
				strerror(errno));
			path_scan_error = 1;
		}
next:
		ptr = strtok_r(NULL, ":", &saved);
	}
	free(path);
	database_readonly_lookup_finish();
	reset_config();

	if (path_found == 0 && path_scan_error == 0)
		puts("No problems found");
	if (path_scan_error)
		return CLI_EXIT_IO;

	return CLI_EXIT_SUCCESS;
}

/*
 * confirm_metric_reset - ask before sending a SIGUSR1 reset intent.
 * Returns 1 when the caller confirms, 0 otherwise.
 */
static int confirm_metric_reset(void)
{
	char answer[8];

	fprintf(stderr,
		"Request runtime metrics reset with this metrics report? [y/N] ");
	fflush(stderr);
	if (fgets(answer, sizeof(answer), stdin) == NULL)
		return 0;

	return answer[0] == 'y' || answer[0] == 'Y';
}

/*
 * confirm_timing_report_overwrite - ask before replacing a timing report.
 * Returns 1 when the caller confirms, 0 otherwise.
 */
static int confirm_timing_report_overwrite(void)
{
	char answer[8];

	fprintf(stderr,
		"Existing timing report %s may contain a long diagnostic run.\n",
		TIMING_REPORT);
	fprintf(stderr, "Overwrite it with this stop request? [y/N] ");
	fflush(stderr);
	if (fgets(answer, sizeof(answer), stdin) == NULL)
		return 0;

	return answer[0] == 'y' || answer[0] == 'Y';
}

/*
 * prepare_timing_report_overwrite - protect an existing timing report.
 * Returns a CLI_EXIT_* value.
 */
static int prepare_timing_report_overwrite(void)
{
	struct stat st;

	if (stat(TIMING_REPORT, &st)) {
		if (errno == ENOENT)
			return CLI_EXIT_SUCCESS;
		fprintf(stderr, "Cannot stat %s: %s\n", TIMING_REPORT,
			strerror(errno));
		return CLI_EXIT_IO;
	}

	if (!confirm_timing_report_overwrite()) {
		fprintf(stderr, "Leaving existing timing report unchanged\n");
		return CLI_EXIT_NOOP;
	}

	if (unlink(TIMING_REPORT) && errno != ENOENT) {
		fprintf(stderr, "Cannot remove %s: %s\n", TIMING_REPORT,
			strerror(errno));
		return CLI_EXIT_IO;
	}

	return CLI_EXIT_SUCCESS;
}

/*
 * check_metric_reset_strategy - verify reset intent against on-disk config.
 * @reset_metrics: reset intent flag to clear when manual reset is unlikely.
 * Returns nothing.
 */
static void check_metric_reset_strategy(int *reset_metrics)
{
	conf_t disk_config;
	const char *strategy;

	if (!*reset_metrics)
		return;

	memset(&disk_config, 0, sizeof(disk_config));
	set_message_mode(MSG_STDERR, DBG_NO);
	if (load_daemon_config(&disk_config)) {
		fprintf(stderr,
			"Unable to verify reset_strategy in %s; sending a "
			"plain --check-metrics request.\n", CONFIG_FILE);
		fprintf(stderr,
			"The daemon's active setting may differ from the "
			"on-disk configuration.\n");
		*reset_metrics = 0;
		free_daemon_config(&disk_config);
		set_message_mode(MSG_QUIET, DBG_NO);
		return;
	}
	strategy = lookup_reset_strategy(disk_config.reset_strategy);

	if (disk_config.reset_strategy != RESET_MANUAL) {
		fprintf(stderr,
			"On-disk reset_strategy is %s, not manual; "
			"--reset-metrics appears unable to reset metrics.\n",
			strategy ? strategy : "unknown");
		fprintf(stderr,
			"Sending a plain --check-metrics request. The daemon's "
			"active setting may differ until config is reloaded.\n");
		*reset_metrics = 0;
	}

	free_daemon_config(&disk_config);
	set_message_mode(MSG_QUIET, DBG_NO);
}

/*
 * send_state_report_signal - request a report from fapolicyd.
 * @pid: daemon PID to signal.
 * @intent: report intent to send.
 * @reason: error text buffer.
 * @reason_len: size of @reason.
 * Returns 0 on success, non-zero on failure.
 */
static int send_state_report_signal(unsigned int pid, report_intent_t intent,
		char *reason, size_t reason_len)
{
	union sigval value;

	value.sival_int = intent;
	if (sigqueue(pid, SIGUSR1, value)) {
		snprintf(reason, reason_len, "signal failed: %s",
			 strerror(errno));
		return 1;
	}
	return 0;
}

/*
 * send_timing_signal - send a SIGUSR1 timing intent to fapolicyd.
 * @pid: daemon PID to signal.
 * @intent: timing intent to send.
 * @reason: error text buffer.
 * @reason_len: size of @reason.
 * Returns 0 on success, non-zero on failure.
 */
static int send_timing_signal(unsigned int pid, report_intent_t intent,
		char *reason, size_t reason_len)
{
	union sigval value;

	value.sival_int = intent;
	if (sigqueue(pid, SIGUSR1, value)) {
		snprintf(reason, reason_len, "signal failed: %s",
			 strerror(errno));
		return 1;
	}
	return 0;
}

/*
 * get_daemon_pid - read and validate the fapolicyd pid file.
 * @pid: output pid on success.
 * @reason: error text buffer.
 * @reason_len: size of @reason.
 * Returns 0 on success, non-zero on failure.
 */
static int get_daemon_pid(unsigned int *pid, char *reason, size_t reason_len)
{
	fd_fgets_state_t *st;
	int pidfd;

	st = fd_fgets_init();
	if (!st) {
		snprintf(reason, reason_len, "internal allocation failure");
		return 1;
	}

	pidfd = open(pidfile, O_RDONLY);
	if (pidfd >= 0) {
		char pid_buf[16];
		int read_rc;

		read_rc = fd_fgets_r(st, pid_buf, sizeof(pid_buf), pidfd);
		if (read_rc > 0) {
			char exe_buf[64];

			errno = 0;
			*pid = strtoul(pid_buf, NULL, 10);
			if (errno) {
				snprintf(reason, reason_len,
					 "bad pid in pid file");
				goto err_out;
			}
			if (get_program_from_pid(*pid, sizeof(exe_buf),
					exe_buf) == NULL) {
				snprintf(reason, reason_len,
					 "can't read proc file");
				goto err_out;
			}
			if (strcmp(basename(exe_buf), "fapolicyd")) {
				snprintf(reason, reason_len,
					 "pid file doesn't point to fapolicyd");
				goto err_out;
			}
			close(pidfd);
			fd_fgets_destroy(st);
			return 0;
		}
		if (read_rc < 0)
			snprintf(reason, reason_len, "can't read pid file");
		else
			snprintf(reason, reason_len, "unreadable pid file");
	} else
		snprintf(reason, reason_len, "no pid file");

err_out:
	if (pidfd >= 0)
		close(pidfd);
	fd_fgets_destroy(st);
	return 1;
}

/*
 * display_report_file - wait for a daemon report and write it to stdout.
 * @path: report path to read.
 * @reason: output error reason on failure.
 * Returns 0 on success, non-zero on timeout or I/O failure.
 */
static int display_report_file(const char *path, const char **reason)
{
	fd_fgets_state_t *st;
	unsigned int tries = 0;
	int rpt_fd;

	st = fd_fgets_init();
	if (!st) {
		*reason = "internal allocation failure";
		return 1;
	}

retry:
	sleep(1);

	rpt_fd = open(path, O_RDONLY);
	if (rpt_fd < 0) {
		if (tries < 25) {
			tries++;
			goto retry;
		}
		*reason = "timed out waiting for report";
		fd_fgets_destroy(st);
		return 1;
	}

	fd_fgets_clear_r(st);
	for (;;) {
		char buf[80];
		int read_rc;

		read_rc = fd_fgets_r(st, buf, sizeof(buf), rpt_fd);
		/* fd_fgets_r() leaves buf and EOF unchanged on read errors. */
		if (read_rc < 0) {
			*reason = "failed reading report";
			close(rpt_fd);
			fd_fgets_destroy(st);
			return 1;
		}
		if (read_rc > 0)
			write(1, buf, strlen(buf));
		if (fd_fgets_eof_r(st))
			break;
	}

	close(rpt_fd);
	fd_fgets_destroy(st);
	return 0;
}

/*
 * do_timing_control - request a manual decision timing control action.
 * @intent: timing intent to send.
 * Returns a CLI_EXIT_* value.
 */
static int do_timing_control(report_intent_t intent)
{
	const char *reason;
	unsigned int pid;
	char signal_reason[80];

	if (get_daemon_pid(&pid, signal_reason, sizeof(signal_reason))) {
		printf("Can't find fapolicyd: %s\n", signal_reason);
		return CLI_EXIT_DAEMON_IPC;
	}

	if (intent == REPORT_INTENT_TIMING_STOP) {
		int rc = prepare_timing_report_overwrite();

		if (rc != CLI_EXIT_SUCCESS)
			return rc;
	}

	if (send_timing_signal(pid, intent, signal_reason,
			       sizeof(signal_reason))) {
		printf("Can't signal fapolicyd: %s\n", signal_reason);
		return CLI_EXIT_DAEMON_IPC;
	}

	if (intent == REPORT_INTENT_TIMING_ARM)
		printf("Decision timing start requested\n");
	else if (display_report_file(TIMING_REPORT, &reason)) {
		printf("Can't read decision timing report: %s\n", reason);
		return CLI_EXIT_DAEMON_IPC;
	}

	return CLI_EXIT_SUCCESS;
}

/*
 * do_status_report - request and display a daemon report.
 * @intent: report intent to send.
 * @reset_metrics: non-zero when the user requested a metrics reset.
 * Returns a CLI_EXIT_* value.
 */
static int do_status_report(report_intent_t intent, int reset_metrics)
{
	const char *reason = "no pid file";
	const char *report_path;
	unsigned int pid;
	char signal_reason[80];
	int fd;

	if (get_daemon_pid(&pid, signal_reason, sizeof(signal_reason))) {
		printf("Can't find fapolicyd: %s\n", signal_reason);
		return CLI_EXIT_DAEMON_IPC;
	}

	check_metric_reset_strategy(&reset_metrics);
	if (reset_metrics && !assume_yes && !confirm_metric_reset())
		return CLI_EXIT_NOOP;
	if (reset_metrics)
		intent = REPORT_INTENT_RESET_METRICS;
	else if (intent == REPORT_INTENT_RESET_METRICS)
		intent = REPORT_INTENT_METRICS;
	report_path = intent == REPORT_INTENT_STATUS ?
		STAT_REPORT : METRICS_REPORT;

	// delete the old report
	unlink(report_path);

	// send the signal for the report
	if (send_state_report_signal(pid, intent, signal_reason,
				     sizeof(signal_reason))) {
		printf("Can't signal fapolicyd: %s\n", signal_reason);
		return CLI_EXIT_DAEMON_IPC;
	}

	// Access a file to provoke a response
	fd = open(CONFIG_FILE, O_RDONLY);
	if (fd >= 0)
		close(fd);

	if (display_report_file(report_path, &reason)) {
		printf("Can't read daemon report: %s\n", reason);
		return CLI_EXIT_DAEMON_IPC;
	}

	return CLI_EXIT_SUCCESS;
}

#ifdef HAVE_LIBRPM
static int do_test_filter(const char *path)
{
	set_message_mode(MSG_STDERR, DBG_NO);
	filter_set_trace(stdout);

	if (filter_init()) {
		fprintf(stderr, "filter_init failed\n");
		return CLI_EXIT_RULE_FILTER;
	}
	if (filter_load_file(FILTER_FILE)) {
		filter_destroy();
		fprintf(stderr, "filter_load_file failed\n");
		return CLI_EXIT_RULE_FILTER;
	}
	filter_check(path);
	filter_destroy();
	return CLI_EXIT_SUCCESS;
}
#endif

/*
 * run_cli_command - execute the requested CLI command.
 * @arg_count: number of sanitized arguments.
 * @args: sanitized argument vector.
 * Returns a CLI_EXIT_* status code.
 */
static int run_cli_command(int arg_count, char **args)
{
	int opt, option_index, rc = CLI_EXIT_GENERIC;

	if (arg_count == 1) {
		fprintf(stderr, "Too few arguments\n\n");
		fprintf(stderr, "%s", usage);
		return CLI_EXIT_USAGE;
	}

	optind = 1;

	/* Run getopt_long on the sanitized copy so command parsing behaves
	 * exactly as before --verbose was introduced. */
	opt = getopt_long(arg_count, (char * const *)args, "Ddf:ht:T:lury",
				 long_opts, &option_index);

	if (assume_yes && opt != 11)
		goto args_err;

	switch (opt) {
	case 'd':
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		rc = do_delete_db();
		break;
	case 'D':
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		rc = do_dump_db();
		break;
	case 'f':
		if (lint_rules)
			goto args_err;
		if (arg_count > 7)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT))
			return CLI_EXIT_IO;
		// fapolicyd-cli, -f, | operation, path ...
		// skip the first two args
		rc = do_manage_files(arg_count-2, args+2);
		break;
	case 'h':
		if (lint_rules)
			goto args_err;
		printf("%s", usage);
		rc = CLI_EXIT_SUCCESS;
		break;
	case 't':
	case 'T':
		if (lint_rules)
			goto args_err;
		if (arg_count > 3)
			goto args_err;
		rc = do_ftype(optarg, opt == 'T');
		break;
	case 'l':
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		rc = do_list();
		break;
	case 'u':
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		rc = do_reload(DB);
		break;
	case 'r':
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		rc = do_reload(RULES);
		break;

	// Now the pure long options
	case 1: { // --check-config

		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		set_message_mode(MSG_STDERR, DBG_YES);
		reset_config();
		if (load_daemon_config(&config)) {
			reset_config();
			fprintf(stderr, "Configuration errors reported\n");
			return CLI_EXIT_PATH_CONFIG;
		} else {
			printf("Daemon config is OK\n");
			reset_config();
			return CLI_EXIT_SUCCESS;
		} }
		break;
	case 2: // --check-watch_fs
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		return check_watch_fs();
		break;
	case 3: // --check-trustdb
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		return check_trustdb();
		break;
	case 4: // --check-status
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT))
			return CLI_EXIT_IO;
		return do_status_report(REPORT_INTENT_STATUS, 0);
		break;
	case 5: // --check-path
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		return check_path();
		break;

	case 14: // --check-metrics
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT))
			return CLI_EXIT_IO;
		return do_status_report(REPORT_INTENT_METRICS, 0);
		break;

	case 15: // --compact-trustdb
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		return do_reload(COMPACT);
		break;

	case 7: { // --check-ignore_mounts
		const char *override = optarg;

		if (lint_rules)
			goto args_err;
		if (override == NULL && optind < arg_count &&
						args[optind][0] != '-')
			override = args[optind++];
		if (optind < arg_count)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		return check_ignore_mounts(override);
		}
		break;

	case 9: { // --check-rules
		const char *path = optarg;

		if (path == NULL && optind < arg_count &&
						args[optind][0] != '-')
			path = args[optind++];
		if (optind < arg_count)
			goto args_err;
		if ((path == NULL || !strcmp(path, OLD_RULES_FILE) ||
		     !strcmp(path, RULES_FILE)) &&
		    cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		return check_rules_file(path, lint_rules);
		}
		break;

	case 10:
		goto args_err;

	case 11: // --reset-metrics
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT))
			return CLI_EXIT_IO;
		return do_status_report(REPORT_INTENT_RESET_METRICS, 1);

	case 12: // --timing-start
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT))
			return CLI_EXIT_IO;
		return do_timing_control(REPORT_INTENT_TIMING_ARM);

	case 13: // --timing-stop
		if (lint_rules)
			goto args_err;
		if (arg_count > 2)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT))
			return CLI_EXIT_IO;
		return do_timing_control(REPORT_INTENT_TIMING_STOP);

	case 'y':
		goto args_err;

#ifdef HAVE_LIBRPM
	case 6: { // --test-filter
		if (lint_rules)
			goto args_err;
		if (arg_count > 3)
			goto args_err;
		if (cli_require_privilege(CLI_PRIVILEGE_ROOT_OR_GROUP))
			return CLI_EXIT_IO;
		return do_test_filter(optarg);
		}
		break;
#endif
	default:
		printf("%s", usage);
		rc = CLI_EXIT_USAGE;
	}
	return rc;

args_err:
	fprintf(stderr, "Too many arguments\n\n");
	fprintf(stderr, "%s", usage);
	return CLI_EXIT_USAGE;
}

int main(int argc, char * const argv[])
{
	int arg_count = 0;
	char **args;
	int rc;

	args = calloc(argc + 1, sizeof(*args));
	if (args == NULL) {
		fprintf(stderr, "Cannot allocate argument list\n");
		return CLI_EXIT_GENERIC;
	}

	for (int i = 0; i < argc; i++) {
		if (strcmp(argv[i], "--verbose") == 0) {
			verbose = true;
			continue;
		}
		if (strcmp(argv[i], "--lint") == 0) {
			lint_rules = true;
			continue;
		}
		if (strcmp(argv[i], "--yes") == 0 ||
		    strcmp(argv[i], "-y") == 0) {
			assume_yes = true;
			continue;
		}
		args[arg_count++] = argv[i];
	}
	args[arg_count] = NULL;

	rc = run_cli_command(arg_count, args);
	free(args);
	return rc;
}
