/*-
 * Copyright (c) 2011-2012 Julien Laffaye <jlaffaye@FreeBSD.org>
 * Copyright (c) 2014-2015 Matthew Seaman <matthew@FreeBSD.org>
 * Copyright (c) 2014 Vsevolod Stakhov <vsevolod@FreeBSD.org>
 * Copyright (c) 2011-2024 Baptiste Daroussin <matthew@FreeBSD.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include "pkg_config.h"

#include <sys/param.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <archive.h>
#include <err.h>
#include <errno.h>
#include <fts.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <ucl.h>

#if __has_include(<sys/capsicum.h>)
#define HAVE_CAPSICUM 1
#include <sys/capsicum.h>
#endif

#include <pkg.h>
#include <pkg/audit.h>
#include "pkgcli.h"
#include "xmalloc.h"
#include "pkghash.h"

static const char* vop_names[] = {
	[0] = "",
	[EQ] = "=",
	[LT] = "<",
	[LTE] = "<=",
	[GT] = ">",
	[GTE] = ">="
};

void
usage_audit(void)
{
	fprintf(stderr, "Usage: pkg audit [-RFqr] [--raw[=format]|-R[format] [-f file] <pattern>\n\n");
	fprintf(stderr, "For more information see 'pkg help audit'.\n");
}

static void
add_to_check(pkghash *check, struct pkg *pkg)
{
	const char *uid;

	pkg_get(pkg, PKG_ATTR_UNIQUEID, &uid);
	pkghash_safe_add(check, uid, pkg, NULL);
}

static void
print_recursive_rdeps(pkghash *head, struct pkg *p, pkghash *seen, bool top, ucl_object_t *array)
{
	pkghash_entry *e;
	struct pkg_dep *dep = NULL;

	while(pkg_rdeps(p, &dep) == EPKG_OK) {
		const char *name = pkg_dep_get(dep, PKG_DEP_NAME);

		if (pkghash_get(seen, name) != NULL)
			continue;

		if ((e = pkghash_get(head, name)) == NULL)
			continue;

		pkghash_safe_add(seen, name, NULL, NULL);
		if (array == NULL) {
			if (!top)
				printf(", ");

			printf("%s", name);
		} else {
			ucl_array_append(array, ucl_object_fromstring(name));
		}

		print_recursive_rdeps(head, (struct pkg *)e->value, seen, false, array);

		top = false;
	}
}

static void
print_issue(struct pkg *p, struct pkg_audit_issue *issue)
{
	const char *version = NULL;
	const struct pkg_audit_entry *e;

	pkg_get(p, PKG_ATTR_VERSION, &version);

	e = issue->audit;
	if (version == NULL) {
		const audit_versv_t *versions =
		    &e->packages.d[issue->pkg_idx].versions;
		printf("  Affected versions:\n");
		vec_foreach(*versions, _vi) {
			const struct pkg_audit_versions_range *vers =
			    &versions->d[_vi];
			if (vers->v1.type > 0 && vers->v2.type > 0)
				printf("  %s %s : %s %s\n",
				    vop_names[vers->v1.type], vers->v1.version,
				    vop_names[vers->v2.type], vers->v2.version);
			else if (vers->v1.type > 0)
				printf("  %s %s\n",
				    vop_names[vers->v1.type], vers->v1.version);
			else
				printf("  %s %s\n",
				    vop_names[vers->v2.type], vers->v2.version);
		}
	}
	printf("  %s\n", e->desc);
	vec_foreach(e->cve, _ci) {
		printf("  CVE: %s\n", e->cve.d[_ci].cvename);
	}
	if (e->url)
		printf("  WWW: %s\n\n", e->url);
	else if (e->id)
		printf("  WWW: https://vuxml.FreeBSD.org/freebsd/%s.html\n\n", e->id);
}

static void
format_issue(struct pkg_audit_issue *issue, ucl_object_t *array)
{
	const struct pkg_audit_entry *e;
	ucl_object_t *o = ucl_object_typed_new(UCL_OBJECT);
	ucl_object_t *affected_versions = ucl_object_typed_new(UCL_ARRAY);

	ucl_array_append(array, o);

	e = issue->audit;
	ucl_object_insert_key(o, affected_versions, "Affected versions", 17, false);
	const audit_versv_t *versions = &e->packages.d[issue->pkg_idx].versions;
	vec_foreach(*versions, _vi) {
		const struct pkg_audit_versions_range *vers = &versions->d[_vi];
		char *ver;
		if (vers->v1.type > 0 && vers->v2.type > 0)
			xasprintf(&ver, "%s %s : %s %s",
			    vop_names[vers->v1.type], vers->v1.version,
			    vop_names[vers->v2.type], vers->v2.version);
		else if (vers->v1.type > 0)
			xasprintf(&ver, "%s %s",
			    vop_names[vers->v1.type], vers->v1.version);
		else
			xasprintf(&ver, "%s %s",
			    vop_names[vers->v2.type], vers->v2.version);
		ucl_array_append(affected_versions, ucl_object_fromstring(ver));
		free(ver);
	}
	ucl_object_insert_key(o, ucl_object_fromstring(e->desc), "description", 11, false);
	if (e->cve.len > 0) {
		ucl_object_t *acve = ucl_object_typed_new(UCL_ARRAY);
		vec_foreach(e->cve, _ci) {
			ucl_array_append(acve, ucl_object_fromstring(e->cve.d[_ci].cvename));
		}
		ucl_object_insert_key(o, acve, "cve", 3, false);
	}
	if (e->url)
		ucl_object_insert_key(o, ucl_object_fromstring(e->url), "url", 3, false);
	else if (e->id) {
		char *url;
		xasprintf(&url, "https://vuxml.FreeBSD.org/freebsd/%s.html", e->id);
		ucl_object_insert_key(o, ucl_object_fromstring(url), "url", 3, false);
		free(url);
	}
}

int
exec_audit(int argc, char **argv)
{
	struct pkg_audit	*audit;
	struct pkg_audit_issues	*issues;
	struct pkg_audit_issue	*issue;
	struct pkgdb		*db = NULL;
	struct pkgdb_it		*it = NULL;
	struct pkg		*pkg = NULL;
	char			*name;
	char			*version;
	char			*audit_file = NULL;
	char			*dirname = NULL;
	int			 affected = 0, vuln = 0;
	bool			 fetch = false, recursive = false;
	int			 ch, i;
	int			 raw;
	int			 ret = EXIT_SUCCESS;
	pkghash			*check = NULL;
	ucl_object_t		*top = NULL, *vuln_objs = NULL;
	ucl_object_t		*obj = NULL;

	struct option longopts[] = {
		{ "directory",	required_argument,	NULL,	'd' },
		{ "fetch",	no_argument,		NULL,	'F' },
		{ "file",	required_argument,	NULL,	'f' },
		{ "recursive",	no_argument,	NULL,	'r' },
		{ "raw",	optional_argument,	NULL,	'R' },
		{ "quiet",	no_argument,		NULL,	'q' },
		{ NULL,		0,			NULL,	0   },
	};

	while ((ch = getopt_long(argc, argv, "+d:Ff:qrR::", longopts, NULL)) != -1) {
		switch (ch) {
		case 'd':
			dirname = optarg;
			break;
		case 'F':
			fetch = true;
			break;
		case 'f':
			audit_file = optarg;
			break;
		case 'q':
			quiet = true;
			break;
		case 'r':
			recursive = true;
			break;
		case 'R':
			if (optarg == NULL) {
				raw = UCL_EMIT_CONFIG;
			} else if (STRIEQ(optarg, "ucl")) {
				raw = UCL_EMIT_CONFIG;
			} else if (STRIEQ(optarg, "json")) {
				raw = UCL_EMIT_JSON;
			} else if (STRIEQ(optarg, "json-compact")) {
				raw = UCL_EMIT_JSON_COMPACT;
			} else if (STRIEQ(optarg, "yaml")) {
				raw = UCL_EMIT_YAML;
			} else {
				errx(EXIT_FAILURE, "invalid argument %s for --raw option", optarg);
			}
			top = ucl_object_typed_new(UCL_OBJECT);
			break;
		default:
			usage_audit();
			return(EXIT_FAILURE);
		}
	}
	argc -= optind;
	argv += optind;

	audit = pkg_audit_new();

	if (fetch) {
		if (pkg_audit_fetch(NULL, audit_file) != EPKG_OK) {
			pkg_audit_free(audit);
			return (EXIT_FAILURE);
		}
	}
	if (dirname != NULL && argc > 1) {
		warnx("No argument expected with -d");
		usage_audit();
		return (EXIT_FAILURE);
	}

	if (pkg_audit_load(audit, audit_file) != EPKG_OK) {
		if (errno == ENOENT)
			warnx("vulnxml file %s does not exist. "
					"Try running 'pkg audit -F' first",
			    audit_file == NULL ? "vuln.xml" : audit_file);
		else
			warn("unable to open vulnxml file %s",
					audit_file);

		pkg_audit_free(audit);
		return (EXIT_FAILURE);
	}

	check = pkghash_new();
	if (dirname != NULL) {
		char * path[2];
		FTSENT *fts_ent;
		path[0] = dirname;
		path[1] = NULL;
		FTS *fts = fts_open(path, FTS_PHYSICAL|FTS_NOSTAT, NULL);
		if (fts == NULL)
			err(EXIT_FAILURE, "fts_open(%s)", dirname);
		while ((fts_ent = fts_read(fts)) != NULL) {
			char *ext = strrchr(fts_ent->fts_name, '.');
			if (ext == NULL)
				continue;
			if (!STREQ(ext, ".pkg"))
				continue;
			*ext = '\0';
			ext = strrchr(fts_ent->fts_name, '-');
			if (ext == NULL)
				continue;
			*ext = '\0';
			ext++;
			if (pkg_new(&pkg, PKG_FILE) != EPKG_OK)
				err(EXIT_FAILURE, "malloc");
			pkg_set(pkg, PKG_ATTR_NAME, fts_ent->fts_name);
			pkg_set(pkg, PKG_ATTR_VERSION, ext);
			add_to_check(check, pkg);
			pkg = NULL;
		}
		fts_close(fts);
	} else if (argc >= 1) {
		for (i = 0; i < argc; i ++) {
			name = argv[i];
			version = strrchr(name, '-');
			if (version != NULL) {
				version[0] = '\0';
				version++;
			}
			if (pkg_new(&pkg, PKG_FILE) != EPKG_OK)
				err(EXIT_FAILURE, "malloc");
			pkg_set(pkg, PKG_ATTR_NAME, name);
			if (version != NULL)
				pkg_set(pkg, PKG_ATTR_VERSION, version);
			add_to_check(check, pkg);
			pkg = NULL;
		}
	}
	else {

		/*
		 * if the database doesn't exist it just means there are no
		 * packages to audit.
		 */

		ret = pkgdb_access(PKGDB_MODE_READ, PKGDB_DB_LOCAL);
		if (ret == EPKG_ENODB) {
			pkg_audit_free(audit);
			pkghash_destroy(check);
			return (EXIT_SUCCESS);
		} else if (ret == EPKG_ENOACCESS) {
			warnx("Insufficient privileges to read the package database");
			pkg_audit_free(audit);
			pkghash_destroy(check);
			return (EXIT_FAILURE);
		} else if (ret != EPKG_OK) {
			warnx("Error accessing the package database");
			pkg_audit_free(audit);
			pkghash_destroy(check);
			return (EXIT_FAILURE);
		}

		if (pkgdb_open(&db, PKGDB_DEFAULT_READONLY) != EPKG_OK) {
			pkg_audit_free(audit);
			pkghash_destroy(check);
			return (EXIT_FAILURE);
		}

		if (pkgdb_obtain_lock(db, PKGDB_LOCK_READONLY) != EPKG_OK) {
			pkgdb_close(db);
			pkg_audit_free(audit);
			pkghash_destroy(check);
			warnx("Cannot get a read lock on a database, it is locked by another process");
			return (EXIT_FAILURE);
		}

		if ((it = pkgdb_query(db, NULL, MATCH_ALL)) == NULL) {
			warnx("Error accessing the package database");
			ret = EXIT_FAILURE;
		}
		else {
			while (pkgdb_it_next(it, &pkg, PKG_LOAD_BASIC|PKG_LOAD_RDEPS)
							== EPKG_OK) {
				add_to_check(check, pkg);
				pkg = NULL;
			}
			ret = EXIT_SUCCESS;
		}
		if (db != NULL) {
			pkgdb_it_free(it);
			pkgdb_release_lock(db, PKGDB_LOCK_READONLY);
		}
		if (ret != EXIT_SUCCESS) {
			pkg_audit_free(audit);
			pkghash_destroy(check);
			return (ret);
		}
	}

	pkg_drop_privileges();

	/* Now we have vulnxml loaded and check list formed */
#ifdef HAVE_CAPSICUM
#ifndef COVERAGE
	if (cap_enter() < 0 && errno != ENOSYS) {
		warn("cap_enter() failed");
		pkg_audit_free(audit);
		pkghash_destroy(check);
		return (EPKG_FATAL);
	}
#endif
#endif

	if (pkg_audit_process(audit) == EPKG_OK) {
		pkghash_foreach(check, hit) {
			issues = NULL;
			pkg = (struct pkg *) hit.value;
			if (pkg_audit_is_vulnerable(audit, pkg, &issues, quiet)) {
				const char *version = NULL;
				const char *name = NULL;
				ucl_object_t *array = NULL;
				vuln ++;

				if (top == NULL) {
					affected += issues->issues.len;
					pkg_get(pkg, PKG_ATTR_VERSION, &version);
					if (quiet) {
						if (version != NULL)
							pkg_printf("%n-%v\n", pkg, pkg);
						else
							pkg_printf("%s\n", pkg);
						pkg_audit_issues_free(issues);
						continue;
					}

					pkg_printf("%n", pkg);
					if (version != NULL)
						pkg_printf("-%v", pkg);
					if (!quiet)
						printf(" is vulnerable");
					printf(":\n");
				} else {
					if (vuln_objs == NULL)
						vuln_objs = ucl_object_typed_new(UCL_OBJECT);
					obj = ucl_object_typed_new(UCL_OBJECT);
					pkg_get(pkg, PKG_ATTR_NAME, &name);
					pkg_get(pkg, PKG_ATTR_VERSION, &version);
					if (version != NULL)
						ucl_object_insert_key(obj, ucl_object_fromstring(version), "version", 7 , false);
					ucl_object_insert_key(obj, ucl_object_fromint(issues->issues.len), "issue_count", 11, false);
				}

				if (top != NULL)
					array = ucl_object_typed_new(UCL_ARRAY);
				vec_foreach(issues->issues, _i) {
					issue = &issues->issues.d[_i];
					if (top == NULL)
						print_issue(pkg, issue);
					else
						format_issue(issue, array);
				}
				if (top != NULL)
					ucl_object_insert_key(obj, array, "issues", 6, false);
				array = NULL;

				if (top != NULL || recursive) {
					pkghash *seen = pkghash_new();

					if (name == NULL)
						pkg_get(pkg, PKG_ATTR_NAME, &name);
					if (top == NULL) {
						printf("  Packages that depend on %s: ", name);
					} else {
						array = ucl_object_typed_new(UCL_ARRAY);
					}
					print_recursive_rdeps(check, pkg , seen, true, array);
					if (top == NULL)
						printf("\n\n");

					pkghash_destroy(seen);
				}
				if (top != NULL) {
					ucl_object_insert_key(obj, array, "reverse dependencies", 20, false);
					ucl_object_insert_key(vuln_objs, obj, name, strlen(name), true);
				}
			}
			pkg_audit_issues_free(issues);
		}
		pkghash_foreach(check, hit) {
			pkg_free(hit.value);
		}
		pkghash_destroy(check);

		if (ret == EPKG_END && vuln == 0)
			ret = EXIT_SUCCESS;

		if (top == NULL) {
			if (!quiet)
				printf("%u problem(s) in %u package(s) found.\n",
				   affected, vuln);

		} else {
			ucl_object_insert_key(top, ucl_object_fromint(vuln), "pkg_count", 9, false );
			ucl_object_insert_key(top, vuln_objs, "packages", 8, false);
			char *output = ucl_object_emit(top, raw);
			fprintf(stdout, "%s\n", output);
			free(output);
			ucl_object_unref(top);
		}
	} else {
		warnx("cannot process vulnxml");
		ret = EXIT_FAILURE;
		pkghash_destroy(check);
	}

	pkg_audit_free(audit);
	pkgdb_close(db);
	if (vuln != 0)
		ret = EXIT_FAILURE;

	return (ret);
}
