]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/analyze/analyze-security.c
strv: make iterator in STRV_FOREACH() declaread in the loop
[thirdparty/systemd.git] / src / analyze / analyze-security.c
index 63d1998ed373b1df21868cf732bd076981b800eb..458b681143a12a7b3b81de499f8fd9adfedb8f6e 100644 (file)
@@ -3,20 +3,25 @@
 #include <sys/utsname.h>
 
 #include "af-list.h"
+#include "analyze.h"
 #include "analyze-security.h"
 #include "analyze-verify.h"
 #include "bus-error.h"
 #include "bus-map-properties.h"
 #include "bus-unit-util.h"
 #include "bus-util.h"
+#include "copy.h"
 #include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
 #include "format-table.h"
-#include "in-addr-util.h"
+#include "in-addr-prefix-util.h"
 #include "locale-util.h"
 #include "macro.h"
 #include "manager.h"
 #include "missing_capability.h"
 #include "missing_sched.h"
+#include "mkdir.h"
 #include "nulstr-util.h"
 #include "parse-util.h"
 #include "path-util.h"
@@ -1340,8 +1345,8 @@ static const struct security_assessor security_assessor_table[] = {
                 .offset = offsetof(SecurityInfo, restrict_suid_sgid),
         },
         {
-                .id = "RestrictNamespaces=~CLONE_NEWUSER",
-                .json_field = "RestrictNamespaces_CLONE_NEWUSER",
+                .id = "RestrictNamespaces=~user",
+                .json_field = "RestrictNamespaces_user",
                 .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",
@@ -1351,8 +1356,8 @@ static const struct security_assessor security_assessor_table[] = {
                 .parameter = CLONE_NEWUSER,
         },
         {
-                .id = "RestrictNamespaces=~CLONE_NEWNS",
-                .json_field = "RestrictNamespaces_CLONE_NEWNS",
+                .id = "RestrictNamespaces=~mnt",
+                .json_field = "RestrictNamespaces_mnt",
                 .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",
@@ -1362,8 +1367,8 @@ static const struct security_assessor security_assessor_table[] = {
                 .parameter = CLONE_NEWNS,
         },
         {
-                .id = "RestrictNamespaces=~CLONE_NEWIPC",
-                .json_field = "RestrictNamespaces_CLONE_NEWIPC",
+                .id = "RestrictNamespaces=~ipc",
+                .json_field = "RestrictNamespaces_ipc",
                 .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",
@@ -1373,8 +1378,8 @@ static const struct security_assessor security_assessor_table[] = {
                 .parameter = CLONE_NEWIPC,
         },
         {
-                .id = "RestrictNamespaces=~CLONE_NEWPID",
-                .json_field = "RestrictNamespaces_CLONE_NEWPID",
+                .id = "RestrictNamespaces=~pid",
+                .json_field = "RestrictNamespaces_pid",
                 .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",
@@ -1384,8 +1389,8 @@ static const struct security_assessor security_assessor_table[] = {
                 .parameter = CLONE_NEWPID,
         },
         {
-                .id = "RestrictNamespaces=~CLONE_NEWCGROUP",
-                .json_field = "RestrictNamespaces_CLONE_NEWCGROUP",
+                .id = "RestrictNamespaces=~cgroup",
+                .json_field = "RestrictNamespaces_cgroup",
                 .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",
@@ -1395,8 +1400,8 @@ static const struct security_assessor security_assessor_table[] = {
                 .parameter = CLONE_NEWCGROUP,
         },
         {
-                .id = "RestrictNamespaces=~CLONE_NEWNET",
-                .json_field = "RestrictNamespaces_CLONE_NEWNET",
+                .id = "RestrictNamespaces=~net",
+                .json_field = "RestrictNamespaces_net",
                 .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",
@@ -1406,8 +1411,8 @@ static const struct security_assessor security_assessor_table[] = {
                 .parameter = CLONE_NEWNET,
         },
         {
-                .id = "RestrictNamespaces=~CLONE_NEWUTS",
-                .json_field = "RestrictNamespaces_CLONE_NEWUTS",
+                .id = "RestrictNamespaces=~uts",
+                .json_field = "RestrictNamespaces_uts",
                 .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",
@@ -1615,7 +1620,7 @@ static JsonVariant* security_assessor_find_in_policy(const struct security_asses
         if (!policy)
                 return NULL;
         if (!json_variant_is_object(policy)) {
-                log_debug("Specificied policy is not a JSON object, ignoring.");
+                log_debug("Specified policy is not a JSON object, ignoring.");
                 return NULL;
         }
 
@@ -1709,7 +1714,9 @@ static int assess(const SecurityInfo *info,
                   Table *overview_table,
                   AnalyzeSecurityFlags flags,
                   unsigned threshold,
-                  JsonVariant *policy) {
+                  JsonVariant *policy,
+                  PagerFlags pager_flags,
+                  JsonFormatFlags json_format_flags) {
 
         static const struct {
                 uint64_t exposure;
@@ -1732,15 +1739,19 @@ static int assess(const SecurityInfo *info,
         int r;
 
         if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) {
-                details_table = table_new(" ", "name", "description", "weight", "badness", "range", "exposure");
+                details_table = table_new(" ", "name", "json_field", "description", "weight", "badness", "range", "exposure");
                 if (!details_table)
                         return log_oom();
 
+                r = table_set_json_field_name(details_table, 0, "set");
+                if (r < 0)
+                        return log_error_errno(r, "Failed to set JSON field name of column 0: %m");
+
                 (void) table_set_sort(details_table, (size_t) 3, (size_t) 1);
                 (void) table_set_reverse(details_table, 3, true);
 
                 if (getenv_bool("SYSTEMD_ANALYZE_DEBUG") <= 0)
-                        (void) table_set_display(details_table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 6);
+                        (void) table_set_display(details_table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 7);
         }
 
         for (i = 0; i < ELEMENTSOF(security_assessor_table); i++) {
@@ -1758,6 +1769,11 @@ static int assess(const SecurityInfo *info,
                         d = strdup("Service runs in special boot phase, option is not appropriate");
                         if (!d)
                                 return log_oom();
+                } else if (weight == 0) {
+                        badness = UINT64_MAX;
+                        d = strdup("Option excluded by policy, skipping");
+                        if (!d)
+                                return log_oom();
                 } else {
                         r = a->assess(a, info, data, &badness, &d);
                         if (r < 0)
@@ -1774,23 +1790,23 @@ static int assess(const SecurityInfo *info,
                 }
 
                 if (details_table) {
-                        const char *checkmark, *description, *color = NULL;
-                        const char *id = a->id;
+                        const char *description, *color = NULL;
+                        int checkmark;
 
                         if (badness == UINT64_MAX) {
-                                checkmark = " ";
+                                checkmark = -1;
                                 description = access_description_na(a, policy);
                                 color = NULL;
                         } else if (badness == a->range) {
-                                checkmark = special_glyph(SPECIAL_GLYPH_CROSS_MARK);
+                                checkmark = 0;
                                 description = access_description_bad(a, policy);
                                 color = ansi_highlight_red();
                         } else if (badness == 0) {
-                                checkmark = special_glyph(SPECIAL_GLYPH_CHECK_MARK);
+                                checkmark = 1;
                                 description = access_description_good(a, policy);
                                 color = ansi_highlight_green();
                         } else {
-                                checkmark = special_glyph(SPECIAL_GLYPH_CROSS_MARK);
+                                checkmark = 0;
                                 description = NULL;
                                 color = ansi_highlight_red();
                         }
@@ -1798,16 +1814,24 @@ static int assess(const SecurityInfo *info,
                         if (d)
                                 description = d;
 
-                        if (json_variant_by_key(policy, a->json_field) != NULL)
-                                id = a->json_field;
+                        if (checkmark < 0) {
+                                r = table_add_many(details_table, TABLE_EMPTY);
+                                if (r < 0)
+                                        return table_log_add_error(r);
+                        } else {
+                                r = table_add_many(details_table,
+                                                   TABLE_BOOLEAN_CHECKMARK, checkmark > 0,
+                                                   TABLE_SET_MINIMUM_WIDTH, 1,
+                                                   TABLE_SET_MAXIMUM_WIDTH, 1,
+                                                   TABLE_SET_ELLIPSIZE_PERCENT, 0,
+                                                   TABLE_SET_COLOR, color);
+                                if (r < 0)
+                                        return table_log_add_error(r);
+                        }
 
                         r = table_add_many(details_table,
-                                           TABLE_STRING, checkmark,
-                                           TABLE_SET_MINIMUM_WIDTH, 1,
-                                           TABLE_SET_MAXIMUM_WIDTH, 1,
-                                           TABLE_SET_ELLIPSIZE_PERCENT, 0,
-                                           TABLE_SET_COLOR, color,
-                                           TABLE_STRING, id, TABLE_SET_URL, a->url,
+                                           TABLE_STRING, a->id, TABLE_SET_URL, a->url,
+                                           TABLE_STRING, a->json_field,
                                            TABLE_STRING, description,
                                            TABLE_UINT64, weight, TABLE_SET_ALIGN_PERCENT, 100,
                                            TABLE_UINT64, badness, TABLE_SET_ALIGN_PERCENT, 100,
@@ -1829,14 +1853,14 @@ static int assess(const SecurityInfo *info,
                         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));
+                        assert_se(weight = table_get_at(details_table, row, 4));
+                        assert_se(badness = table_get_at(details_table, row, 5));
+                        assert_se(range = table_get_at(details_table, row, 6));
 
                         if (*badness == UINT64_MAX || *badness == 0)
                                 continue;
 
-                        assert_se(cell = table_get_cell(details_table, row, 6));
+                        assert_se(cell = table_get_cell(details_table, row, 7));
 
                         x = DIV_ROUND_UP(DIV_ROUND_UP(*badness * *weight * 100U, *range), weight_sum);
                         xsprintf(buf, "%" PRIu64 ".%" PRIu64, x / 10, x % 10);
@@ -1846,7 +1870,13 @@ static int assess(const SecurityInfo *info,
                                 return log_error_errno(r, "Failed to update cell in table: %m");
                 }
 
-                r = table_print(details_table, stdout);
+                if (json_format_flags & JSON_FORMAT_OFF) {
+                        r = table_hide_column_from_display(details_table, (size_t) 2);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to set columns to display: %m");
+                }
+
+                r = table_print_with_pager(details_table, json_format_flags, pager_flags, /* show_header= */true);
                 if (r < 0)
                         return log_error_errno(r, "Failed to output table: %m");
         }
@@ -1859,7 +1889,7 @@ static int assess(const SecurityInfo *info,
 
         assert(i < ELEMENTSOF(badness_table));
 
-        if (details_table) {
+        if (details_table && (json_format_flags & JSON_FORMAT_OFF)) {
                 _cleanup_free_ char *clickable = NULL;
                 const char *name;
 
@@ -1875,7 +1905,7 @@ static int assess(const SecurityInfo *info,
                         name = info->id;
 
                 printf("\n%s %sOverall exposure level for %s%s: %s%" PRIu64 ".%" PRIu64 " %s%s %s\n",
-                       special_glyph(SPECIAL_GLYPH_ARROW),
+                       special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
                        ansi_highlight(),
                        name,
                        ansi_normal(),
@@ -2386,7 +2416,9 @@ static int analyze_security_one(sd_bus *bus,
                                 Table *overview_table,
                                 AnalyzeSecurityFlags flags,
                                 unsigned threshold,
-                                JsonVariant *policy) {
+                                JsonVariant *policy,
+                                PagerFlags pager_flags,
+                                JsonFormatFlags json_format_flags) {
 
         _cleanup_(security_info_freep) SecurityInfo *info = security_info_new();
         if (!info)
@@ -2403,7 +2435,7 @@ static int analyze_security_one(sd_bus *bus,
         if (r < 0)
                 return r;
 
-        r = assess(info, overview_table, flags, threshold, policy);
+        r = assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
         if (r < 0)
                 return r;
 
@@ -2560,10 +2592,10 @@ static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, Security
                                 return log_oom();
                 }
 
-                IPAddressAccessItem *i;
+                struct in_addr_prefix *i;
                 bool deny_ipv4 = false, deny_ipv6 = false;
 
-                LIST_FOREACH(items, i, g->ip_address_deny) {
+                SET_FOREACH(i, g->ip_address_deny) {
                         if (i->family == AF_INET && i->prefixlen == 0)
                                 deny_ipv4 = true;
                         else if (i->family == AF_INET6 && i->prefixlen == 0)
@@ -2572,7 +2604,7 @@ static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, Security
                 info->ip_address_deny_all = deny_ipv4 && deny_ipv6;
 
                 info->ip_address_allow_localhost = info->ip_address_allow_other = false;
-                LIST_FOREACH(items, i, g->ip_address_allow) {
+                SET_FOREACH(i, g->ip_address_allow) {
                         if (in_addr_is_localhost(i->family, &i->address))
                                 info->ip_address_allow_localhost = true;
                         else
@@ -2589,7 +2621,12 @@ static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, Security
         return 0;
 }
 
-static int offline_security_check(Unit *u, unsigned threshold, JsonVariant *policy) {
+static int offline_security_check(Unit *u,
+                                  unsigned threshold,
+                                  JsonVariant *policy,
+                                  PagerFlags pager_flags,
+                                  JsonFormatFlags json_format_flags) {
+
         _cleanup_(table_unrefp) Table *overview_table = NULL;
         AnalyzeSecurityFlags flags = 0;
         _cleanup_(security_info_freep) SecurityInfo *info = NULL;
@@ -2604,7 +2641,7 @@ static int offline_security_check(Unit *u, unsigned threshold, JsonVariant *poli
         if (r < 0)
               return r;
 
-        return assess(info, overview_table, flags, threshold, policy);
+        return assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
 }
 
 static int offline_security_checks(char **filenames,
@@ -2613,11 +2650,15 @@ static int offline_security_checks(char **filenames,
                                    bool check_man,
                                    bool run_generators,
                                    unsigned threshold,
-                                   const char *root) {
+                                   const char *root,
+                                   const char *profile,
+                                   PagerFlags pager_flags,
+                                   JsonFormatFlags json_format_flags) {
 
         const ManagerTestRunFlags flags =
                 MANAGER_TEST_RUN_MINIMAL |
                 MANAGER_TEST_RUN_ENV_GENERATORS |
+                MANAGER_TEST_RUN_IGNORE_DEPENDENCIES |
                 run_generators * MANAGER_TEST_RUN_GENERATORS;
 
         _cleanup_(manager_freep) Manager *m = NULL;
@@ -2625,7 +2666,6 @@ static int offline_security_checks(char **filenames,
         _cleanup_free_ char *var = NULL;
         int r, k;
         size_t count = 0;
-        char **filename;
 
         if (strv_isempty(filenames))
                 return 0;
@@ -2647,6 +2687,13 @@ static int offline_security_checks(char **filenames,
         if (r < 0)
                 return r;
 
+        if (profile) {
+                /* Ensure the temporary directory is in the search path, so that we can add drop-ins. */
+                r = strv_extend(&m->lookup_paths.search_path, m->lookup_paths.temporary_dir);
+                if (r < 0)
+                        return log_oom();
+        }
+
         log_debug("Loading remaining units from the command line...");
 
         STRV_FOREACH(filename, filenames) {
@@ -2662,6 +2709,33 @@ static int offline_security_checks(char **filenames,
                         continue;
                 }
 
+                /* When a portable image is analyzed, the profile is what provides a good chunk of
+                 * the security-related settings, but they are obviously not shipped with the image.
+                 * This allows to take them in consideration. */
+                if (profile) {
+                        _cleanup_free_ char *unit_name = NULL, *dropin = NULL, *profile_path = NULL;
+
+                        r = path_extract_filename(prepared, &unit_name);
+                        if (r < 0)
+                                return log_oom();
+
+                        dropin = strjoin(m->lookup_paths.temporary_dir, "/", unit_name, ".d/profile.conf");
+                        if (!dropin)
+                                return log_oom();
+                        (void) mkdir_parents(dropin, 0755);
+
+                        if (!is_path(profile)) {
+                                r = find_portable_profile(profile, unit_name, &profile_path);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to find portable profile %s: %m", profile);
+                                profile = profile_path;
+                        }
+
+                        r = copy_file(profile, dropin, 0, 0644, 0, 0, 0);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to copy: %m");
+                }
+
                 k = manager_load_startable_unit_or_warn(m, NULL, prepared, &units[count]);
                 if (k < 0) {
                         if (r == 0)
@@ -2673,7 +2747,7 @@ static int offline_security_checks(char **filenames,
         }
 
         for (size_t i = 0; i < count; i++) {
-                k = offline_security_check(units[i], threshold, policy);
+                k = offline_security_check(units[i], threshold, policy, pager_flags, json_format_flags);
                 if (k < 0 && r == 0)
                         r = k;
         }
@@ -2681,7 +2755,7 @@ static int offline_security_checks(char **filenames,
         return r;
 }
 
-int analyze_security(sd_bus *bus,
+static int analyze_security(sd_bus *bus,
                      char **units,
                      JsonVariant *policy,
                      UnitFileScope scope,
@@ -2690,15 +2764,18 @@ int analyze_security(sd_bus *bus,
                      bool offline,
                      unsigned threshold,
                      const char *root,
+                     const char *profile,
+                     JsonFormatFlags json_format_flags,
+                     PagerFlags pager_flags,
                      AnalyzeSecurityFlags flags) {
 
         _cleanup_(table_unrefp) Table *overview_table = NULL;
         int ret = 0, r;
 
-        assert(bus);
+        assert(!!bus != offline);
 
         if (offline)
-                return offline_security_checks(units, policy, scope, check_man, run_generators, threshold, root);
+                return offline_security_checks(units, policy, scope, check_man, run_generators, threshold, root, profile, pager_flags, json_format_flags);
 
         if (strv_length(units) != 1) {
                 overview_table = table_new("unit", "exposure", "predicate", "happy");
@@ -2711,7 +2788,6 @@ int analyze_security(sd_bus *bus,
                 _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
                 _cleanup_strv_free_ char **list = NULL;
                 size_t n = 0;
-                char **i;
 
                 r = sd_bus_call_method(
                                 bus,
@@ -2758,14 +2834,12 @@ int analyze_security(sd_bus *bus,
                 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, threshold, policy);
+                        r = analyze_security_one(bus, *i, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
                         if (r < 0 && ret >= 0)
                                 ret = r;
                 }
 
-        } else {
-                char **i;
-
+        } else
                 STRV_FOREACH(i, units) {
                         _cleanup_free_ char *mangled = NULL, *instance = NULL;
                         const char *name;
@@ -2793,11 +2867,10 @@ int analyze_security(sd_bus *bus,
                         } else
                                 name = mangled;
 
-                        r = analyze_security_one(bus, name, overview_table, flags, threshold, policy);
+                        r = analyze_security_one(bus, name, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
                         if (r < 0 && ret >= 0)
                                 ret = r;
                 }
-        }
 
         if (overview_table) {
                 if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) {
@@ -2805,10 +2878,57 @@ int analyze_security(sd_bus *bus,
                         fflush(stdout);
                 }
 
-                r = table_print(overview_table, stdout);
+                r = table_print_with_pager(overview_table, json_format_flags, pager_flags, /* show_header= */true);
                 if (r < 0)
                         return log_error_errno(r, "Failed to output table: %m");
         }
-
         return ret;
 }
+
+int verb_security(int argc, char *argv[], void *userdata) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *policy = NULL;
+        int r;
+        unsigned line, column;
+
+        if (!arg_offline) {
+                r = acquire_bus(&bus, NULL);
+                if (r < 0)
+                        return bus_log_connect_error(r, arg_transport);
+        }
+
+        pager_open(arg_pager_flags);
+
+        if (arg_security_policy) {
+                r = json_parse_file(/*f=*/ NULL, arg_security_policy, /*flags=*/ 0, &policy, &line, &column);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse '%s' at %u:%u: %m", arg_security_policy, line, column);
+        } else {
+                _cleanup_fclose_ FILE *f = NULL;
+                _cleanup_free_ char *pp = NULL;
+
+                r = search_and_fopen_nulstr("systemd-analyze-security.policy", "re", /*root=*/ NULL, CONF_PATHS_NULSTR("systemd"), &f, &pp);
+                if (r < 0 && r != -ENOENT)
+                        return r;
+
+                if (f) {
+                        r = json_parse_file(f, pp, /*flags=*/ 0, &policy, &line, &column);
+                        if (r < 0)
+                                return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON policy: %m", pp, line, column);
+                }
+        }
+
+        return analyze_security(bus,
+                                strv_skip(argv, 1),
+                                policy,
+                                arg_scope,
+                                arg_man,
+                                arg_generators,
+                                arg_offline,
+                                arg_threshold,
+                                arg_root,
+                                arg_profile,
+                                arg_json_format_flags,
+                                arg_pager_flags,
+                                /*flags=*/ 0);
+}