Radish alpha
H
rad:z3QDZAW2FAfuLvihrhiyDC9fAD8G9
HardenedBSD Package Manager
Radicle
Git
vuln: now install and upgrade shows if they are about to install vulnerable packages
Baptiste Daroussin committed 19 days ago
commit 527d667353d5e48d29c23e792b7844aed1bf47bc
parent f7ea49d
3 files changed +342 -1
modified src/utils.c
@@ -14,6 +14,11 @@

#include <sys/param.h>
#include <sys/stat.h>
+
#include <sys/wait.h>
+

+
#ifdef HAVE_CAPSICUM
+
#include <sys/capsicum.h>
+
#endif

#include <err.h>
#include <fcntl.h>
@@ -31,7 +36,9 @@
#include <errno.h>
#include <pwd.h>
#include <pkg.h>
+
#include <pkghash.h>
#include <xmalloc.h>
+
#include <pkg/audit.h>

#include <bsd_compat.h>

@@ -765,6 +772,7 @@ struct pkg_solved_display {
	struct pkg *new, *old;
	enum pkg_display_type display_type;
	pkg_solved_t solved_type;
+
	bool vulnerable;
};

typedef vec_t(struct pkg_solved_display *) pkg_solved_display_t;
@@ -794,6 +802,7 @@ set_jobs_summary_pkg(struct pkg_jobs *jobs, struct pkg *new_pkg,
	it->new = new_pkg;
	it->old = old_pkg;
	it->solved_type = type;
+
	it->vulnerable = false;
	it->display_type = PKG_DISPLAY_MAX;

	if (old_pkg != NULL && pkg_is_locked(old_pkg)) {
@@ -951,18 +960,24 @@ display_summary_item(struct pkg_solved_display *it, int64_t dlsize)
		pkg_printf("\t%n: %v", it->new, it->new);
		if (pkg_repos_total_count() > 1)
			pkg_printf(" [%N]", it->new);
+
		if (it->vulnerable)
+
			printf(" (vulnerable!)");
		putchar('\n');
		break;
	case PKG_DISPLAY_UPGRADE:
		pkg_printf("\t%n: %v -> %v", it->new, it->old, it->new);
		if (pkg_repos_total_count() > 1)
			pkg_printf(" [%N]", it->new);
+
		if (it->vulnerable)
+
			printf(" (vulnerable!)");
		putchar('\n');
		break;
	case PKG_DISPLAY_DOWNGRADE:
		pkg_printf("\t%n: %v -> %v", it->new, it->old, it->new);
		if (pkg_repos_total_count() > 1)
			pkg_printf(" [%N]", it->new);
+
		if (it->vulnerable)
+
			printf(" (vulnerable!)");
		putchar('\n');
		break;
	case PKG_DISPLAY_REINSTALL:
@@ -972,6 +987,8 @@ display_summary_item(struct pkg_solved_display *it, int64_t dlsize)
			pkg_printf(" [%N]", it->new);
		if (why != NULL)
			printf(" (%s)", why);
+
		if (it->vulnerable)
+
			printf(" (vulnerable!)");
		putchar('\n');
		break;
	case PKG_DISPLAY_FETCH:
@@ -1024,6 +1041,112 @@ namecmp(const void *a, const void *b)
	return (pkg_namecmp(sda->new, sdb->new));
}

+
static pkghash *
+
audit_check_summary(pkg_solved_display_t *disp)
+
{
+
	int pfd[2];
+
	pid_t cld;
+
	struct pkg_audit *audit;
+
	struct pkg_audit_issues *issues;
+
	pkghash *vulnerable = NULL;
+
	FILE *in;
+
	char *line = NULL;
+
	size_t linecap = 0;
+
	ssize_t linelen;
+

+
	if (pipe(pfd) == -1)
+
		return (NULL);
+

+
	cld = fork();
+
	switch (cld) {
+
	case 0:
+
		close(pfd[0]);
+
		FILE *out = fdopen(pfd[1], "w");
+
		if (out == NULL)
+
			_exit(EXIT_FAILURE);
+

+
		audit = pkg_audit_new();
+
		if (pkg_audit_load(audit, NULL) != EPKG_OK) {
+
			pkg_audit_free(audit);
+
			fclose(out);
+
			_exit(EXIT_SUCCESS);
+
		}
+

+
		pkg_drop_privileges();
+

+
#ifdef HAVE_CAPSICUM
+
#ifndef COVERAGE
+
		if (cap_enter() < 0 && errno != ENOSYS) {
+
			warn("cap_enter() failed");
+
			pkg_audit_free(audit);
+
			fclose(out);
+
			_exit(EXIT_FAILURE);
+
		}
+
#endif
+
#endif
+

+
		if (pkg_audit_process(audit) == EPKG_OK) {
+
			for (int type = 0; type < PKG_DISPLAY_MAX; type++) {
+
				vec_foreach(disp[type], i) {
+
					const char *name = NULL, *version = NULL;
+
					issues = NULL;
+
					if (pkg_audit_is_vulnerable(audit,
+
					    disp[type].d[i]->new, &issues,
+
					    true)) {
+
						pkg_get(disp[type].d[i]->new,
+
						    PKG_ATTR_NAME, &name);
+
						pkg_get(disp[type].d[i]->new,
+
						    PKG_ATTR_VERSION, &version);
+
						if (name != NULL &&
+
						    version != NULL)
+
							fprintf(out, "%s-%s\n",
+
							    name, version);
+
					}
+
					pkg_audit_issues_free(issues);
+
				}
+
			}
+
		}
+

+
		pkg_audit_free(audit);
+
		fclose(out);
+
		_exit(EXIT_SUCCESS);
+
	case -1:
+
		close(pfd[0]);
+
		close(pfd[1]);
+
		return (NULL);
+
	default:
+
		/* Parent */
+
		close(pfd[1]);
+
		in = fdopen(pfd[0], "r");
+
		if (in == NULL) {
+
			close(pfd[0]);
+
			waitpid(cld, NULL, 0);
+
			return (NULL);
+
		}
+

+
		vulnerable = pkghash_new();
+
		while ((linelen = getline(&line, &linecap, in)) > 0) {
+
			if (line[linelen - 1] == '\n')
+
				line[linelen - 1] = '\0';
+
			pkghash_safe_add(vulnerable, line, NULL, NULL);
+
		}
+
		free(line);
+
		fclose(in);
+

+
		while (waitpid(cld, NULL, 0) == -1) {
+
			if (errno != EINTR)
+
				break;
+
		}
+

+
		if (pkghash_count(vulnerable) == 0) {
+
			pkghash_destroy(vulnerable);
+
			return (NULL);
+
		}
+

+
		return (vulnerable);
+
	}
+
}
+

int
print_jobs_summary(struct pkg_jobs *jobs, const char *msg, ...)
{
@@ -1049,6 +1172,28 @@ print_jobs_summary(struct pkg_jobs *jobs, const char *msg, ...)
		&newsize, &dlsize, disp, &sum);
	}

+
	/* Check packages against audit database for vulnerabilities */
+
	pkghash *vulnerable = audit_check_summary(disp);
+
	int vuln_count = 0;
+
	if (vulnerable != NULL) {
+
		for (type = 0; type < PKG_DISPLAY_MAX; type++) {
+
			vec_foreach(disp[type], i) {
+
				const char *n = NULL, *v = NULL;
+
				char nv[BUFSIZ];
+
				pkg_get(disp[type].d[i]->new, PKG_ATTR_NAME, &n);
+
				pkg_get(disp[type].d[i]->new, PKG_ATTR_VERSION, &v);
+
				if (n != NULL && v != NULL) {
+
					snprintf(nv, sizeof(nv), "%s-%s", n, v);
+
					if (pkghash_get(vulnerable, nv) != NULL) {
+
						disp[type].d[i]->vulnerable = true;
+
						vuln_count++;
+
					}
+
				}
+
			}
+
		}
+
		pkghash_destroy(vulnerable);
+
	}
+

	for (type = 0; type < PKG_DISPLAY_MAX; type ++) {
		if (disp[type].len != 0) {
			/* Space between each section. */
@@ -1117,6 +1262,10 @@ print_jobs_summary(struct pkg_jobs *jobs, const char *msg, ...)
		printf("%s to be downloaded.\n", size);
	}

+
	if (vuln_count > 0)
+
		printf("\nWARNING: %d package(s) have known vulnerabilities\n",
+
		    vuln_count);
+

	return (displayed);
}

modified tests/frontend/install.sh
@@ -17,7 +17,10 @@ tests_init \
	install_no_suggest_when_flag_matches \
	install_from_url \
	install_automatic_flag_not_on_upgrade \
-
	install_disabled_repo
+
	install_disabled_repo \
+
	install_vulnerable \
+
	install_vulnerable_not_in_range \
+
	install_vulnerable_no_vuln_db

test_setup()
{
@@ -613,3 +616,137 @@ EOF

	atf_check -s exit:0 pkg info -e test
}
+

+
install_vulnerable_body()
+
{
+
	# Create a package and repo
+
	atf_check sh ${RESOURCEDIR}/test_subr.sh new_pkg "vuln" "vuln" "1.5"
+
	atf_check pkg create -M vuln.ucl
+
	atf_check -o ignore pkg repo .
+

+
	mkdir repoconf
+
	cat <<EOF > repoconf/repo.conf
+
local: {
+
	url: "file://${TMPDIR}",
+
	enabled: true
+
}
+
EOF
+

+
	# Create vuln.xml in PKG_DBDIR (current dir)
+
	cat > vuln.xml << 'VULN'
+
<?xml version="1.0" encoding="utf-8"?>
+
<vuxml xmlns="http://www.vuxml.org/apps/vuxml-1">
+
  <vuln vid="test-vuln-001">
+
    <topic>Test vulnerability</topic>
+
    <affects>
+
      <package>
+
        <name>vuln</name>
+
        <range>
+
          <ge>1.0</ge>
+
          <lt>2.0</lt>
+
        </range>
+
      </package>
+
    </affects>
+
    <references>
+
      <cvename>CVE-2024-00001</cvename>
+
    </references>
+
  </vuln>
+
</vuxml>
+
VULN
+

+
	atf_check \
+
		-o ignore \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		update
+

+
	# Install with dry-run: should show vulnerability markers
+
	atf_check \
+
		-o match:"\(vulnerable!\)" \
+
		-o match:"WARNING: 1 package.* have known vulnerabilities" \
+
		-s exit:1 \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		install -n vuln
+
}
+

+
install_vulnerable_not_in_range_body()
+
{
+
	# Create a package outside the vulnerable range
+
	atf_check sh ${RESOURCEDIR}/test_subr.sh new_pkg "safe" "safe" "3.0"
+
	atf_check pkg create -M safe.ucl
+
	atf_check -o ignore pkg repo .
+

+
	mkdir repoconf
+
	cat <<EOF > repoconf/repo.conf
+
local: {
+
	url: "file://${TMPDIR}",
+
	enabled: true
+
}
+
EOF
+

+
	# Vulnerability only affects versions < 2.0
+
	cat > vuln.xml << 'VULN'
+
<?xml version="1.0" encoding="utf-8"?>
+
<vuxml xmlns="http://www.vuxml.org/apps/vuxml-1">
+
  <vuln vid="test-vuln-001">
+
    <topic>Test vulnerability</topic>
+
    <affects>
+
      <package>
+
        <name>safe</name>
+
        <range>
+
          <ge>1.0</ge>
+
          <lt>2.0</lt>
+
        </range>
+
      </package>
+
    </affects>
+
    <references>
+
      <cvename>CVE-2024-00001</cvename>
+
    </references>
+
  </vuln>
+
</vuxml>
+
VULN
+

+
	atf_check \
+
		-o ignore \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		update
+

+
	# safe-3.0 is NOT in range -> no vulnerability marker
+
	atf_check \
+
		-o not-match:"\(vulnerable!\)" \
+
		-o not-match:"WARNING.*vulnerabilities" \
+
		-s exit:1 \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		install -n safe
+
}
+

+
install_vulnerable_no_vuln_db_body()
+
{
+
	# Create a package and repo, but no vuln.xml
+
	atf_check sh ${RESOURCEDIR}/test_subr.sh new_pkg "test" "test" "1.0"
+
	atf_check pkg create -M test.ucl
+
	atf_check -o ignore pkg repo .
+

+
	mkdir repoconf
+
	cat <<EOF > repoconf/repo.conf
+
local: {
+
	url: "file://${TMPDIR}",
+
	enabled: true
+
}
+
EOF
+

+
	atf_check \
+
		-o ignore \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		update
+

+
	# No vuln.xml -> no vulnerability output, install proceeds normally
+
	atf_check \
+
		-o not-match:"\(vulnerable!\)" \
+
		-o not-match:"WARNING.*vulnerabilities" \
+
		-s exit:1 \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		install -n test
+
}
modified tests/frontend/upgrade.sh
@@ -23,6 +23,7 @@ tests_init \
	newpkgversion_two_repos \
	upgrade_disabled_repo \
	upgrade_all_disabled_repos \
+
	upgrade_vulnerable

issue1881_body() {
	atf_check -s exit:0 sh ${RESOURCEDIR}/test_subr.sh new_pkg pkg1 pkg_a 1
@@ -883,3 +884,57 @@ EOF
		-o inline:"2\n" \
		pkg query "%v" test
}
+

+
upgrade_vulnerable_body()
+
{
+
	# Install v1 locally
+
	atf_check sh ${RESOURCEDIR}/test_subr.sh new_pkg "test" "test" "1"
+
	atf_check -o ignore pkg register -M test.ucl
+

+
	# Create v1.5 in repo
+
	atf_check sh ${RESOURCEDIR}/test_subr.sh new_pkg "test" "test" "1.5"
+
	atf_check pkg create -M test.ucl
+
	atf_check -o ignore pkg repo .
+

+
	mkdir repoconf
+
	cat <<EOF > repoconf/repo.conf
+
local: {
+
	url: "file://${TMPDIR}",
+
	enabled: true
+
}
+
EOF
+

+
	# vuln.xml: test >=1.0 <2.0 is vulnerable
+
	cat > vuln.xml << 'VULN'
+
<?xml version="1.0" encoding="utf-8"?>
+
<vuxml xmlns="http://www.vuxml.org/apps/vuxml-1">
+
  <vuln vid="test-vuln-001">
+
    <topic>Test vulnerability</topic>
+
    <affects>
+
      <package>
+
        <name>test</name>
+
        <range>
+
          <ge>1.0</ge>
+
          <lt>2.0</lt>
+
        </range>
+
      </package>
+
    </affects>
+
    <references>
+
      <cvename>CVE-2024-00001</cvename>
+
    </references>
+
  </vuln>
+
</vuxml>
+
VULN
+

+
	atf_check -o ignore \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		update
+

+
	# Upgrade dry-run: should show vulnerability marker on the new version
+
	atf_check \
+
		-o match:"\(vulnerable!\)" \
+
		-o match:"WARNING: 1 package.* have known vulnerabilities" \
+
		-s exit:0 \
+
		pkg -o REPOS_DIR="${TMPDIR}/repoconf" -o PKG_CACHEDIR="${TMPDIR}" \
+
		upgrade -n
+
}