#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"
.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",
.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",
.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",
.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",
.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",
.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",
.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",
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;
}
Table *overview_table,
AnalyzeSecurityFlags flags,
unsigned threshold,
- JsonVariant *policy) {
+ JsonVariant *policy,
+ PagerFlags pager_flags,
+ JsonFormatFlags json_format_flags) {
static const struct {
uint64_t exposure;
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++) {
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)
}
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();
}
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,
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);
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");
}
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;
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(),
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)
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;
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)
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
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;
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,
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;
_cleanup_free_ char *var = NULL;
int r, k;
size_t count = 0;
- char **filename;
if (strv_isempty(filenames))
return 0;
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) {
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)
}
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;
}
return r;
}
-int analyze_security(sd_bus *bus,
+static int analyze_security(sd_bus *bus,
char **units,
JsonVariant *policy,
UnitFileScope scope,
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");
_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,
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;
} 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)) {
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);
+}