From 5f1b0cc6d064f7847982e7b680cab3d080aef52e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 6 Nov 2018 17:59:58 +0100 Subject: [PATCH 01/21] locale-util: add logic to output smiley emojis at various happiness levels --- docs/ENVIRONMENT.md | 7 ++- src/basic/locale-util.c | 89 ++++++++++++++++++++++++++----------- src/basic/locale-util.h | 8 ++++ src/test/test-locale-util.c | 9 +++- 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 3e61ea51e7..e965cb885a 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -46,9 +46,14 @@ All tools: are understood, too (us, ms, s, min, h, d, w, month, y). If it is not set or set to 0, then the built-in default is used. -* `$SYSTEMD_MEMPOOL=0` — if set the internal memory caching logic employed by +* `$SYSTEMD_MEMPOOL=0` — if set, the internal memory caching logic employed by hash tables is turned off, and libc malloc() is used for all allocations. +* `$SYSTEMD_EMOJI=0` — if set, tools such as "systemd-analyze security" will + not output graphical smiley emojis, but ASCII alternatives instead. Note that + this only controls use of Unicode emoji glyphs, and has no effect on other + Unicode glyphs. + systemctl: * `$SYSTEMCTL_FORCE_BUS=1` — if set, do not connect to PID1's private D-Bus diff --git a/src/basic/locale-util.c b/src/basic/locale-util.c index b2ef66392e..2225208cd2 100644 --- a/src/basic/locale-util.c +++ b/src/basic/locale-util.c @@ -16,6 +16,7 @@ #include "def.h" #include "dirent-util.h" +#include "env-util.h" #include "fd-util.h" #include "hashmap.h" #include "locale-util.h" @@ -347,6 +348,24 @@ bool keymap_is_valid(const char *name) { return true; } +static bool emoji_enabled(void) { + static int cached_emoji_enabled = -1; + + if (cached_emoji_enabled < 0) { + int val; + + val = getenv_bool("SYSTEMD_EMOJI"); + if (val < 0) + cached_emoji_enabled = + is_locale_utf8() && + !STRPTR_IN_SET(getenv("TERM"), "dumb", "linux"); + else + cached_emoji_enabled = val; + } + + return cached_emoji_enabled; +} + const char *special_glyph(SpecialGlyph code) { /* A list of a number of interesting unicode glyphs we can use to decorate our output. It's probably wise to be @@ -359,40 +378,56 @@ const char *special_glyph(SpecialGlyph code) { static const char* const draw_table[2][_SPECIAL_GLYPH_MAX] = { /* ASCII fallback */ [false] = { - [TREE_VERTICAL] = "| ", - [TREE_BRANCH] = "|-", - [TREE_RIGHT] = "`-", - [TREE_SPACE] = " ", - [TRIANGULAR_BULLET] = ">", - [BLACK_CIRCLE] = "*", - [BULLET] = "*", - [ARROW] = "->", - [MDASH] = "-", - [ELLIPSIS] = "...", - [MU] = "u", - [CHECK_MARK] = "+", - [CROSS_MARK] = "-", + [TREE_VERTICAL] = "| ", + [TREE_BRANCH] = "|-", + [TREE_RIGHT] = "`-", + [TREE_SPACE] = " ", + [TRIANGULAR_BULLET] = ">", + [BLACK_CIRCLE] = "*", + [BULLET] = "*", + [ARROW] = "->", + [MDASH] = "-", + [ELLIPSIS] = "...", + [MU] = "u", + [CHECK_MARK] = "+", + [CROSS_MARK] = "-", + [ECSTATIC_SMILEY] = ":-]", + [HAPPY_SMILEY] = ":-}", + [SLIGHTLY_HAPPY_SMILEY] = ":-)", + [NEUTRAL_SMILEY] = ":-|", + [SLIGHTLY_UNHAPPY_SMILEY] = ":-(", + [UNHAPPY_SMILEY] = ":-{️", + [DEPRESSED_SMILEY] = ":-[", }, /* UTF-8 */ [true] = { - [TREE_VERTICAL] = "\342\224\202 ", /* │ */ - [TREE_BRANCH] = "\342\224\234\342\224\200", /* ├─ */ - [TREE_RIGHT] = "\342\224\224\342\224\200", /* └─ */ - [TREE_SPACE] = " ", /* */ - [TRIANGULAR_BULLET] = "\342\200\243", /* ‣ */ - [BLACK_CIRCLE] = "\342\227\217", /* ● */ - [BULLET] = "\342\200\242", /* • */ - [ARROW] = "\342\206\222", /* → */ - [MDASH] = "\342\200\223", /* – */ - [ELLIPSIS] = "\342\200\246", /* … */ - [MU] = "\316\274", /* μ */ - [CHECK_MARK] = "\342\234\223", /* ✓ */ - [CROSS_MARK] = "\342\234\227", /* ✗ */ + [TREE_VERTICAL] = "\342\224\202 ", /* │ */ + [TREE_BRANCH] = "\342\224\234\342\224\200", /* ├─ */ + [TREE_RIGHT] = "\342\224\224\342\224\200", /* └─ */ + [TREE_SPACE] = " ", /* */ + [TRIANGULAR_BULLET] = "\342\200\243", /* ‣ */ + [BLACK_CIRCLE] = "\342\227\217", /* ● */ + [BULLET] = "\342\200\242", /* • */ + [ARROW] = "\342\206\222", /* → */ + [MDASH] = "\342\200\223", /* – */ + [ELLIPSIS] = "\342\200\246", /* … */ + [MU] = "\316\274", /* μ */ + [CHECK_MARK] = "\342\234\223", /* ✓ */ + [CROSS_MARK] = "\342\234\227", /* ✗ */ + [ECSTATIC_SMILEY] = "\360\237\230\207", /* 😇 */ + [HAPPY_SMILEY] = "\360\237\230\200", /* 😀 */ + [SLIGHTLY_HAPPY_SMILEY] = "\360\237\231\202", /* 🙂 */ + [NEUTRAL_SMILEY] = "\360\237\230\220", /* 😐 */ + [SLIGHTLY_UNHAPPY_SMILEY] = "\360\237\231\201", /* 🙁 */ + [UNHAPPY_SMILEY] = "\360\237\230\250", /* 😨️️ */ + [DEPRESSED_SMILEY] = "\360\237\244\242", /* 🤢 */ }, }; - return draw_table[is_locale_utf8()][code]; + assert(code < _SPECIAL_GLYPH_MAX); + + return draw_table[code >= _SPECIAL_GLYPH_FIRST_SMILEY ? emoji_enabled() : is_locale_utf8()][code]; } void locale_variables_free(char*l[_VARIABLE_LC_MAX]) { diff --git a/src/basic/locale-util.h b/src/basic/locale-util.h index 2aa2bef8c5..8f4ce5485d 100644 --- a/src/basic/locale-util.h +++ b/src/basic/locale-util.h @@ -52,6 +52,14 @@ typedef enum { MU, CHECK_MARK, CROSS_MARK, + _SPECIAL_GLYPH_FIRST_SMILEY, + ECSTATIC_SMILEY = _SPECIAL_GLYPH_FIRST_SMILEY, + HAPPY_SMILEY, + SLIGHTLY_HAPPY_SMILEY, + NEUTRAL_SMILEY, + SLIGHTLY_UNHAPPY_SMILEY, + UNHAPPY_SMILEY, + DEPRESSED_SMILEY, _SPECIAL_GLYPH_MAX } SpecialGlyph; diff --git a/src/test/test-locale-util.c b/src/test/test-locale-util.c index a1d4307eb5..48994547ac 100644 --- a/src/test/test-locale-util.c +++ b/src/test/test-locale-util.c @@ -65,7 +65,7 @@ static void test_keymaps(void) { #define dump_glyph(x) log_info(STRINGIFY(x) ": %s", special_glyph(x)) static void dump_special_glyphs(void) { - assert_cc(CROSS_MARK + 1 == _SPECIAL_GLYPH_MAX); + assert_cc(DEPRESSED_SMILEY + 1 == _SPECIAL_GLYPH_MAX); log_info("/* %s */", __func__); @@ -84,6 +84,13 @@ static void dump_special_glyphs(void) { dump_glyph(MU); dump_glyph(CHECK_MARK); dump_glyph(CROSS_MARK); + dump_glyph(ECSTATIC_SMILEY); + dump_glyph(HAPPY_SMILEY); + dump_glyph(SLIGHTLY_HAPPY_SMILEY); + dump_glyph(NEUTRAL_SMILEY); + dump_glyph(SLIGHTLY_UNHAPPY_SMILEY); + dump_glyph(UNHAPPY_SMILEY); + dump_glyph(DEPRESSED_SMILEY); } int main(int argc, char *argv[]) { From ad5ffe3716e76469d2d595b648b8e13c86a0c94b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Nov 2018 20:13:54 +0100 Subject: [PATCH 02/21] seccomp-util: drop process_vm_readv from @debug group it's already part of @ipc, no need to have it in both. Given that @ipc is much more popular (as it is part of @system-service for example), let's not define it a second time. --- src/shared/seccomp-util.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/seccomp-util.c b/src/shared/seccomp-util.c index 67ccd43c3f..42d6dd2a94 100644 --- a/src/shared/seccomp-util.c +++ b/src/shared/seccomp-util.c @@ -371,8 +371,6 @@ const SyscallFilterSet syscall_filter_sets[_SYSCALL_FILTER_SET_MAX] = { .value = "lookup_dcookie\0" "perf_event_open\0" - "process_vm_readv\0" - "process_vm_writev\0" "ptrace\0" "rtas\0" #ifdef __NR_s390_runtime_instr From 90bea7448dd73d89d7aea5a9f1d7df5693f12ad6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 6 Nov 2018 12:02:55 +0100 Subject: [PATCH 03/21] analyze: fix include order --- src/analyze/analyze.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 62c62ca526..42a3aff67e 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -17,9 +17,9 @@ #include "bus-unit-util.h" #include "bus-util.h" #include "calendarspec.h" -#include "def.h" #include "conf-files.h" #include "copy.h" +#include "def.h" #include "fd-util.h" #include "fileio.h" #include "glob-util.h" From 0ed3da7c8c5e42009c4351f8af1010b2af9b75b4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 6 Nov 2018 12:04:23 +0100 Subject: [PATCH 04/21] analyze: remove unnecessary negation of bus_error_message() error number parameter --- src/analyze/analyze.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 42a3aff67e..446b4432ee 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -169,7 +169,7 @@ static int bus_get_uint64_property(sd_bus *bus, const char *path, const char *in 't', val); if (r < 0) - return log_error_errno(r, "Failed to parse reply: %s", bus_error_message(&error, -r)); + return log_error_errno(r, "Failed to parse reply: %s", bus_error_message(&error, r)); return 0; } @@ -192,7 +192,7 @@ static int bus_get_unit_property_strv(sd_bus *bus, const char *path, const char &error, strv); if (r < 0) - return log_error_errno(r, "Failed to get unit property %s: %s", property, bus_error_message(&error, -r)); + return log_error_errno(r, "Failed to get unit property %s: %s", property, bus_error_message(&error, r)); return 0; } @@ -355,7 +355,7 @@ static int acquire_time_data(sd_bus *bus, struct unit_times **out) { &error, &reply, NULL); if (r < 0) - return log_error_errno(r, "Failed to list units: %s", bus_error_message(&error, -r)); + 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) @@ -978,7 +978,7 @@ static int list_dependencies(sd_bus *bus, const char *name) { &reply, "s"); if (r < 0) - return log_error_errno(r, "Failed to get ID: %s", bus_error_message(&error, -r)); + return log_error_errno(r, "Failed to get ID: %s", bus_error_message(&error, r)); r = sd_bus_message_read(reply, "s", &id); if (r < 0) @@ -1238,7 +1238,7 @@ static int dot(int argc, char *argv[], void *userdata) { &reply, ""); if (r < 0) - log_error_errno(r, "Failed to list units: %s", bus_error_message(&error, -r)); + 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) From 2987225ce6c3800821fe71e1ad5a698785ca54a3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 6 Nov 2018 12:05:26 +0100 Subject: [PATCH 05/21] analyze: use strv_skip() where appropriate --- src/analyze/analyze.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 446b4432ee..02c9c13204 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -1340,15 +1340,16 @@ static int dump(int argc, char *argv[], void *userdata) { } static int cat_config(int argc, char *argv[], void *userdata) { - char **arg; + char **arg, **list; int r; (void) pager_open(arg_pager_flags); - STRV_FOREACH(arg, argv + 1) { + list = strv_skip(argv, 1); + STRV_FOREACH(arg, list) { const char *t = NULL; - if (arg != argv + 1) + if (arg != list) print_separator(); if (path_is_absolute(*arg)) { From b06f0cc6258b7135182f3b29244a7df0ab203ab6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 6 Nov 2018 12:06:45 +0100 Subject: [PATCH 06/21] parse-util: allow parse_boolean() to take a NULL argument It's pretty useful to allow parse_boolean() to take a NULL argument and return an error in that case, rather than abort. i.e. making this a runtime rather than programming error allows us to shorten code elsewhere. --- src/basic/parse-util.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/basic/parse-util.c b/src/basic/parse-util.c index 718357e290..5b4e94c134 100644 --- a/src/basic/parse-util.c +++ b/src/basic/parse-util.c @@ -20,7 +20,8 @@ #include "string-util.h" int parse_boolean(const char *v) { - assert(v); + if (!v) + return -EINVAL; if (streq(v, "1") || strcaseeq(v, "yes") || strcaseeq(v, "y") || strcaseeq(v, "true") || strcaseeq(v, "t") || strcaseeq(v, "on")) return 1; From 8a453c9dfca19e229f9ef3bc026dc2a775246875 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Nov 2018 11:37:21 +0100 Subject: [PATCH 07/21] macro: update DIV_ROUND_UP() so that it can be called nested --- src/basic/macro.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/basic/macro.h b/src/basic/macro.h index 0a258cc614..f54f13e4fe 100644 --- a/src/basic/macro.h +++ b/src/basic/macro.h @@ -246,11 +246,12 @@ 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) \ +#define DIV_ROUND_UP(x, y) __DIV_ROUND_UP(UNIQ, (x), UNIQ, (y)) +#define __DIV_ROUND_UP(xq, x, yq, y) \ ({ \ - const typeof(_x) __x = (_x); \ - const typeof(_y) __y = (_y); \ - (__x / __y + !!(__x % __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))); \ }) #ifdef __COVERITY__ From 62d6a1cc9f89734db245f208643c052ce6e1e56c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Nov 2018 11:09:03 +0100 Subject: [PATCH 08/21] terminal-util: split out file:// generation from terminal_urlify_path() This way we can use it at other places, for example when preparing URLs for format_table_set_url() --- src/shared/pretty-print.c | 52 +++++++++++++++++++++++++-------------- src/shared/pretty-print.h | 2 ++ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/shared/pretty-print.c b/src/shared/pretty-print.c index 1086018645..de6274a3da 100644 --- a/src/shared/pretty-print.c +++ b/src/shared/pretty-print.c @@ -60,10 +60,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); @@ -88,23 +116,9 @@ 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); } diff --git a/src/shared/pretty-print.h b/src/shared/pretty-print.h index cf9a70dd9e..12ab9acf58 100644 --- a/src/shared/pretty-print.h +++ b/src/shared/pretty-print.h @@ -3,6 +3,8 @@ void print_separator(void); +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); int terminal_urlify_man(const char *page, const char *section, char **ret); From 13b0d4d7bdb674d0e51a6d595abd1e7bf2691bf9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Nov 2018 21:16:23 +0100 Subject: [PATCH 09/21] format-table: when duplicating a cell, also copy the color --- src/shared/format-table.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 10e15c9d70..38dfb145c9 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -398,6 +398,8 @@ static int table_dedup_cell(Table *t, TableCell *cell) { if (!nd) return -ENOMEM; + nd->color = od->color; + table_data_unref(od); t->data[i] = nd; From 165ca5663e9859083c70d793a6b4aa4f3b2af24c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 6 Nov 2018 12:06:14 +0100 Subject: [PATCH 10/21] format-table: optionally make specific cells clickable links --- src/shared/format-table.c | 79 ++++++++++++++++++++++++++++++++++----- src/shared/format-table.h | 1 + 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 38dfb145c9..a99f1cff3c 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -9,6 +9,7 @@ #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" @@ -58,6 +59,7 @@ typedef struct TableData { unsigned align_percent; /* 0 … 100, where to pad with spaces when expanding is needed. 0: left-aligned, 100: right-aligned */ const char *color; /* ANSI color string to use for this cell. When written to terminal should not move cursor. Will automatically be reset after the cell */ + char *url; /* A URL to use for a clickable hyperlink */ char *formatted; /* A cached textual representation of the cell data, before ellipsation/alignment */ union { @@ -175,6 +177,8 @@ static TableData *table_data_free(TableData *d) { assert(d); free(d->formatted); + free(d->url); + return mfree(d); } @@ -376,6 +380,7 @@ int table_dup_cell(Table *t, TableCell *cell) { } static int table_dedup_cell(Table *t, TableCell *cell) { + _cleanup_free_ char *curl = NULL; TableData *nd, *od; size_t i; @@ -394,11 +399,25 @@ static int table_dedup_cell(Table *t, TableCell *cell) { assert(od->n_ref > 1); - nd = table_data_new(od->type, od->data, od->minimum_width, od->maximum_width, od->weight, od->align_percent, od->ellipsize_percent); + if (od->url) { + curl = strdup(od->url); + if (!curl) + return -ENOMEM; + } + + nd = table_data_new( + od->type, + od->data, + od->minimum_width, + od->maximum_width, + od->weight, + od->align_percent, + od->ellipsize_percent); if (!nd) return -ENOMEM; nd->color = od->color; + nd->url = TAKE_PTR(curl); table_data_unref(od); t->data[i] = nd; @@ -526,6 +545,26 @@ int table_set_color(Table *t, TableCell *cell, const char *color) { return 0; } +int table_set_url(Table *t, TableCell *cell, const char *url) { + _cleanup_free_ char *copy = NULL; + int r; + + assert(t); + assert(cell); + + if (url) { + copy = strdup(url); + if (!copy) + return -ENOMEM; + } + + r = table_dedup_cell(t, cell); + if (r < 0) + return r; + + return free_and_replace(table_get_data(t, cell)->url, copy); +} + int table_add_many_internal(Table *t, TableDataType first_type, ...) { TableDataType type; va_list ap; @@ -840,11 +879,13 @@ static int table_data_requested_width(TableData *d, size_t *ret) { return 0; } -static char *align_string_mem(const char *str, size_t new_length, unsigned percent) { - size_t w = 0, space, lspace, old_length; +static char *align_string_mem(const char *str, const char *url, size_t new_length, unsigned percent) { + size_t w = 0, space, lspace, old_length, clickable_length; + _cleanup_free_ char *clickable = NULL; const char *p; char *ret; size_t i; + int r; /* As with ellipsize_mem(), 'old_length' is a byte size while 'new_length' is a width in character cells */ @@ -853,6 +894,15 @@ static char *align_string_mem(const char *str, size_t new_length, unsigned perce old_length = strlen(str); + if (url) { + r = terminal_urlify(url, str, &clickable); + if (r < 0) + return NULL; + + clickable_length = strlen(clickable); + } else + clickable_length = old_length; + /* Determine current width on screen */ p = str; while (p < str + old_length) { @@ -869,23 +919,23 @@ static char *align_string_mem(const char *str, size_t new_length, unsigned perce /* Already wider than the target, if so, don't do anything */ if (w >= new_length) - return strndup(str, old_length); + return clickable ? TAKE_PTR(clickable) : strdup(str); /* How much spaces shall we add? An how much on the left side? */ space = new_length - w; lspace = space * percent / 100U; - ret = new(char, space + old_length + 1); + ret = new(char, space + clickable_length + 1); if (!ret) return NULL; for (i = 0; i < lspace; i++) ret[i] = ' '; - memcpy(ret + lspace, str, old_length); - for (i = lspace + old_length; i < space + old_length; i++) + memcpy(ret + lspace, clickable ?: str, clickable_length); + for (i = lspace + clickable_length; i < space + clickable_length; i++) ret[i] = ' '; - ret[space + old_length] = 0; + ret[space + clickable_length] = 0; return ret; } @@ -1138,13 +1188,24 @@ int table_print(Table *t, FILE *f) { } else if (l < width[j]) { /* Field is shorter than allocated space. Let's align with spaces */ - buffer = align_string_mem(field, width[j], d->align_percent); + buffer = align_string_mem(field, d->url, width[j], d->align_percent); if (!buffer) return -ENOMEM; field = buffer; } + if (l >= width[j] && d->url) { + _cleanup_free_ char *clickable = NULL; + + r = terminal_urlify(d->url, field, &clickable); + if (r < 0) + return r; + + free_and_replace(buffer, clickable); + field = buffer; + } + if (j > 0) fputc(' ', f); /* column separator */ diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 6dc2d16052..9978a8baf2 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -42,6 +42,7 @@ int table_set_weight(Table *t, TableCell *cell, unsigned weight); int table_set_align_percent(Table *t, TableCell *cell, unsigned percent); int table_set_ellipsize_percent(Table *t, TableCell *cell, unsigned percent); int table_set_color(Table *t, TableCell *cell, const char *color); +int table_set_url(Table *t, TableCell *cell, const char *color); int table_add_many_internal(Table *t, TableDataType first_type, ...); #define table_add_many(t, ...) table_add_many_internal(t, __VA_ARGS__, _TABLE_DATA_TYPE_MAX) From a22318e55492af721879d8692ed039144696bb08 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 6 Nov 2018 18:37:21 +0100 Subject: [PATCH 11/21] format-table: before outputting a color, check if colors are available This is in many cases redundant, as a similar check is done by various callers already, but in other cases (where we read the color from a static table for example), it's nice to let the color check be done by the table code itself, and since it doesn't hurt in the other cases just do it again. --- src/shared/format-table.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index a99f1cff3c..959bfc53c4 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -1209,13 +1209,13 @@ int table_print(Table *t, FILE *f) { if (j > 0) fputc(' ', f); /* column separator */ - if (d->color) + if (d->color && colors_enabled()) fputs(d->color, f); fputs(field, f); - if (d->color) - fputs(ansi_normal(), f); + if (d->color && colors_enabled()) + fputs(ANSI_NORMAL, f); } fputc('\n', f); From a4661181fa702a8bff4644210ba7ea14bea51a4a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Nov 2018 15:25:51 +0100 Subject: [PATCH 12/21] format-table: add option to store/format percent and uint64_t values in cells --- src/shared/format-table.c | 48 +++++++++++++++++++++++++++++++++++++++ src/shared/format-table.h | 2 ++ 2 files changed, 50 insertions(+) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 959bfc53c4..ce79d19a57 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -70,6 +70,8 @@ typedef struct TableData { uint64_t size; char string[0]; uint32_t uint32; + uint64_t uint64; + int percent; /* we use 'int' as datatype for percent values in order to match the result of parse_percent() */ /* … add more here as we start supporting more cell data types … */ }; } TableData; @@ -219,11 +221,15 @@ static size_t table_data_size(TableDataType type, const void *data) { return sizeof(usec_t); case TABLE_SIZE: + case TABLE_UINT64: return sizeof(uint64_t); case TABLE_UINT32: return sizeof(uint32_t); + case TABLE_PERCENT: + return sizeof(int); + default: assert_not_reached("Uh? Unexpected cell type"); } @@ -583,6 +589,8 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) { uint64_t size; usec_t usec; uint32_t uint32; + uint64_t uint64; + int percent; bool b; } buffer; @@ -617,6 +625,16 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) { data = &buffer.uint32; break; + case TABLE_UINT64: + buffer.uint64 = va_arg(ap, uint64_t); + data = &buffer.uint64; + break; + + case TABLE_PERCENT: + buffer.percent = va_arg(ap, int); + data = &buffer.percent; + break; + case _TABLE_DATA_TYPE_MAX: /* Used as end marker */ va_end(ap); @@ -740,6 +758,12 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t case TABLE_UINT32: return CMP(a->uint32, b->uint32); + case TABLE_UINT64: + return CMP(a->uint64, b->uint64); + + case TABLE_PERCENT: + return CMP(a->percent, b->percent); + default: ; } @@ -850,6 +874,30 @@ static const char *table_data_format(TableData *d) { break; } + case TABLE_UINT64: { + _cleanup_free_ char *p; + + p = new(char, DECIMAL_STR_WIDTH(d->uint64) + 1); + if (!p) + return NULL; + + sprintf(p, "%" PRIu64, d->uint64); + d->formatted = TAKE_PTR(p); + break; + } + + case TABLE_PERCENT: { + _cleanup_free_ char *p; + + p = new(char, DECIMAL_STR_WIDTH(d->percent) + 2); + if (!p) + return NULL; + + sprintf(p, "%i%%" , d->percent); + d->formatted = TAKE_PTR(p); + break; + } + default: assert_not_reached("Unexpected type?"); } diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 9978a8baf2..2db2084062 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -15,6 +15,8 @@ typedef enum TableDataType { TABLE_TIMESPAN, TABLE_SIZE, TABLE_UINT32, + TABLE_UINT64, + TABLE_PERCENT, _TABLE_DATA_TYPE_MAX, _TABLE_DATA_TYPE_INVALID = -1, } TableDataType; From a2c73e2d3823e878de7a7ee193631108c5fc5be0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Nov 2018 17:41:32 +0100 Subject: [PATCH 13/21] format-table: optionally allow reversing the sort order for a column --- src/shared/format-table.c | 22 +++++++++++++++++++++- src/shared/format-table.h | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index ce79d19a57..59133007e2 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -107,6 +107,8 @@ struct Table { size_t *sort_map; /* The columns to order rows by, in order of preference. */ size_t n_sort_map; + + bool *reverse_map; }; Table *table_new_raw(size_t n_columns) { @@ -199,6 +201,7 @@ Table *table_unref(Table *t) { free(t->data); free(t->display_map); free(t->sort_map); + free(t->reverse_map); return mfree(t); } @@ -797,7 +800,7 @@ static int table_data_compare(const size_t *a, const size_t *b, Table *t) { r = cell_data_compare(d, *a, dd, *b); if (r != 0) - return r; + return t->reverse_map && t->reverse_map[t->sort_map[i]] ? -r : r; } /* Order identical lines by the order there were originally added in */ @@ -1310,3 +1313,20 @@ size_t table_get_columns(Table *t) { assert(t->n_columns > 0); return t->n_columns; } + +int table_set_reverse(Table *t, size_t column, bool b) { + assert(t); + assert(column < t->n_columns); + + if (!t->reverse_map) { + if (!b) + return 0; + + t->reverse_map = new0(bool, t->n_columns); + if (!t->reverse_map) + return -ENOMEM; + } + + t->reverse_map[column] = b; + return 0; +} diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 2db2084062..07cb2351cb 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -53,6 +53,7 @@ void table_set_header(Table *table, bool b); void table_set_width(Table *t, size_t width); int table_set_display(Table *t, size_t first_column, ...); int table_set_sort(Table *t, size_t first_column, ...); +int table_set_reverse(Table *t, size_t column, bool b); int table_print(Table *t, FILE *f); int table_format(Table *t, char **ret); From 27e730e6d0a7709c17ccef170f10846e92dca2a0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Nov 2018 21:17:47 +0100 Subject: [PATCH 14/21] format-table: add table_update() to update existing entries --- src/shared/format-table.c | 40 +++++++++++++++++++++++++++++++++++++++ src/shared/format-table.h | 2 ++ 2 files changed, 42 insertions(+) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 59133007e2..9b9796a625 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -574,6 +574,46 @@ int table_set_url(Table *t, TableCell *cell, const char *url) { return free_and_replace(table_get_data(t, cell)->url, copy); } +int table_update(Table *t, TableCell *cell, TableDataType type, const void *data) { + _cleanup_free_ char *curl = NULL; + TableData *nd, *od; + size_t i; + + assert(t); + assert(cell); + + i = TABLE_CELL_TO_INDEX(cell); + if (i >= t->n_cells) + return -ENXIO; + + assert_se(od = t->data[i]); + + if (od->url) { + curl = strdup(od->url); + if (!curl) + return -ENOMEM; + } + + nd = table_data_new( + type, + data, + od->minimum_width, + od->maximum_width, + od->weight, + od->align_percent, + od->ellipsize_percent); + if (!nd) + return -ENOMEM; + + nd->color = od->color; + nd->url = TAKE_PTR(curl); + + table_data_unref(od); + t->data[i] = nd; + + return 0; +} + int table_add_many_internal(Table *t, TableDataType first_type, ...) { TableDataType type; va_list ap; diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 07cb2351cb..4273c8c49b 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -46,6 +46,8 @@ int table_set_ellipsize_percent(Table *t, TableCell *cell, unsigned percent); int table_set_color(Table *t, TableCell *cell, const char *color); int table_set_url(Table *t, TableCell *cell, const char *color); +int table_update(Table *t, TableCell *cell, TableDataType type, const void *data); + int table_add_many_internal(Table *t, TableDataType first_type, ...); #define table_add_many(t, ...) table_add_many_internal(t, __VA_ARGS__, _TABLE_DATA_TYPE_MAX) From 9314ead7853a1479fc60eb2ae7e3d0a77b7eba7c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Nov 2018 21:21:09 +0100 Subject: [PATCH 15/21] format-table: add an API for getting the cell at a specific row/column --- src/shared/format-table.c | 15 +++++++++++++++ src/shared/format-table.h | 2 ++ 2 files changed, 17 insertions(+) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 9b9796a625..5292843acb 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -1370,3 +1370,18 @@ int table_set_reverse(Table *t, size_t column, bool b) { t->reverse_map[column] = b; return 0; } + +TableCell *table_get_cell(Table *t, size_t row, size_t column) { + size_t i; + + assert(t); + + if (column >= t->n_columns) + return NULL; + + i = row * t->n_columns + column; + if (i >= t->n_cells) + return NULL; + + return TABLE_INDEX_TO_CELL(i); +} diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 4273c8c49b..40fea79f78 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -66,3 +66,5 @@ static inline TableCell* TABLE_HEADER_CELL(size_t i) { size_t table_get_rows(Table *t); size_t table_get_columns(Table *t); + +TableCell *table_get_cell(Table *t, size_t row, size_t column); From 30d98de00c68440ff4d77d851b4b3323c34027da Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Nov 2018 21:39:28 +0100 Subject: [PATCH 16/21] format-table: always underline header line --- src/shared/format-table.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 5292843acb..b5af0f5fe0 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -1297,15 +1297,22 @@ int table_print(Table *t, FILE *f) { field = buffer; } + if (row == t->data) /* underline header line fully, including the column separator */ + fputs(ansi_underline(), f); + if (j > 0) fputc(' ', f); /* column separator */ - if (d->color && colors_enabled()) + if (d->color && colors_enabled()) { + if (row == t->data) /* first undo header underliner */ + fputs(ANSI_NORMAL, f); + fputs(d->color, f); + } fputs(field, f); - if (d->color && colors_enabled()) + if (colors_enabled() && (d->color || row == t->data)) fputs(ANSI_NORMAL, f); } From 62d99b39709f903f8a66a9aae757deb5546a53eb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Nov 2018 11:38:12 +0100 Subject: [PATCH 17/21] format-table: add calls to query the data in a specific cell --- src/shared/format-table.c | 22 ++++++++++++++++++++++ src/shared/format-table.h | 3 +++ 2 files changed, 25 insertions(+) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index b5af0f5fe0..8189c3475d 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -1392,3 +1392,25 @@ TableCell *table_get_cell(Table *t, size_t row, size_t column) { return TABLE_INDEX_TO_CELL(i); } + +const void *table_get(Table *t, TableCell *cell) { + TableData *d; + + assert(t); + + d = table_get_data(t, cell); + if (!d) + return NULL; + + return d->data; +} + +const void* table_get_at(Table *t, size_t row, size_t column) { + TableCell *cell; + + cell = table_get_cell(t, row, column); + if (!cell) + return NULL; + + return table_get(t, cell); +} diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 40fea79f78..a2bb2e0846 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -68,3 +68,6 @@ size_t table_get_rows(Table *t); size_t table_get_columns(Table *t); TableCell *table_get_cell(Table *t, size_t row, size_t column); + +const void *table_get(Table *t, TableCell *cell); +const void *table_get_at(Table *t, size_t row, size_t column); From 88db94fa57c9a5b1a0b926c49d3624fc84c88090 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 14 Nov 2018 18:39:37 +0100 Subject: [PATCH 18/21] format-table: make sure we never call memcmp() with NULL parameters --- src/shared/format-table.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 8189c3475d..e685efea0e 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -275,7 +275,7 @@ static bool table_data_matches( if (k != l) return false; - return memcmp(data, d->data, l) == 0; + return memcmp_safe(data, d->data, l) == 0; } static TableData *table_data_new( From ec16f3b6dd8b03e3ce6eff1fa9f21432208ef42b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Nov 2018 09:32:17 +0100 Subject: [PATCH 19/21] analyze: add new security verb --- src/analyze/analyze-security.c | 2079 ++++++++++++++++++++++++++++++++ src/analyze/analyze-security.h | 12 + src/analyze/analyze.c | 16 + src/analyze/meson.build | 2 + 4 files changed, 2109 insertions(+) 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..ceee83cd83 --- /dev/null +++ b/src/analyze/analyze-security.c @@ -0,0 +1,2079 @@ +/* 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 "pretty-print.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 02c9c13204..2b9cb65256 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -12,6 +12,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" @@ -1784,6 +1785,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_pager_flags); + + return analyze_security(bus, strv_skip(argv, 1), 0); +} + static int help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -1828,6 +1842,7 @@ static int help(int argc, char *argv[], void *userdata) { " calendar SPEC... Validate repetitive calendar time events\n" " service-watchdogs [BOOL] Get/set service watchdog state\n" " timespan SPAN... Validate a time span\n" + " security [UNIT...] Analyze security of unit\n" "\nSee the %s for details.\n" , program_invocation_short_name , link @@ -2015,6 +2030,7 @@ static int run(int argc, char *argv[]) { { "calendar", 2, VERB_ANY, 0, test_calendar }, { "service-watchdogs", VERB_ANY, 2, 0, service_watchdogs }, { "timespan", 2, VERB_ANY, 0, dump_timespan }, + { "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()) From ee93c1e664a7bbc59f1578e285c871999507b14d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Nov 2018 12:19:30 +0100 Subject: [PATCH 20/21] man: document systemd-analyze security --- man/systemd-analyze.xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index 15e95b0c8f..7becf0133e 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -112,6 +112,12 @@ timespan SPAN + + systemd-analyze + OPTIONS + security + UNIT + @@ -263,6 +269,29 @@ NAutoVTs=8 The time span should adhere to the same syntax documented in systemd.time7. Values without associated magnitudes are parsed as seconds. + systemd-analyze security analyzes the security and sandboxing settings of one or more + specified service units. If at least one unit name is specified the security settings of the specified service + units are inspected and a detailed analysis is shown. If no unit name is specified, all currently loaded, + long-running service units are inspected and a terse table with results shown. The command checks for various + security-related service settings, assigning each a numeric "exposure level" value, depending on how important a + setting is. It then calculates an overall exposure level for the whole unit, which is an estimation in the range + 0.0…10.0 indicating how exposed a service is security-wise. High exposure levels indicate very little applied + sandboxing. Low exposure levels indicate tight sandboxing and strongest security restrictions. Note that this only + analyzes the per-service security features systemd itself implements. This means that any additional security + mechanisms applied by the service code itself are not accounted for. The exposure level determined this way should + not be misunderstood: a high exposure level neither means that there is no effective sandboxing applied by the + service code itself, nor that the service is actually vulnerable to remote or local attacks. High exposure levels + do indicate however that most likely the service might benefit from additional settings applied to them. Please + note that many of the security and sandboxing settings individually can be circumvented — unless combined with + others. For example, if a service retains the privilege to establish or undo mount points many of the sandboxing + options can be undone by the service code itself. Due to that is essential that each service uses the most + comprehensive and strict sandboxing and security settings possible. The tool will take into account some of these + combinations and relationships between the settings, but not all. Also note that the security and sandboxing + settings analyzed here only apply to the operations executed by the service code itself. If a service has access to + an IPC system (such as D-Bus) it might request operations from other services that are not subject to the same + restrictions. Any comprehensive security and sandboxing analysis is hence incomplete if the IPC access policy is + not validated too. + If no command is passed, systemd-analyze time is implied. From d96c081aa585ee3b648d588fef370388f276ffaf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Nov 2018 17:41:55 +0100 Subject: [PATCH 21/21] update TODO --- TODO | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TODO b/TODO index d82cfbda67..cafd75a01d 100644 --- a/TODO +++ b/TODO @@ -25,6 +25,10 @@ Features: * when we fork off generators and such, lower LIMIT_NOFILE soft limit to 1K +* rework seccomp/nnp logic that that even if User= is used in combination with + a seccomp option we don't have to set NNP. For that, change uid first whil + keeping CAP_SYS_ADMIN, then apply seccomp, the drop cap. + * add a concept for automatically loading per-unit secrets off disk and inserting them into the kernel keyring. Maybe SecretsDirectory= similar to ConfigurationDirectory=. @@ -49,6 +53,9 @@ Features: * set memory.oom.group in cgroupsv2 for all leaf cgroups (kernel v4.19+) +* add a new syscall group "@esoteric" for more esoteric stuff such as bpf() and + usefaultd() and make systemd-analyze check for it. + * drop umask() calls and suchlike from our generators, pid1 should set things up correctly anyway * paranoia: whenever we process passwords, call mlock() on the memory @@ -290,9 +297,6 @@ Features: * beef up pam_systemd to take unit file settings such as cgroups properties as parameters -* a new "systemd-analyze security" tool outputting a checklist of security - features a service does and does not implement - * maybe hook of xfs/ext4 quotactl() with services? i.e. automatically manage the quota of a the user indicated in User= via unit file settings, like the other resource management concepts. Would mix nicely with DynamicUser=1. Or