From f5bd75fb574b9be80879fda5a889ddfd2b706248 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Nov 2018 09:32:17 +0100 Subject: [PATCH] analyze: add new security verb (cherry picked from commit ec16f3b6dd8b03e3ce6eff1fa9f21432208ef42b) Conflicts: src/analyze/analyze.c Resolves: #1689832 --- src/analyze/analyze-security.c | 2078 ++++++++++++++++++++++++++++++++ src/analyze/analyze-security.h | 12 + src/analyze/analyze.c | 16 + src/analyze/meson.build | 2 + src/basic/format-table.c | 1 - src/basic/macro.h | 13 +- src/basic/terminal-util.c | 53 +- src/basic/terminal-util.h | 1 + 8 files changed, 2150 insertions(+), 26 deletions(-) create mode 100644 src/analyze/analyze-security.c create mode 100644 src/analyze/analyze-security.h diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c new file mode 100644 index 0000000000..541fc0d97a --- /dev/null +++ b/src/analyze/analyze-security.c @@ -0,0 +1,2078 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "analyze-security.h" +#include "bus-error.h" +#include "bus-unit-util.h" +#include "bus-util.h" +#include "env-util.h" +#include "format-table.h" +#include "in-addr-util.h" +#include "locale-util.h" +#include "macro.h" +#include "parse-util.h" +#include "path-util.h" +#include "seccomp-util.h" +#include "set.h" +#include "stdio-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "unit-def.h" +#include "unit-name.h" + +struct security_info { + char *id; + char *type; + char *load_state; + char *fragment_path; + bool default_dependencies; + + uint64_t ambient_capabilities; + uint64_t capability_bounding_set; + + char *user; + char **supplementary_groups; + bool dynamic_user; + + bool ip_address_deny_all; + bool ip_address_allow_localhost; + bool ip_address_allow_other; + + char *keyring_mode; + bool lock_personality; + bool memory_deny_write_execute; + bool no_new_privileges; + char *notify_access; + + bool private_devices; + bool private_mounts; + bool private_network; + bool private_tmp; + bool private_users; + + bool protect_control_groups; + bool protect_kernel_modules; + bool protect_kernel_tunables; + + char *protect_home; + char *protect_system; + + bool remove_ipc; + + bool restrict_address_family_inet; + bool restrict_address_family_unix; + bool restrict_address_family_netlink; + bool restrict_address_family_packet; + bool restrict_address_family_other; + + uint64_t restrict_namespaces; + bool restrict_realtime; + + char *root_directory; + char *root_image; + + bool delegate; + char *device_policy; + bool device_allow_non_empty; + + char **system_call_architectures; + + bool system_call_filter_whitelist; + Set *system_call_filter; + + uint32_t _umask; +}; + +struct security_assessor { + const char *id; + const char *description_good; + const char *description_bad; + const char *description_na; + const char *url; + uint64_t weight; + uint64_t range; + int (*assess)(const struct security_assessor *a, const struct security_info *info, const void *data, uint64_t *ret_badness, char **ret_description); + size_t offset; + uint64_t parameter; + bool default_dependencies_only; +}; + +static void security_info_free(struct security_info *i) { + if (!i) + return; + + free(i->id); + free(i->type); + free(i->load_state); + free(i->fragment_path); + + free(i->user); + + free(i->protect_home); + free(i->protect_system); + + free(i->root_directory); + free(i->root_image); + + free(i->keyring_mode); + free(i->notify_access); + + free(i->device_policy); + + strv_free(i->supplementary_groups); + strv_free(i->system_call_architectures); + + set_free_free(i->system_call_filter); +} + +static bool security_info_runs_privileged(const struct security_info *i) { + assert(i); + + if (STRPTR_IN_SET(i->user, "0", "root")) + return true; + + if (i->dynamic_user) + return false; + + return isempty(i->user); +} + +static int assess_bool( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + const bool *b = data; + + assert(b); + assert(ret_badness); + assert(ret_description); + + *ret_badness = a->parameter ? *b : !*b; + *ret_description = NULL; + + return 0; +} + +static int assess_user( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + _cleanup_free_ char *d = NULL; + uint64_t b; + + assert(ret_badness); + assert(ret_description); + + if (streq_ptr(info->user, NOBODY_USER_NAME)) { + d = strdup("Service runs under as '" NOBODY_USER_NAME "' user, which should not be used for services"); + b = 9; + } else if (info->dynamic_user && !STR_IN_SET(info->user, "0", "root")) { + d = strdup("Service runs under a transient non-root user identity"); + b = 0; + } else if (info->user && !STR_IN_SET(info->user, "0", "root", "")) { + d = strdup("Service runs under a static non-root user identity"); + b = 0; + } else { + *ret_badness = 10; + *ret_description = NULL; + return 0; + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = TAKE_PTR(d); + + return 0; +} + +static int assess_protect_home( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + const char *description; + uint64_t badness; + char *copy; + int r; + + assert(ret_badness); + assert(ret_description); + + badness = 10; + description = "Service has full access to home directories"; + + r = parse_boolean(info->protect_home); + if (r < 0) { + if (streq_ptr(info->protect_home, "read-only")) { + badness = 5; + description = "Service has read-only access to home directories"; + } else if (streq_ptr(info->protect_home, "tmpfs")) { + badness = 1; + description = "Service has access to fake empty home directories"; + } + } else if (r > 0) { + badness = 0; + description = "Service has no access to home directories"; + } + + copy = strdup(description); + if (!copy) + return log_oom(); + + *ret_badness = badness; + *ret_description = copy; + + return 0; +} + +static int assess_protect_system( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + const char *description; + uint64_t badness; + char *copy; + int r; + + assert(ret_badness); + assert(ret_description); + + badness = 10; + description = "Service has full access the OS file hierarchy"; + + r = parse_boolean(info->protect_system); + if (r < 0) { + if (streq_ptr(info->protect_system, "full")) { + badness = 3; + description = "Service has very limited write access to OS file hierarchy"; + } else if (streq_ptr(info->protect_system, "strict")) { + badness = 0; + description = "Service has strict read-only access to the OS file hierarchy"; + } + } else if (r > 0) { + badness = 5; + description = "Service has limited write access to the OS file hierarchy"; + } + + copy = strdup(description); + if (!copy) + return log_oom(); + + *ret_badness = badness; + *ret_description = copy; + + return 0; +} + +static int assess_root_directory( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = + (isempty(info->root_directory) || + path_equal(info->root_directory, "/")) && + (isempty(info->root_image) || + path_equal(info->root_image, "/")); + *ret_description = NULL; + + return 0; +} + +static int assess_capability_bounding_set( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = !!(info->capability_bounding_set & a->parameter); + *ret_description = NULL; + + return 0; +} + +static int assess_umask( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *copy = NULL; + const char *d; + uint64_t b; + + assert(ret_badness); + assert(ret_description); + + if (!FLAGS_SET(info->_umask, 0002)) { + d = "Files created by service are world-writable by default"; + b = 10; + } else if (!FLAGS_SET(info->_umask, 0004)) { + d = "Files created by service are world-readable by default"; + b = 5; + } else if (!FLAGS_SET(info->_umask, 0020)) { + d = "Files created by service are group-writable by default"; + b = 2; + } else if (!FLAGS_SET(info->_umask, 0040)) { + d = "Files created by service are group-readable by default"; + b = 1; + } else { + d = "Files created by service are accessible only by service's own user by default"; + b = 0; + } + + copy = strdup(d); + if (!copy) + return log_oom(); + + *ret_badness = b; + *ret_description = copy; + + return 0; +} + +static int assess_keyring_mode( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = !streq_ptr(info->keyring_mode, "private"); + *ret_description = NULL; + + return 0; +} + +static int assess_notify_access( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = streq_ptr(info->notify_access, "all"); + *ret_description = NULL; + + return 0; +} + +static int assess_remove_ipc( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + if (security_info_runs_privileged(info)) + *ret_badness = UINT64_MAX; + else + *ret_badness = !info->remove_ipc; + + *ret_description = NULL; + return 0; +} + +static int assess_supplementary_groups( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + if (security_info_runs_privileged(info)) + *ret_badness = UINT64_MAX; + else + *ret_badness = !strv_isempty(info->supplementary_groups); + + *ret_description = NULL; + return 0; +} + +static int assess_restrict_namespaces( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = !!(info->restrict_namespaces & a->parameter); + *ret_description = NULL; + + return 0; +} + +static int assess_system_call_architectures( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *d; + uint64_t b; + + assert(ret_badness); + assert(ret_description); + + if (strv_isempty(info->system_call_architectures)) { + b = 10; + d = strdup("Service may execute system calls with all ABIs"); + } else if (strv_equal(info->system_call_architectures, STRV_MAKE("native"))) { + b = 0; + d = strdup("Service may execute system calls only with native ABI"); + } else { + b = 8; + d = strdup("Service may execute system calls with multiple ABIs"); + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = d; + + return 0; +} + +static bool syscall_names_in_filter(Set *s, bool whitelist, const SyscallFilterSet *f) { + const char *syscall; + + NULSTR_FOREACH(syscall, f->value) { + bool b; + + if (syscall[0] == '@') { + const SyscallFilterSet *g; + assert_se(g = syscall_filter_set_find(syscall)); + b = syscall_names_in_filter(s, whitelist, g); + } else { +#if HAVE_SECCOMP + int id; + + /* Let's see if the system call actually exists on this platform, before complaining */ + id = seccomp_syscall_resolve_name(syscall); + if (id < 0) + continue; +#endif + + b = set_contains(s, syscall); + } + + if (whitelist == b) { + log_debug("Offending syscall filter item: %s", syscall); + return true; /* bad! */ + } + } + + return false; +} + +static int assess_system_call_filter( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + const SyscallFilterSet *f; + char *d = NULL; + uint64_t b; + + assert(a); + assert(info); + assert(ret_badness); + assert(ret_description); + + assert(a->parameter < _SYSCALL_FILTER_SET_MAX); + f = syscall_filter_sets + a->parameter; + + if (!info->system_call_filter_whitelist && set_isempty(info->system_call_filter)) { + d = strdup("Service does not filter system calls"); + b = 10; + } else { + bool bad; + + log_debug("Analyzing system call filter, checking against: %s", f->name); + bad = syscall_names_in_filter(info->system_call_filter, info->system_call_filter_whitelist, f); + log_debug("Result: %s", bad ? "bad" : "good"); + + if (info->system_call_filter_whitelist) { + if (bad) { + (void) asprintf(&d, "System call whitelist defined for service, and %s is included", f->name); + b = 9; + } else { + (void) asprintf(&d, "System call whitelist defined for service, and %s is not included", f->name); + b = 0; + } + } else { + if (bad) { + (void) asprintf(&d, "System call blacklist defined for service, and %s is not included", f->name); + b = 10; + } else { + (void) asprintf(&d, "System call blacklist defined for service, and %s is included", f->name); + b = 5; + } + } + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = d; + + return 0; +} + +static int assess_ip_address_allow( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *d = NULL; + uint64_t b; + + assert(info); + assert(ret_badness); + assert(ret_description); + + if (!info->ip_address_deny_all) { + d = strdup("Service does not define an IP address whitelist"); + b = 10; + } else if (info->ip_address_allow_other) { + d = strdup("Service defines IP address whitelist with non-localhost entries"); + b = 5; + } else if (info->ip_address_allow_localhost) { + d = strdup("Service defines IP address whitelits with only localhost entries"); + b = 2; + } else { + d = strdup("Service blocks all IP address ranges"); + b = 0; + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = d; + + return 0; +} + +static int assess_device_allow( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + char *d = NULL; + uint64_t b; + + assert(info); + assert(ret_badness); + assert(ret_description); + + if (STRPTR_IN_SET(info->device_policy, "strict", "closed")) { + + if (info->device_allow_non_empty) { + d = strdup("Service has a device ACL with some special devices"); + b = 5; + } else { + d = strdup("Service has a minimal device ACL"); + b = 0; + } + } else { + d = strdup("Service has no device ACL"); + b = 10; + } + + if (!d) + return log_oom(); + + *ret_badness = b; + *ret_description = d; + + return 0; +} + +static int assess_ambient_capabilities( + const struct security_assessor *a, + const struct security_info *info, + const void *data, + uint64_t *ret_badness, + char **ret_description) { + + assert(ret_badness); + assert(ret_description); + + *ret_badness = info->ambient_capabilities != 0; + *ret_description = NULL; + + return 0; +} + +static const struct security_assessor security_assessor_table[] = { + { + .id = "User=/DynamicUser=", + .description_bad = "Service runs as root user", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#User=", + .weight = 2000, + .range = 10, + .assess = assess_user, + }, + { + .id = "SupplementaryGroups=", + .description_good = "Service has no supplementary groups", + .description_bad = "Service runs with supplementary groups", + .description_na = "Service runs as root, option does not matter", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SupplementaryGroups=", + .weight = 200, + .range = 1, + .assess = assess_supplementary_groups, + }, + { + .id = "PrivateDevices=", + .description_good = "Service has no access to hardware devices", + .description_bad = "Service potentially has access to hardware devices", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, private_devices), + }, + { + .id = "PrivateMounts=", + .description_good = "Service cannot install system mounts", + .description_bad = "Service may install system mounts", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateMounts=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, private_mounts), + }, + { + .id = "PrivateNetwork=", + .description_good = "Service has no access to the host's network", + .description_bad = "Service has access to the host's network", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateNetwork=", + .weight = 2500, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, private_network), + }, + { + .id = "PrivateTmp=", + .description_good = "Service has no access to other software's temporary files", + .description_bad = "Service has access to other software's temporary files", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, private_tmp), + .default_dependencies_only = true, + }, + { + .id = "PrivateUsers=", + .description_good = "Service does not have access to other users", + .description_bad = "Service has access to other users", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateUsers=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, private_users), + }, + { + .id = "ProtectControlGroups=", + .description_good = "Service cannot modify the control group file system", + .description_bad = "Service may modify to the control group file system", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, protect_control_groups), + }, + { + .id = "ProtectKernelModules=", + .description_good = "Service cannot load or read kernel modules", + .description_bad = "Service may load or read kernel modules", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, protect_kernel_modules), + }, + { + .id = "ProtectKernelTunables=", + .description_good = "Service cannot alter kernel tunables (/proc/sys, …)", + .description_bad = "Service may alter kernel tunables", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, protect_kernel_tunables), + }, + { + .id = "ProtectHome=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=", + .weight = 1000, + .range = 10, + .assess = assess_protect_home, + .default_dependencies_only = true, + }, + { + .id = "ProtectSystem=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectSystem=", + .weight = 1000, + .range = 10, + .assess = assess_protect_system, + .default_dependencies_only = true, + }, + { + .id = "RootDirectory=/RootImage=", + .description_good = "Service has its own root directory/image", + .description_bad = "Service runs within the host's root directory", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RootDirectory=", + .weight = 200, + .range = 1, + .assess = assess_root_directory, + .default_dependencies_only = true, + }, + { + .id = "LockPersonality=", + .description_good = "Service cannot change ABI personality", + .description_bad = "Service may change ABI personality", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality=", + .weight = 100, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, lock_personality), + }, + { + .id = "MemoryDenyWriteExecute=", + .description_good = "Service cannot create writable executable memory mappings", + .description_bad = "Service may create writable executable memory mappings", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#MemoryDenyWriteExecute=", + .weight = 100, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, memory_deny_write_execute), + }, + { + .id = "NoNewPrivileges=", + .description_good = "Service processes cannot acquire new privileges", + .description_bad = "Service processes may acquire new privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#NoNewPrivileges=", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, no_new_privileges), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_ADMIN", + .description_good = "Service has no administrator privileges", + .description_bad = "Service has administrator privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = UINT64_C(1) << CAP_SYS_ADMIN, + }, + { + .id = "CapabilityBoundingSet=~CAP_SET(UID|GID|PCAP)", + .description_good = "Service cannot change UID/GID identities/capabilities", + .description_bad = "Service may change UID/GID identities/capabilities", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SETUID)| + (UINT64_C(1) << CAP_SETGID)| + (UINT64_C(1) << CAP_SETPCAP), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_PTRACE", + .description_good = "Service has no ptrace() debugging abilities", + .description_bad = "Service has ptrace() debugging abilities", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_PTRACE), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_TIME", + .description_good = "Service processes cannot change the system clock", + .description_bad = "Service processes may change the system clock", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = UINT64_C(1) << CAP_SYS_TIME, + }, + { + .id = "CapabilityBoundingSet=~CAP_NET_ADMIN", + .description_good = "Service has no network configuration privileges", + .description_bad = "Service has network configuration privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_NET_ADMIN), + }, + { + .id = "CapabilityBoundingSet=~CAP_RAWIO", + .description_good = "Service has no raw I/O access", + .description_bad = "Service has raw I/O access", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_RAWIO), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_MODULE", + .description_good = "Service cannot load kernel modules", + .description_bad = "Service may load kernel modules", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_MODULE), + }, + { + .id = "CapabilityBoundingSet=~CAP_AUDIT_*", + .description_good = "Service has no audit subsystem access", + .description_bad = "Service has audit subsystem access", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_AUDIT_CONTROL) | + (UINT64_C(1) << CAP_AUDIT_READ) | + (UINT64_C(1) << CAP_AUDIT_WRITE), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYSLOG", + .description_good = "Service has no access to kernel logging", + .description_bad = "Service has access to kernel logging", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYSLOG), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_(NICE|RESOURCE)", + .description_good = "Service has no privileges to change resource use parameters", + .description_bad = "Service has privileges to change resource use parameters", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_NICE) | + (UINT64_C(1) << CAP_SYS_RESOURCE), + }, + { + .id = "CapabilityBoundingSet=~CAP_MKNOD", + .description_good = "Service cannot create device nodes", + .description_bad = "Service may create device nodes", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_MKNOD), + }, + { + .id = "CapabilityBoundingSet=~CAP_(CHOWN|FSETID|SETFCAP)", + .description_good = "Service cannot change file ownership/access mode/capabilities", + .description_bad = "Service may change file ownership/access mode/capabilities unrestricted", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_CHOWN) | + (UINT64_C(1) << CAP_FSETID) | + (UINT64_C(1) << CAP_SETFCAP), + }, + { + .id = "CapabilityBoundingSet=~CAP_(DAC_*|FOWNER|IPC_OWNER)", + .description_good = "Service cannot override UNIX file/IPC permission checks", + .description_bad = "Service may override UNIX file/IPC permission checks", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 1000, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_DAC_OVERRIDE) | + (UINT64_C(1) << CAP_DAC_READ_SEARCH) | + (UINT64_C(1) << CAP_FOWNER) | + (UINT64_C(1) << CAP_IPC_OWNER), + }, + { + .id = "CapabilityBoundingSet=~CAP_KILL", + .description_good = "Service cannot send UNIX signals to arbitrary processes", + .description_bad = "Service may send UNIX signals to arbitrary processes", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_KILL), + }, + { + .id = "CapabilityBoundingSet=~CAP_NET_(BIND_SERVICE|BROADCAST|RAW)", + .description_good = "Service has no elevated networking privileges", + .description_bad = "Service has elevated networking privileges", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 500, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_NET_BIND_SERVICE) | + (UINT64_C(1) << CAP_NET_BROADCAST) | + (UINT64_C(1) << CAP_NET_RAW), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_BOOT", + .description_good = "Service cannot issue reboot()", + .description_bad = "Service may issue reboot()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 100, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_BOOT), + }, + { + .id = "CapabilityBoundingSet=~CAP_MAC_*", + .description_good = "Service cannot adjust SMACK MAC", + .description_bad = "Service may adjust SMACK MAC", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 100, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_MAC_ADMIN)| + (UINT64_C(1) << CAP_MAC_OVERRIDE), + }, + { + .id = "CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE", + .description_good = "Service cannot mark files immutable", + .description_bad = "Service may mark files immutable", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 75, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_LINUX_IMMUTABLE), + }, + { + .id = "CapabilityBoundingSet=~CAP_IPC_LOCK", + .description_good = "Service cannot lock memory into RAM", + .description_bad = "Service may lock memory into RAM", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 50, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_IPC_LOCK), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_CHROOT", + .description_good = "Service cannot issue chroot()", + .description_bad = "Service may issue chroot()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 50, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_CHROOT), + }, + { + .id = "CapabilityBoundingSet=~CAP_BLOCK_SUSPEND", + .description_good = "Service cannot establish wake locks", + .description_bad = "Service may establish wake locks", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_BLOCK_SUSPEND), + }, + { + .id = "CapabilityBoundingSet=~CAP_WAKE_ALARM", + .description_good = "Service cannot program timers that wake up the system", + .description_bad = "Service may program timers that wake up the system", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_WAKE_ALARM), + }, + { + .id = "CapabilityBoundingSet=~CAP_LEASE", + .description_good = "Service cannot create file leases", + .description_bad = "Service may create file leases", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_LEASE), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_TTY_CONFIG", + .description_good = "Service cannot issue vhangup()", + .description_bad = "Service may issue vhangup()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_TTY_CONFIG), + }, + { + .id = "CapabilityBoundingSet=~CAP_SYS_PACCT", + .description_good = "Service cannot use acct()", + .description_bad = "Service may use acct()", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=", + .weight = 25, + .range = 1, + .assess = assess_capability_bounding_set, + .parameter = (UINT64_C(1) << CAP_SYS_PACCT), + }, + { + .id = "UMask=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#UMask=", + .weight = 100, + .range = 10, + .assess = assess_umask, + }, + { + .id = "KeyringMode=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#KeyringMode=", + .description_good = "Service doesn't share key material with other services", + .description_bad = "Service shares key material with other service", + .weight = 1000, + .range = 1, + .assess = assess_keyring_mode, + }, + { + .id = "NotifyAccess=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#NotifyAccess=", + .description_good = "Service child processes cannot alter service state", + .description_bad = "Service child processes may alter service state", + .weight = 1000, + .range = 1, + .assess = assess_notify_access, + }, + { + .id = "RemoveIPC=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RemoveIPC=", + .description_good = "Service user cannot leave SysV IPC objects around", + .description_bad = "Service user may leave SysV IPC objects around", + .description_na = "Service runs as root, option does not apply", + .weight = 100, + .range = 1, + .assess = assess_remove_ipc, + .offset = offsetof(struct security_info, remove_ipc), + }, + { + .id = "Delegate=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Delegate=", + .description_good = "Service does not maintain its own delegated control group subtree", + .description_bad = "Service maintains its own delegated control group subtree", + .weight = 100, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, delegate), + .parameter = true, /* invert! */ + }, + { + .id = "RestrictRealtime=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime=", + .description_good = "Service realtime scheduling access is restricted", + .description_bad = "Service may acquire realtime scheduling", + .weight = 500, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, restrict_realtime), + }, + { + .id = "RestrictNamespaces=~CLONE_NEWUSER", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create user namespaces", + .description_bad = "Service may create user namespaces", + .weight = 1500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWUSER, + }, + { + .id = "RestrictNamespaces=~CLONE_NEWNS", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create file system namespaces", + .description_bad = "Service may create file system namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWNS, + }, + { + .id = "RestrictNamespaces=~CLONE_NEWIPC", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create IPC namespaces", + .description_bad = "Service may create IPC namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWIPC, + }, + { + .id = "RestrictNamespaces=~CLONE_NEWPID", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create process namespaces", + .description_bad = "Service may create process namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWPID, + }, + { + .id = "RestrictNamespaces=~CLONE_NEWCGROUP", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create cgroup namespaces", + .description_bad = "Service may create cgroup namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWCGROUP, + }, + { + .id = "RestrictNamespaces=~CLONE_NEWNET", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create network namespaces", + .description_bad = "Service may create network namespaces", + .weight = 500, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWNET, + }, + { + .id = "RestrictNamespaces=~CLONE_NEWUTS", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictNamespaces=", + .description_good = "Service cannot create hostname namespaces", + .description_bad = "Service may create hostname namespaces", + .weight = 100, + .range = 1, + .assess = assess_restrict_namespaces, + .parameter = CLONE_NEWUTS, + }, + { + .id = "RestrictAddressFamilies=~AF_(INET|INET6)", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate Internet sockets", + .description_bad = "Service may allocate Internet sockets", + .weight = 1500, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, restrict_address_family_inet), + }, + { + .id = "RestrictAddressFamilies=~AF_UNIX", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate local sockets", + .description_bad = "Service may allocate local sockets", + .weight = 25, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, restrict_address_family_unix), + }, + { + .id = "RestrictAddressFamilies=~AF_NETLINK", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate netlink sockets", + .description_bad = "Service may allocate netlink sockets", + .weight = 200, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, restrict_address_family_netlink), + }, + { + .id = "RestrictAddressFamilies=~AF_PACKET", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate packet sockets", + .description_bad = "Service may allocate packet sockets", + .weight = 1000, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, restrict_address_family_packet), + }, + { + .id = "RestrictAddressFamilies=~…", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=", + .description_good = "Service cannot allocate exotic sockets", + .description_bad = "Service may allocate exotic sockets", + .weight = 1250, + .range = 1, + .assess = assess_bool, + .offset = offsetof(struct security_info, restrict_address_family_other), + }, + { + .id = "SystemCallArchitectures=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallArchitectures=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_architectures, + }, + { + .id = "SystemCallFilter=~@swap", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_SWAP, + }, + { + .id = "SystemCallFilter=~@obsolete", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 250, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_OBSOLETE, + }, + { + .id = "SystemCallFilter=~@clock", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_CLOCK, + }, + { + .id = "SystemCallFilter=~@cpu-emulation", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 250, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_CPU_EMULATION, + }, + { + .id = "SystemCallFilter=~@debug", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_DEBUG, + }, + { + .id = "SystemCallFilter=~@mount", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_MOUNT, + }, + { + .id = "SystemCallFilter=~@module", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_MODULE, + }, + { + .id = "SystemCallFilter=~@raw-io", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_RAW_IO, + }, + { + .id = "SystemCallFilter=~@reboot", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 1000, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_REBOOT, + }, + { + .id = "SystemCallFilter=~@privileged", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 700, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_PRIVILEGED, + }, + { + .id = "SystemCallFilter=~@resources", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=", + .weight = 700, + .range = 10, + .assess = assess_system_call_filter, + .parameter = SYSCALL_FILTER_SET_RESOURCES, + }, + { + .id = "IPAddressDeny=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#IPAddressDeny=", + .weight = 1000, + .range = 10, + .assess = assess_ip_address_allow, + }, + { + .id = "DeviceAllow=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#DeviceAllow=", + .weight = 1000, + .range = 10, + .assess = assess_device_allow, + }, + { + .id = "AmbientCapabilities=", + .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#AmbientCapabilities=", + .description_good = "Service process does not receive ambient capabilities", + .description_bad = "Service process receives ambient capabilities", + .weight = 500, + .range = 1, + .assess = assess_ambient_capabilities, + }, +}; + +static int assess(const struct security_info *info, Table *overview_table, AnalyzeSecurityFlags flags) { + static const struct { + uint64_t exposure; + const char *name; + const char *color; + SpecialGlyph smiley; + } badness_table[] = { + { 100, "DANGEROUS", ANSI_HIGHLIGHT_RED, DEPRESSED_SMILEY }, + { 90, "UNSAFE", ANSI_HIGHLIGHT_RED, UNHAPPY_SMILEY }, + { 75, "EXPOSED", ANSI_HIGHLIGHT_YELLOW, SLIGHTLY_UNHAPPY_SMILEY }, + { 50, "MEDIUM", NULL, NEUTRAL_SMILEY }, + { 10, "OK", ANSI_HIGHLIGHT_GREEN, SLIGHTLY_HAPPY_SMILEY }, + { 1, "SAFE", ANSI_HIGHLIGHT_GREEN, HAPPY_SMILEY }, + { 0, "PERFECT", ANSI_HIGHLIGHT_GREEN, ECSTATIC_SMILEY }, + }; + + uint64_t badness_sum = 0, weight_sum = 0, exposure; + _cleanup_(table_unrefp) Table *details_table = NULL; + size_t i; + int r; + + if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) { + details_table = table_new("", "NAME", "DESCRIPTION", "WEIGHT", "BADNESS", "RANGE", "EXPOSURE"); + if (!details_table) + return log_oom(); + + (void) table_set_sort(details_table, 3, 1, (size_t) -1); + (void) table_set_reverse(details_table, 3, true); + + if (getenv_bool("SYSTEMD_ANALYZE_DEBUG") <= 0) + (void) table_set_display(details_table, 0, 1, 2, 6, (size_t) -1); + } + + for (i = 0; i < ELEMENTSOF(security_assessor_table); i++) { + const struct security_assessor *a = security_assessor_table + i; + _cleanup_free_ char *d = NULL; + uint64_t badness; + void *data; + + data = (uint8_t*) info + a->offset; + + if (a->default_dependencies_only && !info->default_dependencies) { + badness = UINT64_MAX; + d = strdup("Service runs in special boot phase, option does not apply"); + if (!d) + return log_oom(); + } else { + r = a->assess(a, info, data, &badness, &d); + if (r < 0) + return r; + } + + assert(a->range > 0); + + if (badness != UINT64_MAX) { + assert(badness <= a->range); + + badness_sum += DIV_ROUND_UP(badness * a->weight, a->range); + weight_sum += a->weight; + } + + if (details_table) { + const char *checkmark, *description, *color = NULL; + TableCell *cell; + + if (badness == UINT64_MAX) { + checkmark = " "; + description = a->description_na; + color = NULL; + } else if (badness == a->range) { + checkmark = special_glyph(CROSS_MARK); + description = a->description_bad; + color = ansi_highlight_red(); + } else if (badness == 0) { + checkmark = special_glyph(CHECK_MARK); + description = a->description_good; + color = ansi_highlight_green(); + } else { + checkmark = special_glyph(CROSS_MARK); + description = NULL; + color = ansi_highlight_red(); + } + + if (d) + description = d; + + r = table_add_cell_full(details_table, &cell, TABLE_STRING, checkmark, 1, 1, 0, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + if (color) + (void) table_set_color(details_table, cell, color); + + r = table_add_cell(details_table, &cell, TABLE_STRING, a->id); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + if (a->url) + (void) table_set_url(details_table, cell, a->url); + + r = table_add_cell(details_table, NULL, TABLE_STRING, description); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + + r = table_add_cell(details_table, &cell, TABLE_UINT64, &a->weight); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + (void) table_set_align_percent(details_table, cell, 100); + + r = table_add_cell(details_table, &cell, TABLE_UINT64, &badness); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + (void) table_set_align_percent(details_table, cell, 100); + + r = table_add_cell(details_table, &cell, TABLE_UINT64, &a->range); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + (void) table_set_align_percent(details_table, cell, 100); + + r = table_add_cell(details_table, &cell, TABLE_EMPTY, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + (void) table_set_align_percent(details_table, cell, 100); + } + } + + if (details_table) { + size_t row; + + for (row = 1; row < table_get_rows(details_table); row++) { + char buf[DECIMAL_STR_MAX(uint64_t) + 1 + DECIMAL_STR_MAX(uint64_t) + 1]; + const uint64_t *weight, *badness, *range; + TableCell *cell; + uint64_t x; + + assert_se(weight = table_get_at(details_table, row, 3)); + assert_se(badness = table_get_at(details_table, row, 4)); + assert_se(range = table_get_at(details_table, row, 5)); + + if (*badness == UINT64_MAX || *badness == 0) + continue; + + assert_se(cell = table_get_cell(details_table, row, 6)); + + x = DIV_ROUND_UP(DIV_ROUND_UP(*badness * *weight * 100U, *range), weight_sum); + xsprintf(buf, "%" PRIu64 ".%" PRIu64, x / 10, x % 10); + + r = table_update(details_table, cell, TABLE_STRING, buf); + if (r < 0) + return log_error_errno(r, "Failed to update cell in table: %m"); + } + + r = table_print(details_table, stdout); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + exposure = DIV_ROUND_UP(badness_sum * 100U, weight_sum); + + for (i = 0; i < ELEMENTSOF(badness_table); i++) + if (exposure >= badness_table[i].exposure) + break; + + assert(i < ELEMENTSOF(badness_table)); + + if (details_table) { + _cleanup_free_ char *clickable = NULL; + const char *name; + + /* If we shall output the details table, also print the brief summary underneath */ + + if (info->fragment_path) { + r = terminal_urlify_path(info->fragment_path, info->id, &clickable); + if (r < 0) + return log_oom(); + + name = clickable; + } else + name = info->id; + + printf("\n%s %sOverall exposure level for %s%s: %s%" PRIu64 ".%" PRIu64 " %s%s %s\n", + special_glyph(ARROW), + ansi_highlight(), + name, + ansi_normal(), + colors_enabled() ? strempty(badness_table[i].color) : "", + exposure / 10, exposure % 10, + badness_table[i].name, + ansi_normal(), + special_glyph(badness_table[i].smiley)); + } + + fflush(stdout); + + if (overview_table) { + char buf[DECIMAL_STR_MAX(uint64_t) + 1 + DECIMAL_STR_MAX(uint64_t) + 1]; + TableCell *cell; + + r = table_add_cell(overview_table, &cell, TABLE_STRING, info->id); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + if (info->fragment_path) { + _cleanup_free_ char *url = NULL; + + r = file_url_from_path(info->fragment_path, &url); + if (r < 0) + return log_error_errno(r, "Failed to generate URL from path: %m"); + + (void) table_set_url(overview_table, cell, url); + } + + xsprintf(buf, "%" PRIu64 ".%" PRIu64, exposure / 10, exposure % 10); + r = table_add_cell(overview_table, &cell, TABLE_STRING, buf); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + (void) table_set_align_percent(overview_table, cell, 100); + + r = table_add_cell(overview_table, &cell, TABLE_STRING, badness_table[i].name); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + (void) table_set_color(overview_table, cell, strempty(badness_table[i].color)); + + r = table_add_cell(overview_table, NULL, TABLE_STRING, special_glyph(badness_table[i].smiley)); + if (r < 0) + return log_error_errno(r, "Failed to add cell to table: %m"); + } + + return 0; +} + +static int property_read_restrict_address_families( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + struct security_info *info = userdata; + int whitelist, r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'r', "bas"); + if (r < 0) + return r; + + r = sd_bus_message_read(m, "b", &whitelist); + if (r < 0) + return r; + + info->restrict_address_family_inet = + info->restrict_address_family_unix = + info->restrict_address_family_netlink = + info->restrict_address_family_packet = + info->restrict_address_family_other = whitelist; + + r = sd_bus_message_enter_container(m, 'a', "s"); + if (r < 0) + return r; + + for (;;) { + const char *name; + + r = sd_bus_message_read(m, "s", &name); + if (r < 0) + return r; + if (r == 0) + break; + + if (STR_IN_SET(name, "AF_INET", "AF_INET6")) + info->restrict_address_family_inet = !whitelist; + else if (streq(name, "AF_UNIX")) + info->restrict_address_family_unix = !whitelist; + else if (streq(name, "AF_NETLINK")) + info->restrict_address_family_netlink = !whitelist; + else if (streq(name, "AF_PACKET")) + info->restrict_address_family_packet = !whitelist; + else + info->restrict_address_family_other = !whitelist; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return sd_bus_message_exit_container(m); +} + +static int property_read_system_call_filter( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + struct security_info *info = userdata; + int whitelist, r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'r', "bas"); + if (r < 0) + return r; + + r = sd_bus_message_read(m, "b", &whitelist); + if (r < 0) + return r; + + info->system_call_filter_whitelist = whitelist; + + r = sd_bus_message_enter_container(m, 'a', "s"); + if (r < 0) + return r; + + for (;;) { + const char *name; + + r = sd_bus_message_read(m, "s", &name); + if (r < 0) + return r; + if (r == 0) + break; + + r = set_ensure_allocated(&info->system_call_filter, &string_hash_ops); + if (r < 0) + return r; + + r = set_put_strdup(info->system_call_filter, name); + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return sd_bus_message_exit_container(m); +} + +static int property_read_ip_address_allow( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + struct security_info *info = userdata; + bool deny_ipv4 = false, deny_ipv6 = false; + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', "(iayu)"); + if (r < 0) + return r; + + for (;;) { + const void *data; + size_t size; + int32_t family; + uint32_t prefixlen; + + r = sd_bus_message_enter_container(m, 'r', "iayu"); + if (r < 0) + return r; + if (r == 0) + break; + + r = sd_bus_message_read(m, "i", &family); + if (r < 0) + return r; + + r = sd_bus_message_read_array(m, 'y', &data, &size); + if (r < 0) + return r; + + r = sd_bus_message_read(m, "u", &prefixlen); + if (r < 0) + return r; + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + if (streq(member, "IPAddressAllow")) { + union in_addr_union u; + + if (family == AF_INET && size == 4 && prefixlen == 8) + memcpy(&u.in, data, size); + else if (family == AF_INET6 && size == 16 && prefixlen == 128) + memcpy(&u.in6, data, size); + else { + info->ip_address_allow_other = true; + continue; + } + + if (in_addr_is_localhost(family, &u)) + info->ip_address_allow_localhost = true; + else + info->ip_address_allow_other = true; + } else { + assert(streq(member, "IPAddressDeny")); + + if (family == AF_INET && size == 4 && prefixlen == 0) + deny_ipv4 = true; + else if (family == AF_INET6 && size == 16 && prefixlen == 0) + deny_ipv6 = true; + } + } + + info->ip_address_deny_all = deny_ipv4 && deny_ipv6; + + return sd_bus_message_exit_container(m); +} + +static int property_read_device_allow( + sd_bus *bus, + const char *member, + sd_bus_message *m, + sd_bus_error *error, + void *userdata) { + + struct security_info *info = userdata; + size_t n = 0; + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', "(ss)"); + if (r < 0) + return r; + + for (;;) { + const char *name, *policy; + + r = sd_bus_message_read(m, "(ss)", &name, &policy); + if (r < 0) + return r; + if (r == 0) + break; + + n++; + } + + info->device_allow_non_empty = n > 0; + + return sd_bus_message_exit_container(m); +} + +static int acquire_security_info(sd_bus *bus, const char *name, struct security_info *info, AnalyzeSecurityFlags flags) { + + static const struct bus_properties_map security_map[] = { + { "AmbientCapabilities", "t", NULL, offsetof(struct security_info, ambient_capabilities) }, + { "CapabilityBoundingSet", "t", NULL, offsetof(struct security_info, capability_bounding_set) }, + { "DefaultDependencies", "b", NULL, offsetof(struct security_info, default_dependencies) }, + { "Delegate", "b", NULL, offsetof(struct security_info, delegate) }, + { "DeviceAllow", "a(ss)", property_read_device_allow, 0 }, + { "DevicePolicy", "s", NULL, offsetof(struct security_info, device_policy) }, + { "DynamicUser", "b", NULL, offsetof(struct security_info, dynamic_user) }, + { "FragmentPath", "s", NULL, offsetof(struct security_info, fragment_path) }, + { "IPAddressAllow", "a(iayu)", property_read_ip_address_allow, 0 }, + { "IPAddressDeny", "a(iayu)", property_read_ip_address_allow, 0 }, + { "Id", "s", NULL, offsetof(struct security_info, id) }, + { "KeyringMode", "s", NULL, offsetof(struct security_info, keyring_mode) }, + { "LoadState", "s", NULL, offsetof(struct security_info, load_state) }, + { "LockPersonality", "b", NULL, offsetof(struct security_info, lock_personality) }, + { "MemoryDenyWriteExecute", "b", NULL, offsetof(struct security_info, memory_deny_write_execute) }, + { "NoNewPrivileges", "b", NULL, offsetof(struct security_info, no_new_privileges) }, + { "NotifyAccess", "s", NULL, offsetof(struct security_info, notify_access) }, + { "PrivateDevices", "b", NULL, offsetof(struct security_info, private_devices) }, + { "PrivateMounts", "b", NULL, offsetof(struct security_info, private_mounts) }, + { "PrivateNetwork", "b", NULL, offsetof(struct security_info, private_network) }, + { "PrivateTmp", "b", NULL, offsetof(struct security_info, private_tmp) }, + { "PrivateUsers", "b", NULL, offsetof(struct security_info, private_users) }, + { "PrivateUsers", "b", NULL, offsetof(struct security_info, private_users) }, + { "ProtectControlGroups", "b", NULL, offsetof(struct security_info, protect_control_groups) }, + { "ProtectHome", "s", NULL, offsetof(struct security_info, protect_home) }, + { "ProtectKernelModules", "b", NULL, offsetof(struct security_info, protect_kernel_modules) }, + { "ProtectKernelTunables", "b", NULL, offsetof(struct security_info, protect_kernel_tunables) }, + { "ProtectSystem", "s", NULL, offsetof(struct security_info, protect_system) }, + { "RemoveIPC", "b", NULL, offsetof(struct security_info, remove_ipc) }, + { "RestrictAddressFamilies", "(bas)", property_read_restrict_address_families, 0 }, + { "RestrictNamespaces", "t", NULL, offsetof(struct security_info, restrict_namespaces) }, + { "RestrictRealtime", "b", NULL, offsetof(struct security_info, restrict_realtime) }, + { "RootDirectory", "s", NULL, offsetof(struct security_info, root_directory) }, + { "RootImage", "s", NULL, offsetof(struct security_info, root_image) }, + { "SupplementaryGroups", "as", NULL, offsetof(struct security_info, supplementary_groups) }, + { "SystemCallArchitectures", "as", NULL, offsetof(struct security_info, system_call_architectures) }, + { "SystemCallFilter", "(as)", property_read_system_call_filter, 0 }, + { "Type", "s", NULL, offsetof(struct security_info, type) }, + { "UMask", "u", NULL, offsetof(struct security_info, _umask) }, + { "User", "s", NULL, offsetof(struct security_info, user) }, + {} + }; + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *path = NULL; + int r; + + /* Note: this mangles *info on failure! */ + + assert(bus); + assert(name); + assert(info); + + path = unit_dbus_path_from_name(name); + if (!path) + return log_oom(); + + r = bus_map_all_properties(bus, + "org.freedesktop.systemd1", + path, + security_map, + BUS_MAP_STRDUP|BUS_MAP_BOOLEAN_AS_BOOL, + &error, + NULL, + info); + if (r < 0) + return log_error_errno(r, "Failed to get unit properties: %s", bus_error_message(&error, r)); + + if (!streq_ptr(info->load_state, "loaded")) { + + if (FLAGS_SET(flags, ANALYZE_SECURITY_ONLY_LOADED)) + return -EMEDIUMTYPE; + + if (streq_ptr(info->load_state, "not-found")) + log_error("Unit %s not found, cannot analyze.", name); + else if (streq_ptr(info->load_state, "masked")) + log_error("Unit %s is masked, cannot analyze.", name); + else + log_error("Unit %s not loaded properly, cannot analyze.", name); + + return -EINVAL; + } + + if (FLAGS_SET(flags, ANALYZE_SECURITY_ONLY_LONG_RUNNING) && streq_ptr(info->type, "oneshot")) + return -EMEDIUMTYPE; + + if (info->private_devices || + info->private_tmp || + info->protect_control_groups || + info->protect_kernel_tunables || + info->protect_kernel_modules || + !streq_ptr(info->protect_home, "no") || + !streq_ptr(info->protect_system, "no") || + info->root_image) + info->private_mounts = true; + + if (info->protect_kernel_modules) + info->capability_bounding_set &= ~(UINT64_C(1) << CAP_SYS_MODULE); + + if (info->private_devices) + info->capability_bounding_set &= ~((UINT64_C(1) << CAP_MKNOD) | + (UINT64_C(1) << CAP_SYS_RAWIO)); + + return 0; +} + +static int analyze_security_one(sd_bus *bus, const char *name, Table* overview_table, AnalyzeSecurityFlags flags) { + _cleanup_(security_info_free) struct security_info info = { + .default_dependencies = true, + .capability_bounding_set = UINT64_MAX, + .restrict_namespaces = UINT64_MAX, + ._umask = 0002, + }; + int r; + + assert(bus); + assert(name); + + r = acquire_security_info(bus, name, &info, flags); + if (r == -EMEDIUMTYPE) /* Ignore this one because not loaded or Type is oneshot */ + return 0; + if (r < 0) + return r; + + r = assess(&info, overview_table, flags); + if (r < 0) + return r; + + return 0; +} + +int analyze_security(sd_bus *bus, char **units, AnalyzeSecurityFlags flags) { + _cleanup_(table_unrefp) Table *overview_table = NULL; + int ret = 0, r; + + assert(bus); + + if (strv_length(units) != 1) { + overview_table = table_new("UNIT", "EXPOSURE", "PREDICATE", "HAPPY"); + if (!overview_table) + return log_oom(); + } + + if (strv_isempty(units)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_strv_free_ char **list = NULL; + size_t allocated = 0, n = 0; + char **i; + + r = sd_bus_call_method( + bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "ListUnits", + &error, &reply, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list units: %s", bus_error_message(&error, r)); + + r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + UnitInfo info; + char *copy = NULL; + + r = bus_parse_unit_info(reply, &info); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + if (!endswith(info.id, ".service")) + continue; + + if (!GREEDY_REALLOC(list, allocated, n+2)) + return log_oom(); + + copy = strdup(info.id); + if (!copy) + return log_oom(); + + list[n++] = copy; + list[n] = NULL; + } + + strv_sort(list); + + flags |= ANALYZE_SECURITY_SHORT|ANALYZE_SECURITY_ONLY_LOADED|ANALYZE_SECURITY_ONLY_LONG_RUNNING; + + STRV_FOREACH(i, list) { + r = analyze_security_one(bus, *i, overview_table, flags); + if (r < 0 && ret >= 0) + ret = r; + } + + } else { + char **i; + + STRV_FOREACH(i, units) { + _cleanup_free_ char *mangled = NULL, *instance = NULL; + const char *name; + + if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT) && i != units) { + putc('\n', stdout); + fflush(stdout); + } + + r = unit_name_mangle_with_suffix(*i, 0, ".service", &mangled); + if (r < 0) + return log_error_errno(r, "Failed to mangle unit name '%s': %m", *i); + + if (!endswith(mangled, ".service")) { + log_error("Unit %s is not a service unit, refusing.", *i); + return -EINVAL; + } + + if (unit_name_is_valid(mangled, UNIT_NAME_TEMPLATE)) { + r = unit_name_replace_instance(mangled, "test-instance", &instance); + if (r < 0) + return log_oom(); + + name = instance; + } else + name = mangled; + + r = analyze_security_one(bus, name, overview_table, flags); + if (r < 0 && ret >= 0) + ret = r; + } + } + + if (overview_table) { + if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) { + putc('\n', stdout); + fflush(stdout); + } + + r = table_print(overview_table, stdout); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + return ret; +} diff --git a/src/analyze/analyze-security.h b/src/analyze/analyze-security.h new file mode 100644 index 0000000000..c00ae7c80a --- /dev/null +++ b/src/analyze/analyze-security.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +typedef enum AnalyzeSecurityFlags { + ANALYZE_SECURITY_SHORT = 1 << 0, + ANALYZE_SECURITY_ONLY_LOADED = 1 << 1, + ANALYZE_SECURITY_ONLY_LONG_RUNNING = 1 << 2, +} AnalyzeSecurityFlags; + +int analyze_security(sd_bus *bus, char **units, AnalyzeSecurityFlags flags); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index dc7d2ab0f6..c30a133fc0 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -11,6 +11,7 @@ #include "sd-bus.h" #include "alloc-util.h" +#include "analyze-security.h" #include "analyze-verify.h" #include "bus-error.h" #include "bus-unit-util.h" @@ -1659,6 +1660,19 @@ static int do_verify(int argc, char *argv[], void *userdata) { return verify_units(strv_skip(argv, 1), arg_scope, arg_man, arg_generators); } +static int do_security(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + r = acquire_bus(&bus, NULL); + if (r < 0) + return log_error_errno(r, "Failed to create bus connection: %m"); + + (void) pager_open(arg_no_pager, false); + + return analyze_security(bus, strv_skip(argv, 1), 0); +} + static int help(int argc, char *argv[], void *userdata) { (void) pager_open(arg_no_pager, false); @@ -1696,6 +1710,7 @@ static int help(int argc, char *argv[], void *userdata) { " verify FILE... Check unit files for correctness\n" " calendar SPEC... Validate repetitive calendar time events\n" " service-watchdogs [BOOL] Get/set service watchdog state\n" + " security [UNIT...] Analyze security of unit\n" , program_invocation_short_name); /* When updating this list, including descriptions, apply @@ -1884,6 +1899,7 @@ int main(int argc, char *argv[]) { { "verify", 2, VERB_ANY, 0, do_verify }, { "calendar", 2, VERB_ANY, 0, test_calendar }, { "service-watchdogs", VERB_ANY, 2, 0, service_watchdogs }, + { "security", VERB_ANY, VERB_ANY, 0, do_security }, {} }; diff --git a/src/analyze/meson.build b/src/analyze/meson.build index 3a69a259b1..4db4dfa552 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -4,4 +4,6 @@ systemd_analyze_sources = files(''' analyze.c analyze-verify.c analyze-verify.h + analyze-security.c + analyze-security.h '''.split()) diff --git a/src/basic/format-table.c b/src/basic/format-table.c index 844b92f41c..c541e92b3c 100644 --- a/src/basic/format-table.c +++ b/src/basic/format-table.c @@ -10,7 +10,6 @@ #include "gunicode.h" #include "pager.h" #include "parse-util.h" -#include "pretty-print.h" #include "string-util.h" #include "terminal-util.h" #include "time-util.h" diff --git a/src/basic/macro.h b/src/basic/macro.h index 79ab02b27a..0fe6a62aa8 100644 --- a/src/basic/macro.h +++ b/src/basic/macro.h @@ -249,12 +249,13 @@ static inline unsigned long ALIGN_POWER2(unsigned long u) { * computation should be possible in the given type. Therefore, we use * [x / y + !!(x % y)]. Note that on "Real CPUs" a division returns both the * quotient and the remainder, so both should be equally fast. */ -#define DIV_ROUND_UP(_x, _y) \ - __extension__ ({ \ - const typeof(_x) __x = (_x); \ - const typeof(_y) __y = (_y); \ - (__x / __y + !!(__x % __y)); \ - }) +#define DIV_ROUND_UP(x, y) __DIV_ROUND_UP(UNIQ, (x), UNIQ, (y)) +#define __DIV_ROUND_UP(xq, x, yq, y) \ + ({ \ + const typeof(x) UNIQ_T(X, xq) = (x); \ + const typeof(y) UNIQ_T(Y, yq) = (y); \ + (UNIQ_T(X, xq) / UNIQ_T(Y, yq) + !!(UNIQ_T(X, xq) % UNIQ_T(Y, yq))); \ + }) #define assert_message_se(expr, message) \ do { \ diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index f4af0e6522..e2bbe8187d 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1320,10 +1320,38 @@ int terminal_urlify(const char *url, const char *text, char **ret) { return 0; } -int terminal_urlify_path(const char *path, const char *text, char **ret) { +int file_url_from_path(const char *path, char **ret) { _cleanup_free_ char *absolute = NULL; struct utsname u; - const char *url; + char *url = NULL; + int r; + + if (uname(&u) < 0) + return -errno; + + if (!path_is_absolute(path)) { + r = path_make_absolute_cwd(path, &absolute); + if (r < 0) + return r; + + path = absolute; + } + + /* As suggested by https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda, let's include the local + * hostname here. Note that we don't use gethostname_malloc() or gethostname_strict() since we are interested + * in the raw string the kernel has set, whatever it may be, under the assumption that terminals are not overly + * careful with validating the strings either. */ + + url = strjoin("file://", u.nodename, path); + if (!url) + return -ENOMEM; + + *ret = url; + return 0; +} + +int terminal_urlify_path(const char *path, const char *text, char **ret) { + _cleanup_free_ char *url = NULL; int r; assert(path); @@ -1348,27 +1376,14 @@ int terminal_urlify_path(const char *path, const char *text, char **ret) { return 0; } - if (uname(&u) < 0) - return -errno; - - if (!path_is_absolute(path)) { - r = path_make_absolute_cwd(path, &absolute); - if (r < 0) - return r; - - path = absolute; - } - - /* As suggested by https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda, let's include the local - * hostname here. Note that we don't use gethostname_malloc() or gethostname_strict() since we are interested - * in the raw string the kernel has set, whatever it may be, under the assumption that terminals are not overly - * careful with validating the strings either. */ - - url = strjoina("file://", u.nodename, path); + r = file_url_from_path(path, &url); + if (r < 0) + return r; return terminal_urlify(url, text, ret); } + static int cat_file(const char *filename, bool newline) { _cleanup_fclose_ FILE *f = NULL; _cleanup_free_ char *urlified = NULL; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 0055b72343..3f23ecfd3d 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -154,6 +154,7 @@ int open_terminal_in_namespace(pid_t pid, const char *name, int mode); int vt_default_utf8(void); int vt_reset_keyboard(int fd); +int file_url_from_path(const char *path, char **ret); int terminal_urlify(const char *url, const char *text, char **ret); int terminal_urlify_path(const char *path, const char *text, char **ret);