summaryrefslogtreecommitdiffstats
path: root/src/analyze
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 15:35:18 +0000
commitb750101eb236130cf056c675997decbac904cc49 (patch)
treea5df1a06754bdd014cb975c051c83b01c9a97532 /src/analyze
parentInitial commit. (diff)
downloadsystemd-b750101eb236130cf056c675997decbac904cc49.tar.xz
systemd-b750101eb236130cf056c675997decbac904cc49.zip
Adding upstream version 252.22.upstream/252.22upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/analyze/analyze-blame.c69
-rw-r--r--src/analyze/analyze-blame.h4
-rw-r--r--src/analyze/analyze-calendar.c146
-rw-r--r--src/analyze/analyze-calendar.h4
-rw-r--r--src/analyze/analyze-capability.c56
-rw-r--r--src/analyze/analyze-capability.h4
-rw-r--r--src/analyze/analyze-cat-config.c46
-rw-r--r--src/analyze/analyze-cat-config.h4
-rw-r--r--src/analyze/analyze-compare-versions.c42
-rw-r--r--src/analyze/analyze-compare-versions.h3
-rw-r--r--src/analyze/analyze-condition.c136
-rw-r--r--src/analyze/analyze-condition.h4
-rw-r--r--src/analyze/analyze-critical-chain.c230
-rw-r--r--src/analyze/analyze-critical-chain.h4
-rw-r--r--src/analyze/analyze-dot.c182
-rw-r--r--src/analyze/analyze-dot.h4
-rw-r--r--src/analyze/analyze-dump.c160
-rw-r--r--src/analyze/analyze-dump.h4
-rw-r--r--src/analyze/analyze-exit-status.c56
-rw-r--r--src/analyze/analyze-exit-status.h4
-rw-r--r--src/analyze/analyze-filesystems.c225
-rw-r--r--src/analyze/analyze-filesystems.h4
-rw-r--r--src/analyze/analyze-inspect-elf.c134
-rw-r--r--src/analyze/analyze-inspect-elf.h4
-rw-r--r--src/analyze/analyze-log-control.c22
-rw-r--r--src/analyze/analyze-log-control.h4
-rw-r--r--src/analyze/analyze-plot.c395
-rw-r--r--src/analyze/analyze-plot.h4
-rw-r--r--src/analyze/analyze-security.c2962
-rw-r--r--src/analyze/analyze-security.h10
-rw-r--r--src/analyze/analyze-service-watchdogs.c41
-rw-r--r--src/analyze/analyze-service-watchdogs.h4
-rw-r--r--src/analyze/analyze-syscall-filter.c207
-rw-r--r--src/analyze/analyze-syscall-filter.h4
-rw-r--r--src/analyze/analyze-time-data.c297
-rw-r--r--src/analyze/analyze-time-data.h56
-rw-r--r--src/analyze/analyze-time.c22
-rw-r--r--src/analyze/analyze-time.h4
-rw-r--r--src/analyze/analyze-timespan.c72
-rw-r--r--src/analyze/analyze-timespan.h4
-rw-r--r--src/analyze/analyze-timestamp.c96
-rw-r--r--src/analyze/analyze-timestamp.h4
-rw-r--r--src/analyze/analyze-unit-files.c50
-rw-r--r--src/analyze/analyze-unit-files.h4
-rw-r--r--src/analyze/analyze-unit-paths.c20
-rw-r--r--src/analyze/analyze-unit-paths.h4
-rw-r--r--src/analyze/analyze-verify-util.c390
-rw-r--r--src/analyze/analyze-verify-util.h23
-rw-r--r--src/analyze/analyze-verify.c70
-rw-r--r--src/analyze/analyze-verify.h4
-rw-r--r--src/analyze/analyze.c614
-rw-r--r--src/analyze/analyze.h44
-rw-r--r--src/analyze/meson.build64
-rw-r--r--src/analyze/test-verify.c16
54 files changed, 7040 insertions, 0 deletions
diff --git a/src/analyze/analyze-blame.c b/src/analyze/analyze-blame.c
new file mode 100644
index 0000000..c911268
--- /dev/null
+++ b/src/analyze/analyze-blame.c
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-blame.h"
+#include "analyze-time-data.h"
+#include "format-table.h"
+
+int verb_blame(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ TableCell *cell;
+ int n, r;
+
+ r = acquire_bus(&bus, NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ n = acquire_time_data(bus, &times);
+ if (n <= 0)
+ return n;
+
+ table = table_new("time", "unit");
+ if (!table)
+ return log_oom();
+
+ table_set_header(table, false);
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ r = table_set_align_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ r = table_set_sort(table, (size_t) 0);
+ if (r < 0)
+ return r;
+
+ r = table_set_reverse(table, 0, true);
+ if (r < 0)
+ return r;
+
+ for (UnitTimes *u = times; u->has_data; u++) {
+ if (u->time <= 0)
+ continue;
+
+ r = table_add_many(table,
+ TABLE_TIMESPAN_MSEC, u->time,
+ TABLE_STRING, u->name);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ pager_open(arg_pager_flags);
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return r;
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-blame.h b/src/analyze/analyze-blame.h
new file mode 100644
index 0000000..d9aa985
--- /dev/null
+++ b/src/analyze/analyze-blame.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_blame(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-calendar.c b/src/analyze/analyze-calendar.c
new file mode 100644
index 0000000..952fe95
--- /dev/null
+++ b/src/analyze/analyze-calendar.c
@@ -0,0 +1,146 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-calendar.h"
+#include "calendarspec.h"
+#include "format-table.h"
+#include "terminal-util.h"
+
+static int test_calendar_one(usec_t n, const char *p) {
+ _cleanup_(calendar_spec_freep) CalendarSpec *spec = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_free_ char *t = NULL;
+ TableCell *cell;
+ int r;
+
+ r = calendar_spec_from_string(p, &spec);
+ if (r < 0) {
+ log_error_errno(r, "Failed to parse calendar specification '%s': %m", p);
+ time_parsing_hint(p, /* calendar= */ false, /* timestamp= */ true, /* timespan= */ true);
+ return r;
+ }
+
+ r = calendar_spec_to_string(spec, &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format calendar specification '%s': %m", p);
+
+ table = table_new("name", "value");
+ if (!table)
+ return log_oom();
+
+ table_set_header(table, false);
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ r = table_set_align_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ if (!streq(t, p)) {
+ r = table_add_many(table,
+ TABLE_STRING, "Original form:",
+ TABLE_STRING, p);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_STRING, "Normalized form:",
+ TABLE_STRING, t);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ for (unsigned i = 0; i < arg_iterations; i++) {
+ usec_t next;
+
+ r = calendar_spec_next_usec(spec, n, &next);
+ if (r == -ENOENT) {
+ if (i == 0) {
+ r = table_add_many(table,
+ TABLE_STRING, "Next elapse:",
+ TABLE_STRING, "never",
+ TABLE_SET_COLOR, ansi_highlight_yellow());
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ break;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine next elapse for '%s': %m", p);
+
+ if (i == 0) {
+ r = table_add_many(table,
+ TABLE_STRING, "Next elapse:",
+ TABLE_TIMESTAMP, next,
+ TABLE_SET_COLOR, ansi_highlight_blue());
+ if (r < 0)
+ return table_log_add_error(r);
+ } else {
+ int k = DECIMAL_STR_WIDTH(i + 1);
+
+ if (k < 8)
+ k = 8 - k;
+ else
+ k = 0;
+
+ r = table_add_cell_stringf(table, NULL, "Iter. #%u:", i+1);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_TIMESTAMP, next,
+ TABLE_SET_COLOR, ansi_highlight_blue());
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ if (!in_utc_timezone()) {
+ r = table_add_many(table,
+ TABLE_STRING, "(in UTC):",
+ TABLE_TIMESTAMP_UTC, next);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_many(table,
+ TABLE_STRING, "From now:",
+ TABLE_TIMESTAMP_RELATIVE, next);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ n = next;
+ }
+
+ return table_print(table, NULL);
+}
+
+int verb_calendar(int argc, char *argv[], void *userdata) {
+ int r = 0;
+ usec_t n;
+
+ if (arg_base_time != USEC_INFINITY)
+ n = arg_base_time;
+ else
+ n = now(CLOCK_REALTIME); /* We want to use the same "base" for all expressions */
+
+ STRV_FOREACH(p, strv_skip(argv, 1)) {
+ int k;
+
+ k = test_calendar_one(n, *p);
+ if (r == 0 && k < 0)
+ r = k;
+
+ if (p[1])
+ putchar('\n');
+ }
+
+ return r;
+}
diff --git a/src/analyze/analyze-calendar.h b/src/analyze/analyze-calendar.h
new file mode 100644
index 0000000..3d6eac2
--- /dev/null
+++ b/src/analyze/analyze-calendar.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_calendar(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-capability.c b/src/analyze/analyze-capability.c
new file mode 100644
index 0000000..8072175
--- /dev/null
+++ b/src/analyze/analyze-capability.c
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-capability.h"
+#include "cap-list.h"
+#include "capability-util.h"
+#include "format-table.h"
+
+int verb_capabilities(int argc, char *argv[], void *userdata) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ unsigned last_cap;
+ int r;
+
+ table = table_new("name", "number");
+ if (!table)
+ return log_oom();
+
+ (void) table_set_align_percent(table, table_get_cell(table, 0, 1), 100);
+
+ /* Determine the maximum of the last cap known by the kernel and by us */
+ last_cap = MAX((unsigned) CAP_LAST_CAP, cap_last_cap());
+
+ if (strv_isempty(strv_skip(argv, 1)))
+ for (unsigned c = 0; c <= last_cap; c++) {
+ r = table_add_many(table,
+ TABLE_STRING, capability_to_name(c) ?: "cap_???",
+ TABLE_UINT, c);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ else {
+ for (int i = 1; i < argc; i++) {
+ int c;
+
+ c = capability_from_name(argv[i]);
+ if (c < 0 || (unsigned) c > last_cap)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Capability \"%s\" not known.", argv[i]);
+
+ r = table_add_many(table,
+ TABLE_STRING, capability_to_name(c) ?: "cap_???",
+ TABLE_UINT, (unsigned) c);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ (void) table_set_sort(table, (size_t) 1);
+ }
+
+ pager_open(arg_pager_flags);
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return r;
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-capability.h b/src/analyze/analyze-capability.h
new file mode 100644
index 0000000..07ff088
--- /dev/null
+++ b/src/analyze/analyze-capability.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_capabilities(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-cat-config.c b/src/analyze/analyze-cat-config.c
new file mode 100644
index 0000000..d214cea
--- /dev/null
+++ b/src/analyze/analyze-cat-config.c
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-cat-config.h"
+#include "conf-files.h"
+#include "def.h"
+#include "nulstr-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "strv.h"
+
+int verb_cat_config(int argc, char *argv[], void *userdata) {
+ char **list;
+ int r;
+
+ pager_open(arg_pager_flags);
+
+ list = strv_skip(argv, 1);
+ STRV_FOREACH(arg, list) {
+ const char *t = NULL;
+
+ if (arg != list)
+ print_separator();
+
+ if (path_is_absolute(*arg)) {
+ const char *dir;
+
+ NULSTR_FOREACH(dir, CONF_PATHS_NULSTR("")) {
+ t = path_startswith(*arg, dir);
+ if (t)
+ break;
+ }
+
+ if (!t)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Path %s does not start with any known prefix.", *arg);
+ } else
+ t = *arg;
+
+ r = conf_files_cat(arg_root, t);
+ if (r < 0)
+ return r;
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-cat-config.h b/src/analyze/analyze-cat-config.h
new file mode 100644
index 0000000..64e87a3
--- /dev/null
+++ b/src/analyze/analyze-cat-config.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_cat_config(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-compare-versions.c b/src/analyze/analyze-compare-versions.c
new file mode 100644
index 0000000..07b52a5
--- /dev/null
+++ b/src/analyze/analyze-compare-versions.c
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "analyze-compare-versions.h"
+#include "compare-operator.h"
+#include "macro.h"
+#include "string-util.h"
+#include "strv.h"
+
+int verb_compare_versions(int argc, char *argv[], void *userdata) {
+ int r;
+
+ assert(IN_SET(argc, 3, 4));
+ assert(argv);
+
+ if (argc == 3) {
+ r = strverscmp_improved(ASSERT_PTR(argv[1]), ASSERT_PTR(argv[2]));
+ printf("%s %s %s\n",
+ isempty(argv[1]) ? "''" : argv[1],
+ comparison_operator(r),
+ isempty(argv[2]) ? "''" : argv[2]);
+
+ /* This matches the exit convention used by rpmdev-vercmp.
+ * We don't use named values because 11 and 12 don't have names. */
+ return r < 0 ? 12 : r > 0 ? 11 : 0;
+
+ } else {
+ const char *op = ASSERT_PTR(argv[2]);
+ CompareOperator operator;
+
+ operator = parse_compare_operator(&op, COMPARE_ALLOW_TEXTUAL);
+ if (operator < 0 || !isempty(op))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown operator \"%s\".", op);
+
+ r = version_or_fnmatch_compare(operator, ASSERT_PTR(argv[1]), ASSERT_PTR(argv[3]));
+ if (r < 0)
+ return log_error_errno(r, "Failed to compare versions: %m");
+
+ return r ? EXIT_SUCCESS : EXIT_FAILURE;
+ }
+}
diff --git a/src/analyze/analyze-compare-versions.h b/src/analyze/analyze-compare-versions.h
new file mode 100644
index 0000000..ac90ede
--- /dev/null
+++ b/src/analyze/analyze-compare-versions.h
@@ -0,0 +1,3 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+int verb_compare_versions(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-condition.c b/src/analyze/analyze-condition.c
new file mode 100644
index 0000000..f43876a
--- /dev/null
+++ b/src/analyze/analyze-condition.c
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdlib.h>
+
+#include "analyze.h"
+#include "analyze-condition.h"
+#include "analyze-verify-util.h"
+#include "condition.h"
+#include "conf-parser.h"
+#include "load-fragment.h"
+#include "service.h"
+
+static int parse_condition(Unit *u, const char *line) {
+ assert(u);
+ assert(line);
+
+ for (ConditionType t = 0; t < _CONDITION_TYPE_MAX; t++) {
+ ConfigParserCallback callback;
+ Condition **target;
+ const char *p, *name;
+
+ name = condition_type_to_string(t);
+ p = startswith(line, name);
+ if (p)
+ target = &u->conditions;
+ else {
+ name = assert_type_to_string(t);
+ p = startswith(line, name);
+ if (!p)
+ continue;
+
+ target = &u->asserts;
+ }
+
+ p += strspn(p, WHITESPACE);
+
+ if (*p != '=')
+ continue;
+ p++;
+
+ p += strspn(p, WHITESPACE);
+
+ if (condition_takes_path(t))
+ callback = config_parse_unit_condition_path;
+ else
+ callback = config_parse_unit_condition_string;
+
+ return callback(NULL, "(cmdline)", 0, NULL, 0, name, t, p, target, u);
+ }
+
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot parse \"%s\".", line);
+}
+
+_printf_(7, 8)
+static int log_helper(void *userdata, int level, int error, const char *file, int line, const char *func, const char *format, ...) {
+ Unit *u = ASSERT_PTR(userdata);
+ va_list ap;
+ int r;
+
+ /* "upgrade" debug messages */
+ level = MIN(LOG_INFO, level);
+
+ va_start(ap, format);
+ r = log_object_internalv(level, error, file, line, func,
+ NULL,
+ u->id,
+ NULL,
+ NULL,
+ format, ap);
+ va_end(ap);
+
+ return r;
+}
+
+static int verify_conditions(char **lines, LookupScope scope, const char *unit, const char *root) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *u;
+ int r, q = 1;
+
+ if (unit) {
+ r = verify_set_unit_path(STRV_MAKE(unit));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set unit load path: %m");
+ }
+
+ r = manager_new(scope, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize manager: %m");
+
+ log_debug("Starting manager...");
+ r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root);
+ if (r < 0)
+ return r;
+
+ if (unit) {
+ _cleanup_free_ char *prepared = NULL;
+
+ r = verify_prepare_filename(unit, &prepared);
+ if (r < 0)
+ return log_error_errno(r, "Failed to prepare filename %s: %m", unit);
+
+ r = manager_load_startable_unit_or_warn(m, NULL, prepared, &u);
+ if (r < 0)
+ return r;
+ } else {
+ r = unit_new_for_name(m, sizeof(Service), "test.service", &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create test.service: %m");
+
+ STRV_FOREACH(line, lines) {
+ r = parse_condition(u, *line);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ r = condition_test_list(u->asserts, environ, assert_type_to_string, log_helper, u);
+ if (u->asserts)
+ log_notice("Asserts %s.", r > 0 ? "succeeded" : "failed");
+
+ q = condition_test_list(u->conditions, environ, condition_type_to_string, log_helper, u);
+ if (u->conditions)
+ log_notice("Conditions %s.", q > 0 ? "succeeded" : "failed");
+
+ return r > 0 && q > 0 ? 0 : -EIO;
+}
+
+int verb_condition(int argc, char *argv[], void *userdata) {
+ int r;
+
+ r = verify_conditions(strv_skip(argv, 1), arg_scope, arg_unit, arg_root);
+ if (r < 0)
+ return r;
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-condition.h b/src/analyze/analyze-condition.h
new file mode 100644
index 0000000..28ef51a
--- /dev/null
+++ b/src/analyze/analyze-condition.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_condition(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-critical-chain.c b/src/analyze/analyze-critical-chain.c
new file mode 100644
index 0000000..f782f95
--- /dev/null
+++ b/src/analyze/analyze-critical-chain.c
@@ -0,0 +1,230 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze-critical-chain.h"
+#include "analyze-time-data.h"
+#include "analyze.h"
+#include "bus-error.h"
+#include "copy.h"
+#include "path-util.h"
+#include "sort-util.h"
+#include "special.h"
+#include "static-destruct.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+static Hashmap *unit_times_hashmap = NULL;
+STATIC_DESTRUCTOR_REGISTER(unit_times_hashmap, hashmap_freep);
+
+static int list_dependencies_print(
+ const char *name,
+ unsigned level,
+ unsigned branches,
+ bool last,
+ UnitTimes *times,
+ BootTimes *boot) {
+
+ for (unsigned i = level; i != 0; i--)
+ printf("%s", special_glyph(branches & (1 << (i-1)) ? SPECIAL_GLYPH_TREE_VERTICAL : SPECIAL_GLYPH_TREE_SPACE));
+
+ printf("%s", special_glyph(last ? SPECIAL_GLYPH_TREE_RIGHT : SPECIAL_GLYPH_TREE_BRANCH));
+
+ if (times) {
+ if (times->time > 0)
+ printf("%s%s @%s +%s%s", ansi_highlight_red(), name,
+ FORMAT_TIMESPAN(times->activating - boot->userspace_time, USEC_PER_MSEC),
+ FORMAT_TIMESPAN(times->time, USEC_PER_MSEC), ansi_normal());
+ else if (times->activated > boot->userspace_time)
+ printf("%s @%s", name, FORMAT_TIMESPAN(times->activated - boot->userspace_time, USEC_PER_MSEC));
+ else
+ printf("%s", name);
+ } else
+ printf("%s", name);
+ printf("\n");
+
+ return 0;
+}
+
+static int list_dependencies_get_dependencies(sd_bus *bus, const char *name, char ***deps) {
+ _cleanup_free_ char *path = NULL;
+
+ assert(bus);
+ assert(name);
+ assert(deps);
+
+ path = unit_dbus_path_from_name(name);
+ if (!path)
+ return -ENOMEM;
+
+ return bus_get_unit_property_strv(bus, path, "After", deps);
+}
+
+static int list_dependencies_compare(char *const *a, char *const *b) {
+ usec_t usa = 0, usb = 0;
+ UnitTimes *times;
+
+ times = hashmap_get(unit_times_hashmap, *a);
+ if (times)
+ usa = times->activated;
+ times = hashmap_get(unit_times_hashmap, *b);
+ if (times)
+ usb = times->activated;
+
+ return CMP(usb, usa);
+}
+
+static bool times_in_range(const UnitTimes *times, const BootTimes *boot) {
+ return times && times->activated > 0 && times->activated <= boot->finish_time;
+}
+
+static int list_dependencies_one(sd_bus *bus, const char *name, unsigned level, char ***units, unsigned branches) {
+ _cleanup_strv_free_ char **deps = NULL;
+ int r;
+ usec_t service_longest = 0;
+ int to_print = 0;
+ UnitTimes *times;
+ BootTimes *boot;
+
+ if (strv_extend(units, name))
+ return log_oom();
+
+ r = list_dependencies_get_dependencies(bus, name, &deps);
+ if (r < 0)
+ return r;
+
+ typesafe_qsort(deps, strv_length(deps), list_dependencies_compare);
+
+ r = acquire_boot_times(bus, &boot);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(c, deps) {
+ times = hashmap_get(unit_times_hashmap, *c);
+ if (times_in_range(times, boot) && times->activated >= service_longest)
+ service_longest = times->activated;
+ }
+
+ if (service_longest == 0)
+ return r;
+
+ STRV_FOREACH(c, deps) {
+ times = hashmap_get(unit_times_hashmap, *c);
+ if (times_in_range(times, boot) && service_longest - times->activated <= arg_fuzz)
+ to_print++;
+ }
+
+ if (!to_print)
+ return r;
+
+ STRV_FOREACH(c, deps) {
+ times = hashmap_get(unit_times_hashmap, *c);
+ if (!times_in_range(times, boot) || service_longest - times->activated > arg_fuzz)
+ continue;
+
+ to_print--;
+
+ r = list_dependencies_print(*c, level, branches, to_print == 0, times, boot);
+ if (r < 0)
+ return r;
+
+ if (strv_contains(*units, *c)) {
+ r = list_dependencies_print("...", level + 1, (branches << 1) | (to_print ? 1 : 0),
+ true, NULL, boot);
+ if (r < 0)
+ return r;
+ continue;
+ }
+
+ r = list_dependencies_one(bus, *c, level + 1, units, (branches << 1) | (to_print ? 1 : 0));
+ if (r < 0)
+ return r;
+
+ if (to_print == 0)
+ break;
+ }
+ return 0;
+}
+
+static int list_dependencies(sd_bus *bus, const char *name) {
+ _cleanup_strv_free_ char **units = NULL;
+ UnitTimes *times;
+ int r;
+ const char *id;
+ _cleanup_free_ char *path = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ BootTimes *boot;
+
+ assert(bus);
+
+ path = unit_dbus_path_from_name(name);
+ if (!path)
+ return -ENOMEM;
+
+ r = sd_bus_get_property(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Unit",
+ "Id",
+ &error,
+ &reply,
+ "s");
+ if (r < 0)
+ 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)
+ return bus_log_parse_error(r);
+
+ times = hashmap_get(unit_times_hashmap, id);
+
+ r = acquire_boot_times(bus, &boot);
+ if (r < 0)
+ return r;
+
+ if (times) {
+ if (times->time)
+ printf("%s%s +%s%s\n", ansi_highlight_red(), id,
+ FORMAT_TIMESPAN(times->time, USEC_PER_MSEC), ansi_normal());
+ else if (times->activated > boot->userspace_time)
+ printf("%s @%s\n", id,
+ FORMAT_TIMESPAN(times->activated - boot->userspace_time, USEC_PER_MSEC));
+ else
+ printf("%s\n", id);
+ }
+
+ return list_dependencies_one(bus, name, 0, &units, 0);
+}
+
+int verb_critical_chain(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL;
+ int n, r;
+
+ r = acquire_bus(&bus, NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ n = acquire_time_data(bus, &times);
+ if (n <= 0)
+ return n;
+
+ for (UnitTimes *u = times; u->has_data; u++) {
+ r = hashmap_ensure_put(&unit_times_hashmap, &string_hash_ops, u->name, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add entry to hashmap: %m");
+ }
+
+ pager_open(arg_pager_flags);
+
+ puts("The time when unit became active or started is printed after the \"@\" character.\n"
+ "The time the unit took to start is printed after the \"+\" character.\n");
+
+ if (argc > 1)
+ STRV_FOREACH(name, strv_skip(argv, 1))
+ list_dependencies(bus, *name);
+ else
+ list_dependencies(bus, SPECIAL_DEFAULT_TARGET);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-critical-chain.h b/src/analyze/analyze-critical-chain.h
new file mode 100644
index 0000000..844249c
--- /dev/null
+++ b/src/analyze/analyze-critical-chain.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_critical_chain(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-dot.c b/src/analyze/analyze-dot.c
new file mode 100644
index 0000000..bf8aa81
--- /dev/null
+++ b/src/analyze/analyze-dot.c
@@ -0,0 +1,182 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-dot.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "bus-unit-util.h"
+#include "glob-util.h"
+#include "terminal-util.h"
+
+static int graph_one_property(
+ sd_bus *bus,
+ const UnitInfo *u,
+ const char *prop,
+ const char *color,
+ char *patterns[],
+ char *from_patterns[],
+ char *to_patterns[]) {
+
+ _cleanup_strv_free_ char **units = NULL;
+ bool match_patterns;
+ int r;
+
+ assert(u);
+ assert(prop);
+ assert(color);
+
+ match_patterns = strv_fnmatch(patterns, u->id);
+
+ if (!strv_isempty(from_patterns) && !match_patterns && !strv_fnmatch(from_patterns, u->id))
+ return 0;
+
+ r = bus_get_unit_property_strv(bus, u->unit_path, prop, &units);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(unit, units) {
+ bool match_patterns2;
+
+ match_patterns2 = strv_fnmatch(patterns, *unit);
+
+ if (!strv_isempty(to_patterns) && !match_patterns2 && !strv_fnmatch(to_patterns, *unit))
+ continue;
+
+ if (!strv_isempty(patterns) && !match_patterns && !match_patterns2)
+ continue;
+
+ printf("\t\"%s\"->\"%s\" [color=\"%s\"];\n", u->id, *unit, color);
+ }
+
+ return 0;
+}
+
+static int graph_one(sd_bus *bus, const UnitInfo *u, char *patterns[], char *from_patterns[], char *to_patterns[]) {
+ int r;
+
+ assert(bus);
+ assert(u);
+
+ if (IN_SET(arg_dot, DEP_ORDER, DEP_ALL)) {
+ r = graph_one_property(bus, u, "After", "green", patterns, from_patterns, to_patterns);
+ if (r < 0)
+ return r;
+ }
+
+ if (IN_SET(arg_dot, DEP_REQUIRE, DEP_ALL)) {
+ r = graph_one_property(bus, u, "Requires", "black", patterns, from_patterns, to_patterns);
+ if (r < 0)
+ return r;
+ r = graph_one_property(bus, u, "Requisite", "darkblue", patterns, from_patterns, to_patterns);
+ if (r < 0)
+ return r;
+ r = graph_one_property(bus, u, "Wants", "grey66", patterns, from_patterns, to_patterns);
+ if (r < 0)
+ return r;
+ r = graph_one_property(bus, u, "Conflicts", "red", patterns, from_patterns, to_patterns);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int expand_patterns(sd_bus *bus, char **patterns, char ***ret) {
+ _cleanup_strv_free_ char **expanded_patterns = NULL;
+ int r;
+
+ STRV_FOREACH(pattern, patterns) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *unit = NULL, *unit_id = NULL;
+
+ if (strv_extend(&expanded_patterns, *pattern) < 0)
+ return log_oom();
+
+ if (string_is_glob(*pattern))
+ continue;
+
+ unit = unit_dbus_path_from_name(*pattern);
+ if (!unit)
+ return log_oom();
+
+ r = sd_bus_get_property_string(
+ bus,
+ "org.freedesktop.systemd1",
+ unit,
+ "org.freedesktop.systemd1.Unit",
+ "Id",
+ &error,
+ &unit_id);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get ID: %s", bus_error_message(&error, r));
+
+ if (!streq(*pattern, unit_id)) {
+ if (strv_extend(&expanded_patterns, unit_id) < 0)
+ return log_oom();
+ }
+ }
+
+ *ret = TAKE_PTR(expanded_patterns); /* do not free */
+
+ return 0;
+}
+
+int verb_dot(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **expanded_patterns = NULL;
+ _cleanup_strv_free_ char **expanded_from_patterns = NULL;
+ _cleanup_strv_free_ char **expanded_to_patterns = NULL;
+ int r;
+ UnitInfo u;
+
+ r = acquire_bus(&bus, NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ r = expand_patterns(bus, strv_skip(argv, 1), &expanded_patterns);
+ if (r < 0)
+ return r;
+
+ r = expand_patterns(bus, arg_dot_from_patterns, &expanded_from_patterns);
+ if (r < 0)
+ return r;
+
+ r = expand_patterns(bus, arg_dot_to_patterns, &expanded_to_patterns);
+ if (r < 0)
+ return r;
+
+ r = bus_call_method(bus, bus_systemd_mgr, "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);
+
+ printf("digraph systemd {\n");
+
+ while ((r = bus_parse_unit_info(reply, &u)) > 0) {
+
+ r = graph_one(bus, &u, expanded_patterns, expanded_from_patterns, expanded_to_patterns);
+ if (r < 0)
+ return r;
+ }
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ printf("}\n");
+
+ log_info(" Color legend: black = Requires\n"
+ " dark blue = Requisite\n"
+ " dark grey = Wants\n"
+ " red = Conflicts\n"
+ " green = After\n");
+
+ if (on_tty() && !arg_quiet)
+ log_notice("-- You probably want to process this output with graphviz' dot tool.\n"
+ "-- Try a shell pipeline like 'systemd-analyze dot | dot -Tsvg > systemd.svg'!\n");
+
+ return 0;
+}
diff --git a/src/analyze/analyze-dot.h b/src/analyze/analyze-dot.h
new file mode 100644
index 0000000..144b43d
--- /dev/null
+++ b/src/analyze/analyze-dot.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_dot(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-dump.c b/src/analyze/analyze-dump.c
new file mode 100644
index 0000000..2e838c9
--- /dev/null
+++ b/src/analyze/analyze-dump.c
@@ -0,0 +1,160 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-bus.h"
+
+#include "analyze-dump.h"
+#include "analyze.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "bus-util.h"
+#include "copy.h"
+
+static int dump_fallback(sd_bus *bus) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ const char *text;
+ int r;
+
+ assert(bus);
+
+ r = bus_call_method(bus, bus_systemd_mgr, "Dump", &error, &reply, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to call Dump: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "s", &text);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ fputs(text, stdout);
+ return 0;
+}
+
+static int dump_fd_reply(sd_bus_message *message) {
+ int fd, r;
+
+ r = sd_bus_message_read(message, "h", &fd);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ fflush(stdout);
+ r = copy_bytes(fd, STDOUT_FILENO, UINT64_MAX, 0);
+ if (r < 0)
+ return r;
+
+ return 1; /* Success */
+}
+
+static int dump(sd_bus *bus) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ int r;
+
+ r = bus_call_method(bus, bus_systemd_mgr, "DumpByFileDescriptor", &error, &reply, NULL);
+ if (IN_SET(r, -EACCES, -EBADR))
+ return 0; /* Fall back to non-fd method. We need to do this even if the bus supports sending
+ * fds to cater to very old managers which didn't have the fd-based method. */
+ if (r < 0)
+ return log_error_errno(r, "Failed to call DumpByFileDescriptor: %s",
+ bus_error_message(&error, r));
+
+ return dump_fd_reply(reply);
+}
+
+static int dump_patterns_fallback(sd_bus *bus, char **patterns) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL;
+ const char *text;
+ int r;
+
+ r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "DumpUnitsMatchingPatterns");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, patterns);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, 0, &error, &reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to call DumpUnitsMatchingPatterns: %s",
+ bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "s", &text);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ fputs(text, stdout);
+ return 0;
+}
+
+static int dump_patterns(sd_bus *bus, char **patterns) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL;
+ int r;
+
+ r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "DumpUnitsMatchingPatternsByFileDescriptor");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, patterns);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, 0, &error, &reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to call DumpUnitsMatchingPatternsByFileDescriptor: %s",
+ bus_error_message(&error, r));
+
+ return dump_fd_reply(reply);
+}
+
+static int mangle_patterns(char **args, char ***ret) {
+ _cleanup_strv_free_ char **mangled = NULL;
+ int r;
+
+ STRV_FOREACH(arg, args) {
+ char *t;
+
+ r = unit_name_mangle_with_suffix(*arg, NULL, UNIT_NAME_MANGLE_GLOB, ".service", &t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to mangle name '%s': %m", *arg);
+
+ r = strv_consume(&mangled, t);
+ if (r < 0)
+ return log_oom();
+ }
+
+ if (strv_isempty(mangled))
+ mangled = strv_free(mangled);
+
+ *ret = TAKE_PTR(mangled);
+ return 0;
+}
+
+int verb_dump(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **patterns = NULL;
+ int r;
+
+ r = acquire_bus(&bus, NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ pager_open(arg_pager_flags);
+
+ r = mangle_patterns(strv_skip(argv, 1), &patterns);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_can_send(bus, SD_BUS_TYPE_UNIX_FD);
+ if (r < 0)
+ return log_error_errno(r, "Unable to determine if bus connection supports fd passing: %m");
+ if (r > 0)
+ r = patterns ? dump_patterns(bus, patterns) : dump(bus);
+ if (r == 0) /* wasn't supported */
+ r = patterns ? dump_patterns_fallback(bus, patterns) : dump_fallback(bus);
+ if (r < 0)
+ return r;
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-dump.h b/src/analyze/analyze-dump.h
new file mode 100644
index 0000000..5d6107c
--- /dev/null
+++ b/src/analyze/analyze-dump.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_dump(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-exit-status.c b/src/analyze/analyze-exit-status.c
new file mode 100644
index 0000000..3a8d3f4
--- /dev/null
+++ b/src/analyze/analyze-exit-status.c
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-exit-status.h"
+#include "exit-status.h"
+#include "format-table.h"
+
+int verb_exit_status(int argc, char *argv[], void *userdata) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r;
+
+ table = table_new("name", "status", "class");
+ if (!table)
+ return log_oom();
+
+ r = table_set_align_percent(table, table_get_cell(table, 0, 1), 100);
+ if (r < 0)
+ return log_error_errno(r, "Failed to right-align status: %m");
+
+ if (strv_isempty(strv_skip(argv, 1)))
+ for (size_t i = 0; i < ELEMENTSOF(exit_status_mappings); i++) {
+ if (!exit_status_mappings[i].name)
+ continue;
+
+ r = table_add_many(table,
+ TABLE_STRING, exit_status_mappings[i].name,
+ TABLE_INT, (int) i,
+ TABLE_STRING, exit_status_class(i));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ else
+ for (int i = 1; i < argc; i++) {
+ int status;
+
+ status = exit_status_from_string(argv[i]);
+ if (status < 0)
+ return log_error_errno(status, "Invalid exit status \"%s\".", argv[i]);
+
+ assert(status >= 0 && (size_t) status < ELEMENTSOF(exit_status_mappings));
+ r = table_add_many(table,
+ TABLE_STRING, exit_status_mappings[status].name ?: "-",
+ TABLE_INT, status,
+ TABLE_STRING, exit_status_class(status) ?: "-");
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ pager_open(arg_pager_flags);
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return r;
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-exit-status.h b/src/analyze/analyze-exit-status.h
new file mode 100644
index 0000000..ce14cdb
--- /dev/null
+++ b/src/analyze/analyze-exit-status.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_exit_status(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-filesystems.c b/src/analyze/analyze-filesystems.c
new file mode 100644
index 0000000..e30b3a6
--- /dev/null
+++ b/src/analyze/analyze-filesystems.c
@@ -0,0 +1,225 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-filesystems.h"
+#include "fileio.h"
+#include "filesystems.h"
+#include "set.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+static int load_available_kernel_filesystems(Set **ret) {
+ _cleanup_set_free_ Set *filesystems = NULL;
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ assert(ret);
+
+ /* Let's read the available filesystems */
+
+ r = read_virtual_file("/proc/filesystems", SIZE_MAX, &t, NULL);
+ if (r < 0)
+ return r;
+
+ for (int i = 0;;) {
+ _cleanup_free_ char *line = NULL;
+ const char *p;
+
+ r = string_extract_line(t, i++, &line);
+ if (r < 0)
+ return log_oom();
+ if (r == 0)
+ break;
+
+ if (!line)
+ line = t;
+
+ p = strchr(line, '\t');
+ if (!p)
+ continue;
+
+ p += strspn(p, WHITESPACE);
+
+ r = set_put_strdup(&filesystems, p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add filesystem to list: %m");
+ }
+
+ *ret = TAKE_PTR(filesystems);
+ return 0;
+}
+
+static void filesystem_set_remove(Set *s, const FilesystemSet *set) {
+ const char *filesystem;
+
+ NULSTR_FOREACH(filesystem, set->value) {
+ if (filesystem[0] == '@')
+ continue;
+
+ free(set_remove(s, filesystem));
+ }
+}
+
+static void dump_filesystem_set(const FilesystemSet *set) {
+ const char *filesystem;
+ int r;
+
+ if (!set)
+ return;
+
+ printf("%s%s%s\n"
+ " # %s\n",
+ ansi_highlight(),
+ set->name,
+ ansi_normal(),
+ set->help);
+
+ NULSTR_FOREACH(filesystem, set->value) {
+ const statfs_f_type_t *magic;
+
+ if (filesystem[0] == '@') {
+ printf(" %s%s%s\n", ansi_underline(), filesystem, ansi_normal());
+ continue;
+ }
+
+ r = fs_type_from_string(filesystem, &magic);
+ assert_se(r >= 0);
+
+ printf(" %s", filesystem);
+
+ for (size_t i = 0; magic[i] != 0; i++) {
+ const char *primary;
+ if (i == 0)
+ printf(" %s(magic: ", ansi_grey());
+ else
+ printf(", ");
+
+ printf("0x%llx", (unsigned long long) magic[i]);
+
+ primary = fs_type_to_string(magic[i]);
+ if (primary && !streq(primary, filesystem))
+ printf("[%s]", primary);
+
+ if (magic[i+1] == 0)
+ printf(")%s", ansi_normal());
+ }
+
+ printf("\n");
+ }
+}
+
+int verb_filesystems(int argc, char *argv[], void *userdata) {
+ bool first = true;
+
+#if ! HAVE_LIBBPF
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Not compiled with libbpf support, sorry.");
+#endif
+
+ pager_open(arg_pager_flags);
+
+ if (strv_isempty(strv_skip(argv, 1))) {
+ _cleanup_set_free_ Set *kernel = NULL, *known = NULL;
+ const char *fs;
+ int k;
+
+ NULSTR_FOREACH(fs, filesystem_sets[FILESYSTEM_SET_KNOWN].value)
+ if (set_put_strdup(&known, fs) < 0)
+ return log_oom();
+
+ k = load_available_kernel_filesystems(&kernel);
+
+ for (FilesystemGroups i = 0; i < _FILESYSTEM_SET_MAX; i++) {
+ const FilesystemSet *set = filesystem_sets + i;
+ if (!first)
+ puts("");
+
+ dump_filesystem_set(set);
+ filesystem_set_remove(kernel, set);
+ if (i != FILESYSTEM_SET_KNOWN)
+ filesystem_set_remove(known, set);
+ first = false;
+ }
+
+ if (arg_quiet) /* Let's not show the extra stuff in quiet mode */
+ return 0;
+
+ if (!set_isempty(known)) {
+ _cleanup_free_ char **l = NULL;
+
+ printf("\n"
+ "# %sUngrouped filesystems%s (known but not included in any of the groups except @known):\n",
+ ansi_highlight(), ansi_normal());
+
+ l = set_get_strv(known);
+ if (!l)
+ return log_oom();
+
+ strv_sort(l);
+
+ STRV_FOREACH(filesystem, l) {
+ const statfs_f_type_t *magic;
+ bool is_primary = false;
+
+ assert_se(fs_type_from_string(*filesystem, &magic) >= 0);
+
+ for (size_t i = 0; magic[i] != 0; i++) {
+ const char *primary;
+
+ primary = fs_type_to_string(magic[i]);
+ assert(primary);
+
+ if (streq(primary, *filesystem))
+ is_primary = true;
+ }
+
+ if (!is_primary) {
+ log_debug("Skipping ungrouped file system '%s', because it's an alias for another one.", *filesystem);
+ continue;
+ }
+
+ printf("# %s\n", *filesystem);
+ }
+ }
+
+ if (k < 0) {
+ fputc('\n', stdout);
+ fflush(stdout);
+ log_notice_errno(k, "# Not showing unlisted filesystems, couldn't retrieve kernel filesystem list: %m");
+ } else if (!set_isempty(kernel)) {
+ _cleanup_free_ char **l = NULL;
+
+ printf("\n"
+ "# %sUnlisted filesystems%s (available to the local kernel, but not included in any of the groups listed above):\n",
+ ansi_highlight(), ansi_normal());
+
+ l = set_get_strv(kernel);
+ if (!l)
+ return log_oom();
+
+ strv_sort(l);
+
+ STRV_FOREACH(filesystem, l)
+ printf("# %s\n", *filesystem);
+ }
+ } else
+ STRV_FOREACH(name, strv_skip(argv, 1)) {
+ const FilesystemSet *set;
+
+ if (!first)
+ puts("");
+
+ set = filesystem_set_find(*name);
+ if (!set) {
+ /* make sure the error appears below normal output */
+ fflush(stdout);
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "Filesystem set \"%s\" not found.", *name);
+ }
+
+ dump_filesystem_set(set);
+ first = false;
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-filesystems.h b/src/analyze/analyze-filesystems.h
new file mode 100644
index 0000000..0904571
--- /dev/null
+++ b/src/analyze/analyze-filesystems.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_filesystems(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-inspect-elf.c b/src/analyze/analyze-inspect-elf.c
new file mode 100644
index 0000000..155c611
--- /dev/null
+++ b/src/analyze/analyze-inspect-elf.c
@@ -0,0 +1,134 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-inspect-elf.h"
+#include "elf-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "json.h"
+#include "path-util.h"
+#include "strv.h"
+
+static int analyze_elf(char **filenames, JsonFormatFlags json_flags) {
+ int r;
+
+ STRV_FOREACH(filename, filenames) {
+ _cleanup_(json_variant_unrefp) JsonVariant *package_metadata = NULL;
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_free_ char *abspath = NULL;
+ _cleanup_close_ int fd = -1;
+
+ r = path_make_absolute_cwd(*filename, &abspath);
+ if (r < 0)
+ return log_error_errno(r, "Could not make an absolute path out of \"%s\": %m", *filename);
+
+ path_simplify(abspath);
+
+ fd = RET_NERRNO(open(abspath, O_RDONLY|O_CLOEXEC));
+ if (fd < 0)
+ return log_error_errno(fd, "Could not open \"%s\": %m", abspath);
+
+ r = parse_elf_object(fd, abspath, /* fork_disable_dump= */false, NULL, &package_metadata);
+ if (r < 0)
+ return log_error_errno(r, "Parsing \"%s\" as ELF object failed: %m", abspath);
+
+ t = table_new("", "");
+ if (!t)
+ return log_oom();
+
+ r = table_set_align_percent(t, TABLE_HEADER_CELL(0), 100);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(
+ t,
+ TABLE_STRING, "path:",
+ TABLE_STRING, abspath);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (package_metadata) {
+ JsonVariant *module_json;
+ const char *module_name;
+
+ JSON_VARIANT_OBJECT_FOREACH(module_name, module_json, package_metadata) {
+ const char *field_name;
+ JsonVariant *field;
+
+ /* The ELF type and architecture are added as top-level objects,
+ * since they are only parsed for the file itself, but the packaging
+ * metadata is parsed recursively in core files, so there might be
+ * multiple modules. */
+ if (STR_IN_SET(module_name, "elfType", "elfArchitecture")) {
+ _cleanup_free_ char *suffixed = NULL;
+
+ suffixed = strjoin(module_name, ":");
+ if (!suffixed)
+ return log_oom();
+
+ r = table_add_many(
+ t,
+ TABLE_STRING, suffixed,
+ TABLE_STRING, json_variant_string(module_json));
+ if (r < 0)
+ return table_log_add_error(r);
+
+ continue;
+ }
+
+ /* path/elfType/elfArchitecture come first just once per file,
+ * then we might have multiple modules, so add a separator between
+ * them to make the output more readable. */
+ r = table_add_many(t, TABLE_EMPTY, TABLE_EMPTY);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ /* In case of core files the module name will be the executable,
+ * but for binaries/libraries it's just the path, so don't print it
+ * twice. */
+ if (!streq(abspath, module_name)) {
+ r = table_add_many(
+ t,
+ TABLE_STRING, "module name:",
+ TABLE_STRING, module_name);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ JSON_VARIANT_OBJECT_FOREACH(field_name, field, module_json)
+ if (json_variant_is_string(field)) {
+ _cleanup_free_ char *suffixed = NULL;
+
+ suffixed = strjoin(field_name, ":");
+ if (!suffixed)
+ return log_oom();
+
+ r = table_add_many(
+ t,
+ TABLE_STRING, suffixed,
+ TABLE_STRING, json_variant_string(field));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+ }
+ if (json_flags & JSON_FORMAT_OFF) {
+ (void) table_set_header(t, true);
+
+ r = table_print(t, NULL);
+ if (r < 0)
+ return table_log_print_error(r);
+ } else
+ json_variant_dump(package_metadata, json_flags, stdout, NULL);
+ }
+
+ return 0;
+}
+
+int verb_elf_inspection(int argc, char *argv[], void *userdata) {
+ pager_open(arg_pager_flags);
+
+ return analyze_elf(strv_skip(argv, 1), arg_json_format_flags);
+}
diff --git a/src/analyze/analyze-inspect-elf.h b/src/analyze/analyze-inspect-elf.h
new file mode 100644
index 0000000..a790eae
--- /dev/null
+++ b/src/analyze/analyze-inspect-elf.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_elf_inspection(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-log-control.c b/src/analyze/analyze-log-control.c
new file mode 100644
index 0000000..cead0e8
--- /dev/null
+++ b/src/analyze/analyze-log-control.c
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-log-control.h"
+#include "verb-log-control.h"
+
+int verb_log_control(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ assert(IN_SET(argc, 1, 2));
+
+ r = acquire_bus(&bus, NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ r = verb_log_control_common(bus, "org.freedesktop.systemd1", argv[0], argc == 2 ? argv[1] : NULL);
+ if (r < 0)
+ return r;
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-log-control.h b/src/analyze/analyze-log-control.h
new file mode 100644
index 0000000..350c228
--- /dev/null
+++ b/src/analyze/analyze-log-control.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_log_control(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-plot.c b/src/analyze/analyze-plot.c
new file mode 100644
index 0000000..100bdc3
--- /dev/null
+++ b/src/analyze/analyze-plot.c
@@ -0,0 +1,395 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-plot.h"
+#include "analyze-time-data.h"
+#include "bus-error.h"
+#include "bus-map-properties.h"
+#include "sort-util.h"
+#include "version.h"
+
+#define SCALE_X (0.1 / 1000.0) /* pixels per us */
+#define SCALE_Y (20.0)
+
+#define svg(...) printf(__VA_ARGS__)
+
+#define svg_bar(class, x1, x2, y) \
+ svg(" <rect class=\"%s\" x=\"%.03f\" y=\"%.03f\" width=\"%.03f\" height=\"%.03f\" />\n", \
+ (class), \
+ SCALE_X * (x1), SCALE_Y * (y), \
+ SCALE_X * ((x2) - (x1)), SCALE_Y - 1.0)
+
+#define svg_text(b, x, y, format, ...) \
+ do { \
+ svg(" <text class=\"%s\" x=\"%.03f\" y=\"%.03f\">", (b) ? "left" : "right", SCALE_X * (x) + (b ? 5.0 : -5.0), SCALE_Y * (y) + 14.0); \
+ svg(format, ## __VA_ARGS__); \
+ svg("</text>\n"); \
+ } while (false)
+
+
+typedef struct HostInfo {
+ char *hostname;
+ char *kernel_name;
+ char *kernel_release;
+ char *kernel_version;
+ char *os_pretty_name;
+ char *virtualization;
+ char *architecture;
+} HostInfo;
+
+static HostInfo* free_host_info(HostInfo *hi) {
+ if (!hi)
+ return NULL;
+
+ free(hi->hostname);
+ free(hi->kernel_name);
+ free(hi->kernel_release);
+ free(hi->kernel_version);
+ free(hi->os_pretty_name);
+ free(hi->virtualization);
+ free(hi->architecture);
+ return mfree(hi);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(HostInfo *, free_host_info);
+
+static int acquire_host_info(sd_bus *bus, HostInfo **hi) {
+ static const struct bus_properties_map hostname_map[] = {
+ { "Hostname", "s", NULL, offsetof(HostInfo, hostname) },
+ { "KernelName", "s", NULL, offsetof(HostInfo, kernel_name) },
+ { "KernelRelease", "s", NULL, offsetof(HostInfo, kernel_release) },
+ { "KernelVersion", "s", NULL, offsetof(HostInfo, kernel_version) },
+ { "OperatingSystemPrettyName", "s", NULL, offsetof(HostInfo, os_pretty_name) },
+ {}
+ };
+
+ static const struct bus_properties_map manager_map[] = {
+ { "Virtualization", "s", NULL, offsetof(HostInfo, virtualization) },
+ { "Architecture", "s", NULL, offsetof(HostInfo, architecture) },
+ {}
+ };
+
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *system_bus = NULL;
+ _cleanup_(free_host_infop) HostInfo *host = NULL;
+ int r;
+
+ host = new0(HostInfo, 1);
+ if (!host)
+ return log_oom();
+
+ if (arg_scope != LOOKUP_SCOPE_SYSTEM) {
+ r = bus_connect_transport(arg_transport, arg_host, false, &system_bus);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to connect to system bus, ignoring: %m");
+ goto manager;
+ }
+ }
+
+ r = bus_map_all_properties(
+ system_bus ?: bus,
+ "org.freedesktop.hostname1",
+ "/org/freedesktop/hostname1",
+ hostname_map,
+ BUS_MAP_STRDUP,
+ &error,
+ NULL,
+ host);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to get host information from systemd-hostnamed, ignoring: %s",
+ bus_error_message(&error, r));
+ sd_bus_error_free(&error);
+ }
+
+manager:
+ r = bus_map_all_properties(
+ bus,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ manager_map,
+ BUS_MAP_STRDUP,
+ &error,
+ NULL,
+ host);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get host information from systemd: %s",
+ bus_error_message(&error, r));
+
+ *hi = TAKE_PTR(host);
+ return 0;
+}
+
+static int compare_unit_start(const UnitTimes *a, const UnitTimes *b) {
+ return CMP(a->activating, b->activating);
+}
+
+static void svg_graph_box(double height, double begin, double end) {
+ /* outside box, fill */
+ svg("<rect class=\"box\" x=\"0\" y=\"0\" width=\"%.03f\" height=\"%.03f\" />\n",
+ SCALE_X * (end - begin),
+ SCALE_Y * height);
+
+ for (long long i = ((long long) (begin / 100000)) * 100000; i <= end; i += 100000) {
+ /* lines for each second */
+ if (i % 5000000 == 0)
+ svg(" <line class=\"sec5\" x1=\"%.03f\" y1=\"0\" x2=\"%.03f\" y2=\"%.03f\" />\n"
+ " <text class=\"sec\" x=\"%.03f\" y=\"%.03f\" >%.01fs</text>\n",
+ SCALE_X * i,
+ SCALE_X * i,
+ SCALE_Y * height,
+ SCALE_X * i,
+ -5.0,
+ 0.000001 * i);
+ else if (i % 1000000 == 0)
+ svg(" <line class=\"sec1\" x1=\"%.03f\" y1=\"0\" x2=\"%.03f\" y2=\"%.03f\" />\n"
+ " <text class=\"sec\" x=\"%.03f\" y=\"%.03f\" >%.01fs</text>\n",
+ SCALE_X * i,
+ SCALE_X * i,
+ SCALE_Y * height,
+ SCALE_X * i,
+ -5.0,
+ 0.000001 * i);
+ else
+ svg(" <line class=\"sec01\" x1=\"%.03f\" y1=\"0\" x2=\"%.03f\" y2=\"%.03f\" />\n",
+ SCALE_X * i,
+ SCALE_X * i,
+ SCALE_Y * height);
+ }
+}
+
+static int plot_unit_times(UnitTimes *u, double width, int y) {
+ bool b;
+
+ if (!u->name)
+ return 0;
+
+ svg_bar("activating", u->activating, u->activated, y);
+ svg_bar("active", u->activated, u->deactivating, y);
+ svg_bar("deactivating", u->deactivating, u->deactivated, y);
+
+ /* place the text on the left if we have passed the half of the svg width */
+ b = u->activating * SCALE_X < width / 2;
+ if (u->time)
+ svg_text(b, u->activating, y, "%s (%s)",
+ u->name, FORMAT_TIMESPAN(u->time, USEC_PER_MSEC));
+ else
+ svg_text(b, u->activating, y, "%s", u->name);
+
+ return 1;
+}
+
+int verb_plot(int argc, char *argv[], void *userdata) {
+ _cleanup_(free_host_infop) HostInfo *host = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(unit_times_free_arrayp) UnitTimes *times = NULL;
+ _cleanup_free_ char *pretty_times = NULL;
+ bool use_full_bus = arg_scope == LOOKUP_SCOPE_SYSTEM;
+ BootTimes *boot;
+ UnitTimes *u;
+ int n, m = 1, y = 0, r;
+ double width;
+
+ r = acquire_bus(&bus, &use_full_bus);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ n = acquire_boot_times(bus, &boot);
+ if (n < 0)
+ return n;
+
+ n = pretty_boot_time(bus, &pretty_times);
+ if (n < 0)
+ return n;
+
+ if (use_full_bus || arg_scope != LOOKUP_SCOPE_SYSTEM) {
+ n = acquire_host_info(bus, &host);
+ if (n < 0)
+ return n;
+ }
+
+ n = acquire_time_data(bus, &times);
+ if (n <= 0)
+ return n;
+
+ typesafe_qsort(times, n, compare_unit_start);
+
+ width = SCALE_X * (boot->firmware_time + boot->finish_time);
+ if (width < 800.0)
+ width = 800.0;
+
+ if (boot->firmware_time > boot->loader_time)
+ m++;
+ if (boot->loader_time > 0) {
+ m++;
+ if (width < 1000.0)
+ width = 1000.0;
+ }
+ if (boot->initrd_time > 0)
+ m++;
+ if (boot->kernel_done_time > 0)
+ m++;
+
+ for (u = times; u->has_data; u++) {
+ double text_start, text_width;
+
+ if (u->activating > boot->finish_time) {
+ u->name = mfree(u->name);
+ continue;
+ }
+
+ /* If the text cannot fit on the left side then
+ * increase the svg width so it fits on the right.
+ * TODO: calculate the text width more accurately */
+ text_width = 8.0 * strlen(u->name);
+ text_start = (boot->firmware_time + u->activating) * SCALE_X;
+ if (text_width > text_start && text_width + text_start > width)
+ width = text_width + text_start;
+
+ if (u->deactivated > u->activating &&
+ u->deactivated <= boot->finish_time &&
+ u->activated == 0 && u->deactivating == 0)
+ u->activated = u->deactivating = u->deactivated;
+ if (u->activated < u->activating || u->activated > boot->finish_time)
+ u->activated = boot->finish_time;
+ if (u->deactivating < u->activated || u->deactivating > boot->finish_time)
+ u->deactivating = boot->finish_time;
+ if (u->deactivated < u->deactivating || u->deactivated > boot->finish_time)
+ u->deactivated = boot->finish_time;
+ m++;
+ }
+
+ svg("<?xml version=\"1.0\" standalone=\"no\"?>\n"
+ "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
+ "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n");
+
+ svg("<svg width=\"%.0fpx\" height=\"%.0fpx\" version=\"1.1\" "
+ "xmlns=\"http://www.w3.org/2000/svg\">\n\n",
+ 80.0 + width, 150.0 + (m * SCALE_Y) +
+ 5 * SCALE_Y /* legend */);
+
+ /* write some basic info as a comment, including some help */
+ svg("<!-- This file is a systemd-analyze SVG file. It is best rendered in a -->\n"
+ "<!-- browser such as Chrome, Chromium or Firefox. Other applications -->\n"
+ "<!-- that render these files properly but much slower are ImageMagick, -->\n"
+ "<!-- gimp, inkscape, etc. To display the files on your system, just -->\n"
+ "<!-- point your browser to this file. -->\n\n"
+ "<!-- This plot was generated by systemd-analyze version %-16.16s -->\n\n", GIT_VERSION);
+
+ /* style sheet */
+ svg("<defs>\n <style type=\"text/css\">\n <![CDATA[\n"
+ " rect { stroke-width: 1; stroke-opacity: 0; }\n"
+ " rect.background { fill: rgb(255,255,255); }\n"
+ " rect.activating { fill: rgb(255,0,0); fill-opacity: 0.7; }\n"
+ " rect.active { fill: rgb(200,150,150); fill-opacity: 0.7; }\n"
+ " rect.deactivating { fill: rgb(150,100,100); fill-opacity: 0.7; }\n"
+ " rect.kernel { fill: rgb(150,150,150); fill-opacity: 0.7; }\n"
+ " rect.initrd { fill: rgb(150,150,150); fill-opacity: 0.7; }\n"
+ " rect.firmware { fill: rgb(150,150,150); fill-opacity: 0.7; }\n"
+ " rect.loader { fill: rgb(150,150,150); fill-opacity: 0.7; }\n"
+ " rect.userspace { fill: rgb(150,150,150); fill-opacity: 0.7; }\n"
+ " rect.security { fill: rgb(144,238,144); fill-opacity: 0.7; }\n"
+ " rect.generators { fill: rgb(102,204,255); fill-opacity: 0.7; }\n"
+ " rect.unitsload { fill: rgb( 82,184,255); fill-opacity: 0.7; }\n"
+ " rect.box { fill: rgb(240,240,240); stroke: rgb(192,192,192); }\n"
+ " line { stroke: rgb(64,64,64); stroke-width: 1; }\n"
+ "// line.sec1 { }\n"
+ " line.sec5 { stroke-width: 2; }\n"
+ " line.sec01 { stroke: rgb(224,224,224); stroke-width: 1; }\n"
+ " text { font-family: Verdana, Helvetica; font-size: 14px; }\n"
+ " text.left { font-family: Verdana, Helvetica; font-size: 14px; text-anchor: start; }\n"
+ " text.right { font-family: Verdana, Helvetica; font-size: 14px; text-anchor: end; }\n"
+ " text.sec { font-size: 10px; }\n"
+ " ]]>\n </style>\n</defs>\n\n");
+
+ svg("<rect class=\"background\" width=\"100%%\" height=\"100%%\" />\n");
+ svg("<text x=\"20\" y=\"50\">%s</text>", pretty_times);
+ if (host)
+ svg("<text x=\"20\" y=\"30\">%s %s (%s %s %s) %s %s</text>",
+ isempty(host->os_pretty_name) ? "Linux" : host->os_pretty_name,
+ strempty(host->hostname),
+ strempty(host->kernel_name),
+ strempty(host->kernel_release),
+ strempty(host->kernel_version),
+ strempty(host->architecture),
+ strempty(host->virtualization));
+
+ svg("<g transform=\"translate(%.3f,100)\">\n", 20.0 + (SCALE_X * boot->firmware_time));
+ svg_graph_box(m, -(double) boot->firmware_time, boot->finish_time);
+
+ if (boot->firmware_time > 0) {
+ svg_bar("firmware", -(double) boot->firmware_time, -(double) boot->loader_time, y);
+ svg_text(true, -(double) boot->firmware_time, y, "firmware");
+ y++;
+ }
+ if (boot->loader_time > 0) {
+ svg_bar("loader", -(double) boot->loader_time, 0, y);
+ svg_text(true, -(double) boot->loader_time, y, "loader");
+ y++;
+ }
+ if (boot->kernel_done_time > 0) {
+ svg_bar("kernel", 0, boot->kernel_done_time, y);
+ svg_text(true, 0, y, "kernel");
+ y++;
+ }
+ if (boot->initrd_time > 0) {
+ svg_bar("initrd", boot->initrd_time, boot->userspace_time, y);
+ if (boot->initrd_security_start_time < boot->initrd_security_finish_time)
+ svg_bar("security", boot->initrd_security_start_time, boot->initrd_security_finish_time, y);
+ if (boot->initrd_generators_start_time < boot->initrd_generators_finish_time)
+ svg_bar("generators", boot->initrd_generators_start_time, boot->initrd_generators_finish_time, y);
+ if (boot->initrd_unitsload_start_time < boot->initrd_unitsload_finish_time)
+ svg_bar("unitsload", boot->initrd_unitsload_start_time, boot->initrd_unitsload_finish_time, y);
+ svg_text(true, boot->initrd_time, y, "initrd");
+ y++;
+ }
+
+ for (u = times; u->has_data; u++) {
+ if (u->activating >= boot->userspace_time)
+ break;
+
+ y += plot_unit_times(u, width, y);
+ }
+
+ svg_bar("active", boot->userspace_time, boot->finish_time, y);
+ if (boot->security_start_time > 0)
+ svg_bar("security", boot->security_start_time, boot->security_finish_time, y);
+ svg_bar("generators", boot->generators_start_time, boot->generators_finish_time, y);
+ svg_bar("unitsload", boot->unitsload_start_time, boot->unitsload_finish_time, y);
+ svg_text(true, boot->userspace_time, y, "systemd");
+ y++;
+
+ for (; u->has_data; u++)
+ y += plot_unit_times(u, width, y);
+
+ svg("</g>\n");
+
+ /* Legend */
+ svg("<g transform=\"translate(20,100)\">\n");
+ y++;
+ svg_bar("activating", 0, 300000, y);
+ svg_text(true, 400000, y, "Activating");
+ y++;
+ svg_bar("active", 0, 300000, y);
+ svg_text(true, 400000, y, "Active");
+ y++;
+ svg_bar("deactivating", 0, 300000, y);
+ svg_text(true, 400000, y, "Deactivating");
+ y++;
+ if (boot->security_start_time > 0) {
+ svg_bar("security", 0, 300000, y);
+ svg_text(true, 400000, y, "Setting up security module");
+ y++;
+ }
+ svg_bar("generators", 0, 300000, y);
+ svg_text(true, 400000, y, "Generators");
+ y++;
+ svg_bar("unitsload", 0, 300000, y);
+ svg_text(true, 400000, y, "Loading unit files");
+ y++;
+
+ svg("</g>\n\n");
+
+ svg("</svg>\n");
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-plot.h b/src/analyze/analyze-plot.h
new file mode 100644
index 0000000..eb2e398
--- /dev/null
+++ b/src/analyze/analyze-plot.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_plot(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c
new file mode 100644
index 0000000..0451710
--- /dev/null
+++ b/src/analyze/analyze-security.c
@@ -0,0 +1,2962 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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-locator.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-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"
+#include "pretty-print.h"
+#if HAVE_SECCOMP
+# include "seccomp-util.h"
+#endif
+#include "service.h"
+#include "set.h"
+#include "stdio-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "unit-def.h"
+#include "unit-name.h"
+#include "unit-serialize.h"
+
+typedef struct SecurityInfo {
+ 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;
+
+ bool ip_filters_custom_ingress;
+ bool ip_filters_custom_egress;
+
+ char *keyring_mode;
+ char *protect_proc;
+ char *proc_subset;
+ bool lock_personality;
+ bool memory_deny_write_execute;
+ bool no_new_privileges;
+ char *notify_access;
+ bool protect_hostname;
+
+ 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;
+ bool protect_kernel_logs;
+ bool protect_clock;
+
+ 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;
+
+ unsigned long long restrict_namespaces;
+ bool restrict_realtime;
+ bool restrict_suid_sgid;
+
+ char *root_directory;
+ char *root_image;
+
+ bool delegate;
+ char *device_policy;
+ char **device_allow;
+
+ Set *system_call_architectures;
+
+ bool system_call_filter_allow_list;
+ Set *system_call_filter;
+
+ mode_t _umask;
+} SecurityInfo;
+
+struct security_assessor {
+ const char *id;
+ const char *json_field;
+ 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 SecurityInfo *info,
+ const void *data,
+ uint64_t *ret_badness,
+ char **ret_description);
+ size_t offset;
+ uint64_t parameter;
+ bool default_dependencies_only;
+};
+
+static SecurityInfo *security_info_new(void) {
+ SecurityInfo *info = new(SecurityInfo, 1);
+ if (!info)
+ return NULL;
+
+ *info = (SecurityInfo) {
+ .default_dependencies = true,
+ .capability_bounding_set = UINT64_MAX,
+ .restrict_namespaces = UINT64_MAX,
+ ._umask = 0002,
+ };
+
+ return info;
+}
+
+static SecurityInfo *security_info_free(SecurityInfo *i) {
+ if (!i)
+ return NULL;
+
+ 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->protect_proc);
+ free(i->proc_subset);
+ free(i->notify_access);
+
+ free(i->device_policy);
+ strv_free(i->device_allow);
+
+ strv_free(i->supplementary_groups);
+ set_free(i->system_call_architectures);
+ set_free(i->system_call_filter);
+
+ return mfree(i);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(SecurityInfo*, security_info_free);
+
+static bool security_info_runs_privileged(const SecurityInfo *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 SecurityInfo *info,
+ const void *data,
+ uint64_t *ret_badness,
+ char **ret_description) {
+
+ const bool *b = ASSERT_PTR(data);
+
+ 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 SecurityInfo *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 SecurityInfo *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 SecurityInfo *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 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 the 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 SecurityInfo *info,
+ const void *data,
+ uint64_t *ret_badness,
+ char **ret_description) {
+
+ assert(ret_badness);
+ assert(ret_description);
+
+ *ret_badness =
+ empty_or_root(info->root_directory) &&
+ empty_or_root(info->root_image);
+ *ret_description = NULL;
+
+ return 0;
+}
+
+static int assess_capability_bounding_set(
+ const struct security_assessor *a,
+ const SecurityInfo *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 SecurityInfo *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 SecurityInfo *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_protect_proc(
+ const struct security_assessor *a,
+ const SecurityInfo *info,
+ const void *data,
+ uint64_t *ret_badness,
+ char **ret_description) {
+
+ assert(ret_badness);
+ assert(ret_description);
+
+ if (streq_ptr(info->protect_proc, "noaccess"))
+ *ret_badness = 1;
+ else if (STRPTR_IN_SET(info->protect_proc, "invisible", "ptraceable"))
+ *ret_badness = 0;
+ else
+ *ret_badness = 3;
+
+ *ret_description = NULL;
+
+ return 0;
+}
+
+static int assess_proc_subset(
+ const struct security_assessor *a,
+ const SecurityInfo *info,
+ const void *data,
+ uint64_t *ret_badness,
+ char **ret_description) {
+
+ assert(ret_badness);
+ assert(ret_description);
+
+ *ret_badness = !streq_ptr(info->proc_subset, "pid");
+ *ret_description = NULL;
+
+ return 0;
+}
+
+static int assess_notify_access(
+ const struct security_assessor *a,
+ const SecurityInfo *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 SecurityInfo *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 SecurityInfo *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 SecurityInfo *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;
+}
+
+#if HAVE_SECCOMP
+
+static int assess_system_call_architectures(
+ const struct security_assessor *a,
+ const SecurityInfo *info,
+ const void *data,
+ uint64_t *ret_badness,
+ char **ret_description) {
+
+ char *d;
+ uint64_t b;
+
+ assert(ret_badness);
+ assert(ret_description);
+
+ if (set_isempty(info->system_call_architectures)) {
+ b = 10;
+ d = strdup("Service may execute system calls with all ABIs");
+ } else if (set_contains(info->system_call_architectures, "native") &&
+ set_size(info->system_call_architectures) == 1) {
+ 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 allow_list, const SyscallFilterSet *f, const char **ret_offending_syscall) {
+ const char *syscall;
+
+ NULSTR_FOREACH(syscall, f->value) {
+ if (syscall[0] == '@') {
+ const SyscallFilterSet *g;
+
+ assert_se(g = syscall_filter_set_find(syscall));
+ if (syscall_names_in_filter(s, allow_list, g, ret_offending_syscall))
+ return true; /* bad! */
+
+ continue;
+ }
+
+ /* Let's see if the system call actually exists on this platform, before complaining */
+ if (seccomp_syscall_resolve_name(syscall) < 0)
+ continue;
+
+ if (set_contains(s, syscall) == allow_list) {
+ log_debug("Offending syscall filter item: %s", syscall);
+ if (ret_offending_syscall)
+ *ret_offending_syscall = syscall;
+ return true; /* bad! */
+ }
+ }
+
+ *ret_offending_syscall = NULL;
+ return false;
+}
+
+static int assess_system_call_filter(
+ const struct security_assessor *a,
+ const SecurityInfo *info,
+ const void *data,
+ uint64_t *ret_badness,
+ char **ret_description) {
+
+ assert(a);
+ assert(info);
+ assert(ret_badness);
+ assert(ret_description);
+
+ assert(a->parameter < _SYSCALL_FILTER_SET_MAX);
+ const SyscallFilterSet *f = syscall_filter_sets + a->parameter;
+
+ _cleanup_free_ char *d = NULL;
+ uint64_t b;
+ int r;
+
+ if (!info->system_call_filter_allow_list && set_isempty(info->system_call_filter)) {
+ r = free_and_strdup(&d, "Service does not filter system calls");
+ b = 10;
+ } else {
+ bool bad;
+ const char *offender = NULL;
+
+ log_debug("Analyzing system call filter, checking against: %s", f->name);
+ bad = syscall_names_in_filter(info->system_call_filter, info->system_call_filter_allow_list, f, &offender);
+ log_debug("Result: %s", bad ? "bad" : "good");
+
+ if (info->system_call_filter_allow_list) {
+ if (bad) {
+ r = asprintf(&d, "System call allow list defined for service, and %s is included "
+ "(e.g. %s is allowed)",
+ f->name, offender);
+ b = 9;
+ } else {
+ r = asprintf(&d, "System call allow list defined for service, and %s is not included",
+ f->name);
+ b = 0;
+ }
+ } else {
+ if (bad) {
+ r = asprintf(&d, "System call deny list defined for service, and %s is not included "
+ "(e.g. %s is allowed)",
+ f->name, offender);
+ b = 10;
+ } else {
+ r = asprintf(&d, "System call deny list defined for service, and %s is included",
+ f->name);
+ b = 0;
+ }
+ }
+ }
+ if (r < 0)
+ return log_oom();
+
+ *ret_badness = b;
+ *ret_description = TAKE_PTR(d);
+
+ return 0;
+}
+
+#endif
+
+static int assess_ip_address_allow(
+ const struct security_assessor *a,
+ const SecurityInfo *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_filters_custom_ingress || info->ip_filters_custom_egress) {
+ d = strdup("Service defines custom ingress/egress IP filters with BPF programs");
+ b = 0;
+ } else if (!info->ip_address_deny_all) {
+ d = strdup("Service does not define an IP address allow list");
+ b = 10;
+ } else if (info->ip_address_allow_other) {
+ d = strdup("Service defines IP address allow list with non-localhost entries");
+ b = 5;
+ } else if (info->ip_address_allow_localhost) {
+ d = strdup("Service defines IP address allow list 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 SecurityInfo *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 (!strv_isempty(info->device_allow)) {
+ _cleanup_free_ char *join = NULL;
+
+ join = strv_join(info->device_allow, " ");
+ if (!join)
+ return log_oom();
+
+ d = strjoin("Service has a device ACL with some special devices: ", join);
+ 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 SecurityInfo *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=",
+ .json_field = "UserOrDynamicUser",
+ .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=",
+ .json_field = "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=",
+ .json_field = "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(SecurityInfo, private_devices),
+ },
+ {
+ .id = "PrivateMounts=",
+ .json_field = "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(SecurityInfo, private_mounts),
+ },
+ {
+ .id = "PrivateNetwork=",
+ .json_field = "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(SecurityInfo, private_network),
+ },
+ {
+ .id = "PrivateTmp=",
+ .json_field = "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(SecurityInfo, private_tmp),
+ .default_dependencies_only = true,
+ },
+ {
+ .id = "PrivateUsers=",
+ .json_field = "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(SecurityInfo, private_users),
+ },
+ {
+ .id = "ProtectControlGroups=",
+ .json_field = "ProtectControlGroups",
+ .description_good = "Service cannot modify the control group file system",
+ .description_bad = "Service may modify 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(SecurityInfo, protect_control_groups),
+ },
+ {
+ .id = "ProtectKernelModules=",
+ .json_field = "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(SecurityInfo, protect_kernel_modules),
+ },
+ {
+ .id = "ProtectKernelTunables=",
+ .json_field = "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(SecurityInfo, protect_kernel_tunables),
+ },
+ {
+ .id = "ProtectKernelLogs=",
+ .json_field = "ProtectKernelLogs",
+ .description_good = "Service cannot read from or write to the kernel log ring buffer",
+ .description_bad = "Service may read from or write to the kernel log ring buffer",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelLogs=",
+ .weight = 1000,
+ .range = 1,
+ .assess = assess_bool,
+ .offset = offsetof(SecurityInfo, protect_kernel_logs),
+ },
+ {
+ .id = "ProtectClock=",
+ .json_field = "ProtectClock",
+ .description_good = "Service cannot write to the hardware clock or system clock",
+ .description_bad = "Service may write to the hardware clock or system clock",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectClock=",
+ .weight = 1000,
+ .range = 1,
+ .assess = assess_bool,
+ .offset = offsetof(SecurityInfo, protect_clock),
+ },
+ {
+ .id = "ProtectHome=",
+ .json_field = "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 = "ProtectHostname=",
+ .json_field = "ProtectHostname",
+ .description_good = "Service cannot change system host/domainname",
+ .description_bad = "Service may change system host/domainname",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHostname=",
+ .weight = 50,
+ .range = 1,
+ .assess = assess_bool,
+ .offset = offsetof(SecurityInfo, protect_hostname),
+ },
+ {
+ .id = "ProtectSystem=",
+ .json_field = "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=",
+ .json_field = "RootDirectoryOrRootImage",
+ .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=",
+ .json_field = "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(SecurityInfo, lock_personality),
+ },
+ {
+ .id = "MemoryDenyWriteExecute=",
+ .json_field = "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(SecurityInfo, memory_deny_write_execute),
+ },
+ {
+ .id = "NoNewPrivileges=",
+ .json_field = "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(SecurityInfo, no_new_privileges),
+ },
+ {
+ .id = "CapabilityBoundingSet=~CAP_SYS_ADMIN",
+ .json_field = "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)",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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_SYS_RAWIO",
+ .json_field = "CapabilityBoundingSet_CAP_SYS_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",
+ .json_field = "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_*",
+ .json_field = "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",
+ .json_field = "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)",
+ .json_field = "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",
+ .json_field = "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)",
+ .json_field = "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)",
+ .json_field = "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",
+ .json_field = "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)",
+ .json_field = "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",
+ .json_field = "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_*",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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 = "CapabilityBoundingSet=~CAP_BPF",
+ .json_field = "CapabilityBoundingSet_CAP_BPF",
+ .description_good = "Service may load BPF programs",
+ .description_bad = "Service may not load BPF programs",
+ .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_BPF),
+ },
+ {
+ .id = "UMask=",
+ .json_field = "UMask",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#UMask=",
+ .weight = 100,
+ .range = 10,
+ .assess = assess_umask,
+ },
+ {
+ .id = "KeyringMode=",
+ .json_field = "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 = "ProtectProc=",
+ .json_field = "ProtectProc",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectProc=",
+ .description_good = "Service has restricted access to process tree (/proc hidepid=)",
+ .description_bad = "Service has full access to process tree (/proc hidepid=)",
+ .weight = 1000,
+ .range = 3,
+ .assess = assess_protect_proc,
+ },
+ {
+ .id = "ProcSubset=",
+ .json_field = "ProcSubset",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProcSubset=",
+ .description_good = "Service has no access to non-process /proc files (/proc subset=)",
+ .description_bad = "Service has full access to non-process /proc files (/proc subset=)",
+ .weight = 10,
+ .range = 1,
+ .assess = assess_proc_subset,
+ },
+ {
+ .id = "NotifyAccess=",
+ .json_field = "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=",
+ .json_field = "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(SecurityInfo, remove_ipc),
+ },
+ {
+ .id = "Delegate=",
+ .json_field = "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(SecurityInfo, delegate),
+ .parameter = true, /* invert! */
+ },
+ {
+ .id = "RestrictRealtime=",
+ .json_field = "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(SecurityInfo, restrict_realtime),
+ },
+ {
+ .id = "RestrictSUIDSGID=",
+ .json_field = "RestrictSUIDSGID",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictSUIDSGID=",
+ .description_good = "SUID/SGID file creation by service is restricted",
+ .description_bad = "Service may create SUID/SGID files",
+ .weight = 1000,
+ .range = 1,
+ .assess = assess_bool,
+ .offset = offsetof(SecurityInfo, restrict_suid_sgid),
+ },
+ {
+ .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",
+ .weight = 1500,
+ .range = 1,
+ .assess = assess_restrict_namespaces,
+ .parameter = CLONE_NEWUSER,
+ },
+ {
+ .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",
+ .weight = 500,
+ .range = 1,
+ .assess = assess_restrict_namespaces,
+ .parameter = CLONE_NEWNS,
+ },
+ {
+ .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",
+ .weight = 500,
+ .range = 1,
+ .assess = assess_restrict_namespaces,
+ .parameter = CLONE_NEWIPC,
+ },
+ {
+ .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",
+ .weight = 500,
+ .range = 1,
+ .assess = assess_restrict_namespaces,
+ .parameter = CLONE_NEWPID,
+ },
+ {
+ .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",
+ .weight = 500,
+ .range = 1,
+ .assess = assess_restrict_namespaces,
+ .parameter = CLONE_NEWCGROUP,
+ },
+ {
+ .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",
+ .weight = 500,
+ .range = 1,
+ .assess = assess_restrict_namespaces,
+ .parameter = CLONE_NEWNET,
+ },
+ {
+ .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",
+ .weight = 100,
+ .range = 1,
+ .assess = assess_restrict_namespaces,
+ .parameter = CLONE_NEWUTS,
+ },
+ {
+ .id = "RestrictAddressFamilies=~AF_(INET|INET6)",
+ .json_field = "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(SecurityInfo, restrict_address_family_inet),
+ },
+ {
+ .id = "RestrictAddressFamilies=~AF_UNIX",
+ .json_field = "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(SecurityInfo, restrict_address_family_unix),
+ },
+ {
+ .id = "RestrictAddressFamilies=~AF_NETLINK",
+ .json_field = "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(SecurityInfo, restrict_address_family_netlink),
+ },
+ {
+ .id = "RestrictAddressFamilies=~AF_PACKET",
+ .json_field = "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(SecurityInfo, restrict_address_family_packet),
+ },
+ {
+ .id = "RestrictAddressFamilies=~…",
+ .json_field = "RestrictAddressFamilies_OTHER",
+ .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(SecurityInfo, restrict_address_family_other),
+ },
+#if HAVE_SECCOMP
+ {
+ .id = "SystemCallArchitectures=",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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",
+ .json_field = "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,
+ },
+#endif
+ {
+ .id = "IPAddressDeny=",
+ .json_field = "IPAddressDeny",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#IPAddressDeny=",
+ .weight = 1000,
+ .range = 10,
+ .assess = assess_ip_address_allow,
+ },
+ {
+ .id = "DeviceAllow=",
+ .json_field = "DeviceAllow",
+ .url = "https://www.freedesktop.org/software/systemd/man/systemd.exec.html#DeviceAllow=",
+ .weight = 1000,
+ .range = 10,
+ .assess = assess_device_allow,
+ },
+ {
+ .id = "AmbientCapabilities=",
+ .json_field = "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 JsonVariant* security_assessor_find_in_policy(const struct security_assessor *a, JsonVariant *policy, const char *name) {
+ JsonVariant *item;
+ assert(a);
+
+ if (!policy)
+ return NULL;
+ if (!json_variant_is_object(policy)) {
+ log_debug("Specified policy is not a JSON object, ignoring.");
+ return NULL;
+ }
+
+ item = json_variant_by_key(policy, a->json_field);
+ if (!item)
+ return NULL;
+ if (!json_variant_is_object(item)) {
+ log_debug("Item for '%s' in policy JSON object is not an object, ignoring.", a->id);
+ return NULL;
+ }
+
+ return name ? json_variant_by_key(item, name) : item;
+}
+
+static uint64_t access_weight(const struct security_assessor *a, JsonVariant *policy) {
+ JsonVariant *val;
+
+ assert(a);
+
+ val = security_assessor_find_in_policy(a, policy, "weight");
+ if (val) {
+ if (json_variant_is_unsigned(val))
+ return json_variant_unsigned(val);
+ log_debug("JSON field 'weight' of policy for %s is not an unsigned integer, ignoring.", a->id);
+ }
+
+ return a->weight;
+}
+
+static uint64_t access_range(const struct security_assessor *a, JsonVariant *policy) {
+ JsonVariant *val;
+
+ assert(a);
+
+ val = security_assessor_find_in_policy(a, policy, "range");
+ if (val) {
+ if (json_variant_is_unsigned(val))
+ return json_variant_unsigned(val);
+ log_debug("JSON field 'range' of policy for %s is not an unsigned integer, ignoring.", a->id);
+ }
+
+ return a->range;
+}
+
+static const char *access_description_na(const struct security_assessor *a, JsonVariant *policy) {
+ JsonVariant *val;
+
+ assert(a);
+
+ val = security_assessor_find_in_policy(a, policy, "description_na");
+ if (val) {
+ if (json_variant_is_string(val))
+ return json_variant_string(val);
+ log_debug("JSON field 'description_na' of policy for %s is not a string, ignoring.", a->id);
+ }
+
+ return a->description_na;
+}
+
+static const char *access_description_good(const struct security_assessor *a, JsonVariant *policy) {
+ JsonVariant *val;
+
+ assert(a);
+
+ val = security_assessor_find_in_policy(a, policy, "description_good");
+ if (val) {
+ if (json_variant_is_string(val))
+ return json_variant_string(val);
+ log_debug("JSON field 'description_good' of policy for %s is not a string, ignoring.", a->id);
+ }
+
+ return a->description_good;
+}
+
+static const char *access_description_bad(const struct security_assessor *a, JsonVariant *policy) {
+ JsonVariant *val;
+
+ assert(a);
+
+ val = security_assessor_find_in_policy(a, policy, "description_bad");
+ if (val) {
+ if (json_variant_is_string(val))
+ return json_variant_string(val);
+ log_debug("JSON field 'description_bad' of policy for %s is not a string, ignoring.", a->id);
+ }
+
+ return a->description_bad;
+}
+
+static int assess(const SecurityInfo *info,
+ Table *overview_table,
+ AnalyzeSecurityFlags flags,
+ unsigned threshold,
+ JsonVariant *policy,
+ PagerFlags pager_flags,
+ JsonFormatFlags json_format_flags) {
+
+ static const struct {
+ uint64_t exposure;
+ const char *name;
+ const char *color;
+ SpecialGlyph smiley;
+ } badness_table[] = {
+ { 100, "DANGEROUS", ANSI_HIGHLIGHT_RED, SPECIAL_GLYPH_DEPRESSED_SMILEY },
+ { 90, "UNSAFE", ANSI_HIGHLIGHT_RED, SPECIAL_GLYPH_UNHAPPY_SMILEY },
+ { 75, "EXPOSED", ANSI_HIGHLIGHT_YELLOW, SPECIAL_GLYPH_SLIGHTLY_UNHAPPY_SMILEY },
+ { 50, "MEDIUM", NULL, SPECIAL_GLYPH_NEUTRAL_SMILEY },
+ { 10, "OK", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_SLIGHTLY_HAPPY_SMILEY },
+ { 1, "SAFE", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_HAPPY_SMILEY },
+ { 0, "PERFECT", ANSI_HIGHLIGHT_GREEN, SPECIAL_GLYPH_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", "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) 3, (size_t) 7);
+ }
+
+ 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;
+ uint64_t weight = access_weight(a, policy);
+ uint64_t range = access_range(a, policy);
+
+ 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 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)
+ return r;
+ }
+
+ assert(range > 0);
+
+ if (badness != UINT64_MAX) {
+ assert(badness <= range);
+
+ badness_sum += DIV_ROUND_UP(badness * weight, range);
+ weight_sum += weight;
+ }
+
+ if (details_table) {
+ const char *description, *color = NULL;
+ int checkmark;
+
+ if (badness == UINT64_MAX) {
+ checkmark = -1;
+ description = access_description_na(a, policy);
+ color = NULL;
+ } else if (badness == a->range) {
+ checkmark = 0;
+ description = access_description_bad(a, policy);
+ color = ansi_highlight_red();
+ } else if (badness == 0) {
+ checkmark = 1;
+ description = access_description_good(a, policy);
+ color = ansi_highlight_green();
+ } else {
+ checkmark = 0;
+ description = NULL;
+ color = ansi_highlight_red();
+ }
+
+ if (d)
+ description = d;
+
+ 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, 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,
+ TABLE_UINT64, range, TABLE_SET_ALIGN_PERCENT, 100,
+ TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+
+ assert(weight_sum > 0);
+
+ 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, 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, 7));
+
+ 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");
+ }
+
+ 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");
+ }
+
+ 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 && (json_format_flags & JSON_FORMAT_OFF)) {
+ _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(SPECIAL_GLYPH_ARROW_RIGHT),
+ 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];
+ _cleanup_free_ char *url = NULL;
+
+ if (info->fragment_path) {
+ r = file_url_from_path(info->fragment_path, &url);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate URL from path: %m");
+ }
+
+ xsprintf(buf, "%" PRIu64 ".%" PRIu64, exposure / 10, exposure % 10);
+
+ r = table_add_many(overview_table,
+ TABLE_STRING, info->id,
+ TABLE_SET_URL, url,
+ TABLE_STRING, buf,
+ TABLE_SET_ALIGN_PERCENT, 100,
+ TABLE_STRING, badness_table[i].name,
+ TABLE_SET_COLOR, strempty(badness_table[i].color),
+ TABLE_STRING, special_glyph(badness_table[i].smiley));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ /* Return error when overall exposure level is over threshold */
+ if (exposure > threshold)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int property_read_restrict_namespaces(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ SecurityInfo *info = ASSERT_PTR(userdata);
+ int r;
+ uint64_t namespaces;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+
+ r = sd_bus_message_read(m, "t", &namespaces);
+ if (r < 0)
+ return r;
+
+ info->restrict_namespaces = (unsigned long long) namespaces;
+
+ return 0;
+}
+
+static int property_read_umask(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ SecurityInfo *info = ASSERT_PTR(userdata);
+ int r;
+ uint32_t umask;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+
+ r = sd_bus_message_read(m, "u", &umask);
+ if (r < 0)
+ return r;
+
+ info->_umask = (mode_t) umask;
+
+ 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) {
+
+ SecurityInfo *info = userdata;
+ int allow_list, 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", &allow_list);
+ 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 = allow_list;
+
+ 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 = !allow_list;
+ else if (streq(name, "AF_UNIX"))
+ info->restrict_address_family_unix = !allow_list;
+ else if (streq(name, "AF_NETLINK"))
+ info->restrict_address_family_netlink = !allow_list;
+ else if (streq(name, "AF_PACKET"))
+ info->restrict_address_family_packet = !allow_list;
+ else
+ info->restrict_address_family_other = !allow_list;
+ }
+
+ r = sd_bus_message_exit_container(m);
+ if (r < 0)
+ return r;
+
+ return sd_bus_message_exit_container(m);
+}
+
+static int property_read_syscall_archs(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ SecurityInfo *info = ASSERT_PTR(userdata);
+ int r;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+
+ 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_put_strdup(&info->system_call_architectures, name);
+ 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) {
+
+ SecurityInfo *info = userdata;
+ int allow_list, 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", &allow_list);
+ if (r < 0)
+ return r;
+
+ info->system_call_filter_allow_list = allow_list;
+
+ 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;
+
+ /* ignore errno or action after colon */
+ r = set_put_strndup(&info->system_call_filter, name, strchrnul(name, ':') - 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) {
+
+ SecurityInfo *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_ip_filters(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ SecurityInfo *info = userdata;
+ _cleanup_(strv_freep) char **l = NULL;
+ int r;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+
+ r = sd_bus_message_read_strv(m, &l);
+ if (r < 0)
+ return r;
+
+ if (streq(member, "IPIngressFilterPath"))
+ info->ip_filters_custom_ingress = !strv_isempty(l);
+ else if (streq(member, "IPEgressFilterPath"))
+ info->ip_filters_custom_egress = !strv_isempty(l);
+
+ return 0;
+}
+
+static int property_read_device_allow(
+ sd_bus *bus,
+ const char *member,
+ sd_bus_message *m,
+ sd_bus_error *error,
+ void *userdata) {
+
+ SecurityInfo *info = userdata;
+ 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;
+
+ r = strv_extendf(&info->device_allow, "%s:%s", name, policy);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_message_exit_container(m);
+}
+
+static int acquire_security_info(sd_bus *bus, const char *name, SecurityInfo *info, AnalyzeSecurityFlags flags) {
+
+ static const struct bus_properties_map security_map[] = {
+ { "AmbientCapabilities", "t", NULL, offsetof(SecurityInfo, ambient_capabilities) },
+ { "CapabilityBoundingSet", "t", NULL, offsetof(SecurityInfo, capability_bounding_set) },
+ { "DefaultDependencies", "b", NULL, offsetof(SecurityInfo, default_dependencies) },
+ { "Delegate", "b", NULL, offsetof(SecurityInfo, delegate) },
+ { "DeviceAllow", "a(ss)", property_read_device_allow, 0 },
+ { "DevicePolicy", "s", NULL, offsetof(SecurityInfo, device_policy) },
+ { "DynamicUser", "b", NULL, offsetof(SecurityInfo, dynamic_user) },
+ { "FragmentPath", "s", NULL, offsetof(SecurityInfo, fragment_path) },
+ { "IPAddressAllow", "a(iayu)", property_read_ip_address_allow, 0 },
+ { "IPAddressDeny", "a(iayu)", property_read_ip_address_allow, 0 },
+ { "IPIngressFilterPath", "as", property_read_ip_filters, 0 },
+ { "IPEgressFilterPath", "as", property_read_ip_filters, 0 },
+ { "Id", "s", NULL, offsetof(SecurityInfo, id) },
+ { "KeyringMode", "s", NULL, offsetof(SecurityInfo, keyring_mode) },
+ { "ProtectProc", "s", NULL, offsetof(SecurityInfo, protect_proc) },
+ { "ProcSubset", "s", NULL, offsetof(SecurityInfo, proc_subset) },
+ { "LoadState", "s", NULL, offsetof(SecurityInfo, load_state) },
+ { "LockPersonality", "b", NULL, offsetof(SecurityInfo, lock_personality) },
+ { "MemoryDenyWriteExecute", "b", NULL, offsetof(SecurityInfo, memory_deny_write_execute) },
+ { "NoNewPrivileges", "b", NULL, offsetof(SecurityInfo, no_new_privileges) },
+ { "NotifyAccess", "s", NULL, offsetof(SecurityInfo, notify_access) },
+ { "PrivateDevices", "b", NULL, offsetof(SecurityInfo, private_devices) },
+ { "PrivateMounts", "b", NULL, offsetof(SecurityInfo, private_mounts) },
+ { "PrivateNetwork", "b", NULL, offsetof(SecurityInfo, private_network) },
+ { "PrivateTmp", "b", NULL, offsetof(SecurityInfo, private_tmp) },
+ { "PrivateUsers", "b", NULL, offsetof(SecurityInfo, private_users) },
+ { "ProtectControlGroups", "b", NULL, offsetof(SecurityInfo, protect_control_groups) },
+ { "ProtectHome", "s", NULL, offsetof(SecurityInfo, protect_home) },
+ { "ProtectHostname", "b", NULL, offsetof(SecurityInfo, protect_hostname) },
+ { "ProtectKernelModules", "b", NULL, offsetof(SecurityInfo, protect_kernel_modules) },
+ { "ProtectKernelTunables", "b", NULL, offsetof(SecurityInfo, protect_kernel_tunables) },
+ { "ProtectKernelLogs", "b", NULL, offsetof(SecurityInfo, protect_kernel_logs) },
+ { "ProtectClock", "b", NULL, offsetof(SecurityInfo, protect_clock) },
+ { "ProtectSystem", "s", NULL, offsetof(SecurityInfo, protect_system) },
+ { "RemoveIPC", "b", NULL, offsetof(SecurityInfo, remove_ipc) },
+ { "RestrictAddressFamilies", "(bas)", property_read_restrict_address_families, 0 },
+ { "RestrictNamespaces", "t", property_read_restrict_namespaces, 0 },
+ { "RestrictRealtime", "b", NULL, offsetof(SecurityInfo, restrict_realtime) },
+ { "RestrictSUIDSGID", "b", NULL, offsetof(SecurityInfo, restrict_suid_sgid) },
+ { "RootDirectory", "s", NULL, offsetof(SecurityInfo, root_directory) },
+ { "RootImage", "s", NULL, offsetof(SecurityInfo, root_image) },
+ { "SupplementaryGroups", "as", NULL, offsetof(SecurityInfo, supplementary_groups) },
+ { "SystemCallArchitectures", "as", property_read_syscall_archs, 0 },
+ { "SystemCallFilter", "(as)", property_read_system_call_filter, 0 },
+ { "Type", "s", NULL, offsetof(SecurityInfo, type) },
+ { "UMask", "u", property_read_umask, 0 },
+ { "User", "s", NULL, offsetof(SecurityInfo, 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->protect_kernel_logs)
+ info->capability_bounding_set &= ~(UINT64_C(1) << CAP_SYSLOG);
+
+ if (info->protect_clock)
+ info->capability_bounding_set &= ~((UINT64_C(1) << CAP_SYS_TIME) |
+ (UINT64_C(1) << CAP_WAKE_ALARM));
+
+ 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,
+ unsigned threshold,
+ JsonVariant *policy,
+ PagerFlags pager_flags,
+ JsonFormatFlags json_format_flags) {
+
+ _cleanup_(security_info_freep) SecurityInfo *info = security_info_new();
+ if (!info)
+ return log_oom();
+
+ 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, threshold, policy, pager_flags, json_format_flags);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+/* Refactoring SecurityInfo so that it can make use of existing struct variables instead of reading from dbus */
+static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, SecurityInfo **ret_info) {
+ assert(ret_info);
+
+ _cleanup_(security_info_freep) SecurityInfo *info = security_info_new();
+ if (!info)
+ return log_oom();
+
+ if (u) {
+ if (u->id) {
+ info->id = strdup(u->id);
+ if (!info->id)
+ return log_oom();
+ }
+ if (unit_type_to_string(u->type)) {
+ info->type = strdup(unit_type_to_string(u->type));
+ if (!info->type)
+ return log_oom();
+ }
+ if (unit_load_state_to_string(u->load_state)) {
+ info->load_state = strdup(unit_load_state_to_string(u->load_state));
+ if (!info->load_state)
+ return log_oom();
+ }
+ if (u->fragment_path) {
+ info->fragment_path = strdup(u->fragment_path);
+ if (!info->fragment_path)
+ return log_oom();
+ }
+ info->default_dependencies = u->default_dependencies;
+ if (u->type == UNIT_SERVICE && notify_access_to_string(SERVICE(u)->notify_access)) {
+ info->notify_access = strdup(notify_access_to_string(SERVICE(u)->notify_access));
+ if (!info->notify_access)
+ return log_oom();
+ }
+ }
+
+ if (c) {
+ info->ambient_capabilities = c->capability_ambient_set;
+ info->capability_bounding_set = c->capability_bounding_set;
+ if (c->user) {
+ info->user = strdup(c->user);
+ if (!info->user)
+ return log_oom();
+ }
+ if (c->supplementary_groups) {
+ info->supplementary_groups = strv_copy(c->supplementary_groups);
+ if (!info->supplementary_groups)
+ return log_oom();
+ }
+ info->dynamic_user = c->dynamic_user;
+ if (exec_keyring_mode_to_string(c->keyring_mode)) {
+ info->keyring_mode = strdup(exec_keyring_mode_to_string(c->keyring_mode));
+ if (!info->keyring_mode)
+ return log_oom();
+ }
+ if (protect_proc_to_string(c->protect_proc)) {
+ info->protect_proc = strdup(protect_proc_to_string(c->protect_proc));
+ if (!info->protect_proc)
+ return log_oom();
+ }
+ if (proc_subset_to_string(c->proc_subset)) {
+ info->proc_subset = strdup(proc_subset_to_string(c->proc_subset));
+ if (!info->proc_subset)
+ return log_oom();
+ }
+ info->lock_personality = c->lock_personality;
+ info->memory_deny_write_execute = c->memory_deny_write_execute;
+ info->no_new_privileges = c->no_new_privileges;
+ info->protect_hostname = c->protect_hostname;
+ info->private_devices = c->private_devices;
+ info->private_mounts = c->private_mounts;
+ info->private_network = c->private_network;
+ info->private_tmp = c->private_tmp;
+ info->private_users = c->private_users;
+ info->protect_control_groups = c->protect_control_groups;
+ info->protect_kernel_modules = c->protect_kernel_modules;
+ info->protect_kernel_tunables = c->protect_kernel_tunables;
+ info->protect_kernel_logs = c->protect_kernel_logs;
+ info->protect_clock = c->protect_clock;
+ if (protect_home_to_string(c->protect_home)) {
+ info->protect_home = strdup(protect_home_to_string(c->protect_home));
+ if (!info->protect_home)
+ return log_oom();
+ }
+ if (protect_system_to_string(c->protect_system)) {
+ info->protect_system = strdup(protect_system_to_string(c->protect_system));
+ if (!info->protect_system)
+ return log_oom();
+ }
+ info->remove_ipc = c->remove_ipc;
+ 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 =
+ c->address_families_allow_list;
+
+ void *key;
+ SET_FOREACH(key, c->address_families) {
+ int family = PTR_TO_INT(key);
+ if (family == 0)
+ continue;
+ if (IN_SET(family, AF_INET, AF_INET6))
+ info->restrict_address_family_inet = !c->address_families_allow_list;
+ else if (family == AF_UNIX)
+ info->restrict_address_family_unix = !c->address_families_allow_list;
+ else if (family == AF_NETLINK)
+ info->restrict_address_family_netlink = !c->address_families_allow_list;
+ else if (family == AF_PACKET)
+ info->restrict_address_family_packet = !c->address_families_allow_list;
+ else
+ info->restrict_address_family_other = !c->address_families_allow_list;
+ }
+
+ info->restrict_namespaces = c->restrict_namespaces;
+ info->restrict_realtime = c->restrict_realtime;
+ info->restrict_suid_sgid = c->restrict_suid_sgid;
+ if (c->root_directory) {
+ info->root_directory = strdup(c->root_directory);
+ if (!info->root_directory)
+ return log_oom();
+ }
+ if (c->root_image) {
+ info->root_image = strdup(c->root_image);
+ if (!info->root_image)
+ return log_oom();
+ }
+ info->_umask = c->umask;
+
+#if HAVE_SECCOMP
+ SET_FOREACH(key, c->syscall_archs) {
+ const char *name;
+
+ name = seccomp_arch_to_string(PTR_TO_UINT32(key) - 1);
+ if (!name)
+ continue;
+
+ if (set_put_strdup(&info->system_call_architectures, name) < 0)
+ return log_oom();
+ }
+
+ info->system_call_filter_allow_list = c->syscall_allow_list;
+
+ void *id, *num;
+ HASHMAP_FOREACH_KEY(num, id, c->syscall_filter) {
+ _cleanup_free_ char *name = NULL;
+
+ if (info->system_call_filter_allow_list && PTR_TO_INT(num) >= 0)
+ continue;
+
+ name = seccomp_syscall_resolve_num_arch(SCMP_ARCH_NATIVE, PTR_TO_INT(id) - 1);
+ if (!name)
+ continue;
+
+ if (set_ensure_consume(&info->system_call_filter, &string_hash_ops_free, TAKE_PTR(name)) < 0)
+ return log_oom();
+ }
+#endif
+ }
+
+ if (g) {
+ info->delegate = g->delegate;
+ if (cgroup_device_policy_to_string(g->device_policy)) {
+ info->device_policy = strdup(cgroup_device_policy_to_string(g->device_policy));
+ if (!info->device_policy)
+ return log_oom();
+ }
+
+ struct in_addr_prefix *i;
+ bool deny_ipv4 = false, deny_ipv6 = false;
+
+ 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)
+ deny_ipv6 = true;
+ }
+ info->ip_address_deny_all = deny_ipv4 && deny_ipv6;
+
+ info->ip_address_allow_localhost = info->ip_address_allow_other = false;
+ SET_FOREACH(i, g->ip_address_allow) {
+ if (in_addr_is_localhost(i->family, &i->address))
+ info->ip_address_allow_localhost = true;
+ else
+ info->ip_address_allow_other = true;
+ }
+
+ info->ip_filters_custom_ingress = !strv_isempty(g->ip_filters_ingress);
+ info->ip_filters_custom_egress = !strv_isempty(g->ip_filters_egress);
+
+ LIST_FOREACH(device_allow, a, g->device_allow)
+ if (strv_extendf(&info->device_allow,
+ "%s:%s%s%s",
+ a->path,
+ a->r ? "r" : "", a->w ? "w" : "", a->m ? "m" : "") < 0)
+ return log_oom();
+ }
+
+ *ret_info = TAKE_PTR(info);
+
+ return 0;
+}
+
+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;
+ int r;
+
+ assert(u);
+
+ if (DEBUG_LOGGING)
+ unit_dump(u, stdout, "\t");
+
+ r = get_security_info(u, unit_get_exec_context(u), unit_get_cgroup_context(u), &info);
+ if (r < 0)
+ return r;
+
+ return assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
+}
+
+static int offline_security_checks(char **filenames,
+ JsonVariant *policy,
+ LookupScope scope,
+ bool check_man,
+ bool run_generators,
+ unsigned threshold,
+ 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;
+ Unit *units[strv_length(filenames)];
+ int r, k;
+ size_t count = 0;
+
+ if (strv_isempty(filenames))
+ return 0;
+
+ r = verify_set_unit_path(filenames);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set unit load path: %m");
+
+ r = manager_new(scope, flags, &m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize manager: %m");
+
+ log_debug("Starting manager...");
+
+ r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root);
+ 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) {
+ _cleanup_free_ char *prepared = NULL;
+
+ log_debug("Handling %s...", *filename);
+
+ k = verify_prepare_filename(*filename, &prepared);
+ if (k < 0) {
+ log_warning_errno(k, "Failed to prepare filename %s: %m", *filename);
+ if (r == 0)
+ r = k;
+ 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)
+ r = k;
+ continue;
+ }
+
+ count++;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ k = offline_security_check(units[i], threshold, policy, pager_flags, json_format_flags);
+ if (k < 0 && r == 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int analyze_security(sd_bus *bus,
+ char **units,
+ JsonVariant *policy,
+ LookupScope scope,
+ bool check_man,
+ bool run_generators,
+ 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 != offline);
+
+ if (offline)
+ 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");
+ 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 n = 0;
+
+ r = bus_call_method(
+ bus,
+ bus_systemd_mgr,
+ "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, 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, threshold, policy, pager_flags, json_format_flags);
+ if (r < 0 && ret >= 0)
+ ret = r;
+ }
+
+ } else
+ 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(*i, 0, &mangled);
+ if (r < 0)
+ return log_error_errno(r, "Failed to mangle unit name '%s': %m", *i);
+
+ if (!endswith(mangled, ".service"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unit %s is not a service unit, refusing.",
+ *i);
+
+ 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, threshold, policy, pager_flags, json_format_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_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);
+}
diff --git a/src/analyze/analyze-security.h b/src/analyze/analyze-security.h
new file mode 100644
index 0000000..82f4c7f
--- /dev/null
+++ b/src/analyze/analyze-security.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef enum AnalyzeSecurityFlags {
+ ANALYZE_SECURITY_SHORT = 1 << 0,
+ ANALYZE_SECURITY_ONLY_LOADED = 1 << 1,
+ ANALYZE_SECURITY_ONLY_LONG_RUNNING = 1 << 2,
+} AnalyzeSecurityFlags;
+
+int verb_security(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-service-watchdogs.c b/src/analyze/analyze-service-watchdogs.c
new file mode 100644
index 0000000..6535eb1
--- /dev/null
+++ b/src/analyze/analyze-service-watchdogs.c
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-service-watchdogs.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "parse-util.h"
+
+int verb_service_watchdogs(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int b, r;
+
+ assert(IN_SET(argc, 1, 2));
+ assert(argv);
+
+ r = acquire_bus(&bus, NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ if (argc == 1) {
+ /* get ServiceWatchdogs */
+ r = bus_get_property_trivial(bus, bus_systemd_mgr, "ServiceWatchdogs", &error, 'b', &b);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get service-watchdog state: %s", bus_error_message(&error, r));
+
+ printf("%s\n", yes_no(!!b));
+
+ } else {
+ /* set ServiceWatchdogs */
+ b = parse_boolean(argv[1]);
+ if (b < 0)
+ return log_error_errno(b, "Failed to parse service-watchdogs argument: %m");
+
+ r = bus_set_property(bus, bus_systemd_mgr, "ServiceWatchdogs", &error, "b", b);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set service-watchdog state: %s", bus_error_message(&error, r));
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-service-watchdogs.h b/src/analyze/analyze-service-watchdogs.h
new file mode 100644
index 0000000..2f59f5a
--- /dev/null
+++ b/src/analyze/analyze-service-watchdogs.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_service_watchdogs(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-syscall-filter.c b/src/analyze/analyze-syscall-filter.c
new file mode 100644
index 0000000..f20e4a8
--- /dev/null
+++ b/src/analyze/analyze-syscall-filter.c
@@ -0,0 +1,207 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze-syscall-filter.h"
+#include "analyze.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "nulstr-util.h"
+#include "seccomp-util.h"
+#include "set.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+#if HAVE_SECCOMP
+
+static int load_kernel_syscalls(Set **ret) {
+ _cleanup_set_free_ Set *syscalls = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ /* Let's read the available system calls from the list of available tracing events. Slightly dirty,
+ * but good enough for analysis purposes. */
+
+ f = fopen("/sys/kernel/tracing/available_events", "re");
+ if (!f) {
+ /* We tried the non-debugfs mount point and that didn't work. If it wasn't mounted, maybe the
+ * old debugfs mount point works? */
+ f = fopen("/sys/kernel/debug/tracing/available_events", "re");
+ if (!f)
+ return log_full_errno(IN_SET(errno, EPERM, EACCES, ENOENT) ? LOG_DEBUG : LOG_WARNING, errno,
+ "Can't read open tracefs' available_events file: %m");
+ }
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ const char *e;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read system call list: %m");
+ if (r == 0)
+ break;
+
+ e = startswith(line, "syscalls:sys_enter_");
+ if (!e)
+ continue;
+
+ /* These are named differently inside the kernel than their external name for historical
+ * reasons. Let's hide them here. */
+ if (STR_IN_SET(e, "newuname", "newfstat", "newstat", "newlstat", "sysctl"))
+ continue;
+
+ r = set_put_strdup(&syscalls, e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add system call to list: %m");
+ }
+
+ *ret = TAKE_PTR(syscalls);
+ return 0;
+}
+
+static int syscall_set_add(Set **s, const SyscallFilterSet *set) {
+ const char *sc;
+ int r;
+
+ assert(s);
+
+ if (!set)
+ return 0;
+
+ NULSTR_FOREACH(sc, set->value) {
+ if (sc[0] == '@')
+ continue;
+
+ r = set_put_strdup(s, sc);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static void syscall_set_remove(Set *s, const SyscallFilterSet *set) {
+ const char *sc;
+
+ if (!set)
+ return;
+
+ NULSTR_FOREACH(sc, set->value) {
+ if (sc[0] == '@')
+ continue;
+
+ free(set_remove(s, sc));
+ }
+}
+
+static void dump_syscall_filter(const SyscallFilterSet *set) {
+ const char *syscall;
+
+ printf("%s%s%s\n"
+ " # %s\n",
+ ansi_highlight(),
+ set->name,
+ ansi_normal(),
+ set->help);
+
+ NULSTR_FOREACH(syscall, set->value)
+ printf(" %s%s%s\n", syscall[0] == '@' ? ansi_underline() : "", syscall, ansi_normal());
+}
+
+int verb_syscall_filters(int argc, char *argv[], void *userdata) {
+ bool first = true;
+ int r;
+
+ pager_open(arg_pager_flags);
+
+ if (strv_isempty(strv_skip(argv, 1))) {
+ _cleanup_set_free_ Set *kernel = NULL, *known = NULL;
+ int k = 0; /* explicit initialization to appease gcc */
+
+ r = syscall_set_add(&known, syscall_filter_sets + SYSCALL_FILTER_SET_KNOWN);
+ if (r < 0)
+ return log_error_errno(r, "Failed to prepare set of known system calls: %m");
+
+ if (!arg_quiet)
+ k = load_kernel_syscalls(&kernel);
+
+ for (int i = 0; i < _SYSCALL_FILTER_SET_MAX; i++) {
+ const SyscallFilterSet *set = syscall_filter_sets + i;
+ if (!first)
+ puts("");
+
+ dump_syscall_filter(set);
+ syscall_set_remove(kernel, set);
+ if (i != SYSCALL_FILTER_SET_KNOWN)
+ syscall_set_remove(known, set);
+ first = false;
+ }
+
+ if (arg_quiet) /* Let's not show the extra stuff in quiet mode */
+ return 0;
+
+ if (!set_isempty(known)) {
+ _cleanup_free_ char **l = NULL;
+
+ printf("\n"
+ "# %sUngrouped System Calls%s (known but not included in any of the groups except @known):\n",
+ ansi_highlight(), ansi_normal());
+
+ l = set_get_strv(known);
+ if (!l)
+ return log_oom();
+
+ strv_sort(l);
+
+ STRV_FOREACH(syscall, l)
+ printf("# %s\n", *syscall);
+ }
+
+ if (k < 0) {
+ fputc('\n', stdout);
+ fflush(stdout);
+ if (!arg_quiet)
+ log_notice_errno(k, "# Not showing unlisted system calls, couldn't retrieve kernel system call list: %m");
+ } else if (!set_isempty(kernel)) {
+ _cleanup_free_ char **l = NULL;
+
+ printf("\n"
+ "# %sUnlisted System Calls%s (supported by the local kernel, but not included in any of the groups listed above):\n",
+ ansi_highlight(), ansi_normal());
+
+ l = set_get_strv(kernel);
+ if (!l)
+ return log_oom();
+
+ strv_sort(l);
+
+ STRV_FOREACH(syscall, l)
+ printf("# %s\n", *syscall);
+ }
+ } else
+ STRV_FOREACH(name, strv_skip(argv, 1)) {
+ const SyscallFilterSet *set;
+
+ if (!first)
+ puts("");
+
+ set = syscall_filter_set_find(*name);
+ if (!set) {
+ /* make sure the error appears below normal output */
+ fflush(stdout);
+
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "Filter set \"%s\" not found.", *name);
+ }
+
+ dump_syscall_filter(set);
+ first = false;
+ }
+
+ return EXIT_SUCCESS;
+}
+
+#else
+int verb_syscall_filters(int argc, char *argv[], void *userdata) {
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Not compiled with syscall filters, sorry.");
+}
+#endif
diff --git a/src/analyze/analyze-syscall-filter.h b/src/analyze/analyze-syscall-filter.h
new file mode 100644
index 0000000..3a1af85
--- /dev/null
+++ b/src/analyze/analyze-syscall-filter.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_syscall_filters(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-time-data.c b/src/analyze/analyze-time-data.c
new file mode 100644
index 0000000..aa42d69
--- /dev/null
+++ b/src/analyze/analyze-time-data.c
@@ -0,0 +1,297 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-time-data.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "bus-map-properties.h"
+#include "bus-unit-util.h"
+#include "special.h"
+
+static void subtract_timestamp(usec_t *a, usec_t b) {
+ assert(a);
+
+ if (*a > 0) {
+ assert(*a >= b);
+ *a -= b;
+ }
+}
+
+int acquire_boot_times(sd_bus *bus, BootTimes **ret) {
+ static const struct bus_properties_map property_map[] = {
+ { "FirmwareTimestampMonotonic", "t", NULL, offsetof(BootTimes, firmware_time) },
+ { "LoaderTimestampMonotonic", "t", NULL, offsetof(BootTimes, loader_time) },
+ { "KernelTimestamp", "t", NULL, offsetof(BootTimes, kernel_time) },
+ { "InitRDTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_time) },
+ { "UserspaceTimestampMonotonic", "t", NULL, offsetof(BootTimes, userspace_time) },
+ { "FinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, finish_time) },
+ { "SecurityStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, security_start_time) },
+ { "SecurityFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, security_finish_time) },
+ { "GeneratorsStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, generators_start_time) },
+ { "GeneratorsFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, generators_finish_time) },
+ { "UnitsLoadStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, unitsload_start_time) },
+ { "UnitsLoadFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, unitsload_finish_time) },
+ { "InitRDSecurityStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_security_start_time) },
+ { "InitRDSecurityFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_security_finish_time) },
+ { "InitRDGeneratorsStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_generators_start_time) },
+ { "InitRDGeneratorsFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_generators_finish_time) },
+ { "InitRDUnitsLoadStartTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_unitsload_start_time) },
+ { "InitRDUnitsLoadFinishTimestampMonotonic", "t", NULL, offsetof(BootTimes, initrd_unitsload_finish_time) },
+ {},
+ };
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ static BootTimes times;
+ static bool cached = false;
+ int r;
+
+ if (cached)
+ goto finish;
+
+ assert_cc(sizeof(usec_t) == sizeof(uint64_t));
+
+ r = bus_map_all_properties(
+ bus,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ property_map,
+ BUS_MAP_STRDUP,
+ &error,
+ NULL,
+ &times);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get timestamp properties: %s", bus_error_message(&error, r));
+
+ if (times.finish_time <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINPROGRESS),
+ "Bootup is not yet finished (org.freedesktop.systemd1.Manager.FinishTimestampMonotonic=%"PRIu64").\n"
+ "Please try again later.\n"
+ "Hint: Use 'systemctl%s list-jobs' to see active jobs",
+ times.finish_time,
+ arg_scope == LOOKUP_SCOPE_SYSTEM ? "" : " --user");
+
+ if (arg_scope == LOOKUP_SCOPE_SYSTEM && times.security_start_time > 0) {
+ /* security_start_time is set when systemd is not running under container environment. */
+ if (times.initrd_time > 0)
+ times.kernel_done_time = times.initrd_time;
+ else
+ times.kernel_done_time = times.userspace_time;
+ } else {
+ /*
+ * User-instance-specific or container-system-specific timestamps processing
+ * (see comment to reverse_offset in BootTimes).
+ */
+ times.reverse_offset = times.userspace_time;
+
+ times.firmware_time = times.loader_time = times.kernel_time = times.initrd_time =
+ times.userspace_time = times.security_start_time = times.security_finish_time = 0;
+
+ subtract_timestamp(&times.finish_time, times.reverse_offset);
+
+ subtract_timestamp(&times.generators_start_time, times.reverse_offset);
+ subtract_timestamp(&times.generators_finish_time, times.reverse_offset);
+
+ subtract_timestamp(&times.unitsload_start_time, times.reverse_offset);
+ subtract_timestamp(&times.unitsload_finish_time, times.reverse_offset);
+ }
+
+ cached = true;
+
+finish:
+ *ret = &times;
+ return 0;
+}
+
+static int bus_get_uint64_property(sd_bus *bus, const char *path, const char *interface, const char *property, uint64_t *val) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(property);
+ assert(val);
+
+ r = sd_bus_get_property_trivial(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ interface,
+ property,
+ &error,
+ 't', val);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse reply: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+int pretty_boot_time(sd_bus *bus, char **ret) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *path = NULL, *unit_id = NULL, *text = NULL;
+ usec_t activated_time = USEC_INFINITY;
+ BootTimes *t;
+ int r;
+
+ r = acquire_boot_times(bus, &t);
+ if (r < 0)
+ return r;
+
+ path = unit_dbus_path_from_name(SPECIAL_DEFAULT_TARGET);
+ if (!path)
+ return log_oom();
+
+ r = sd_bus_get_property_string(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Unit",
+ "Id",
+ &error,
+ &unit_id);
+ if (r < 0)
+ log_warning_errno(r, "default.target doesn't seem to exist, ignoring: %s", bus_error_message(&error, r));
+
+ r = bus_get_uint64_property(bus, path,
+ "org.freedesktop.systemd1.Unit",
+ "ActiveEnterTimestampMonotonic",
+ &activated_time);
+ if (r < 0)
+ log_warning_errno(r, "Could not get time to reach default.target, ignoring: %m");
+
+ text = strdup("Startup finished in ");
+ if (!text)
+ return log_oom();
+
+ if (t->firmware_time > 0 && !strextend(&text, FORMAT_TIMESPAN(t->firmware_time - t->loader_time, USEC_PER_MSEC), " (firmware) + "))
+ return log_oom();
+ if (t->loader_time > 0 && !strextend(&text, FORMAT_TIMESPAN(t->loader_time, USEC_PER_MSEC), " (loader) + "))
+ return log_oom();
+ if (t->kernel_done_time > 0 && !strextend(&text, FORMAT_TIMESPAN(t->kernel_done_time, USEC_PER_MSEC), " (kernel) + "))
+ return log_oom();
+ if (t->initrd_time > 0 && !strextend(&text, FORMAT_TIMESPAN(t->userspace_time - t->initrd_time, USEC_PER_MSEC), " (initrd) + "))
+ return log_oom();
+
+ if (!strextend(&text, FORMAT_TIMESPAN(t->finish_time - t->userspace_time, USEC_PER_MSEC), " (userspace) "))
+ return log_oom();
+
+ if (t->kernel_done_time > 0)
+ if (!strextend(&text, "= ", FORMAT_TIMESPAN(t->firmware_time + t->finish_time, USEC_PER_MSEC), " "))
+ return log_oom();
+
+ if (unit_id && timestamp_is_set(activated_time)) {
+ usec_t base = t->userspace_time > 0 ? t->userspace_time : t->reverse_offset;
+
+ if (!strextend(&text, "\n", unit_id, " reached after ", FORMAT_TIMESPAN(activated_time - base, USEC_PER_MSEC), " in userspace."))
+ return log_oom();
+
+ } else if (unit_id && activated_time == 0) {
+
+ if (!strextend(&text, "\n", unit_id, " was never reached."))
+ return log_oom();
+
+ } else if (unit_id && activated_time == USEC_INFINITY) {
+
+ if (!strextend(&text, "\nCould not get time to reach ", unit_id, "."))
+ return log_oom();
+
+ } else if (!unit_id) {
+
+ if (!strextend(&text, "\ncould not find default.target."))
+ return log_oom();
+ }
+
+ *ret = TAKE_PTR(text);
+ return 0;
+}
+
+UnitTimes* unit_times_free_array(UnitTimes *t) {
+ if (!t)
+ return NULL;
+
+ for (UnitTimes *p = t; p->has_data; p++)
+ free(p->name);
+
+ return mfree(t);
+}
+
+int acquire_time_data(sd_bus *bus, UnitTimes **out) {
+ static const struct bus_properties_map property_map[] = {
+ { "InactiveExitTimestampMonotonic", "t", NULL, offsetof(UnitTimes, activating) },
+ { "ActiveEnterTimestampMonotonic", "t", NULL, offsetof(UnitTimes, activated) },
+ { "ActiveExitTimestampMonotonic", "t", NULL, offsetof(UnitTimes, deactivating) },
+ { "InactiveEnterTimestampMonotonic", "t", NULL, offsetof(UnitTimes, deactivated) },
+ {},
+ };
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(unit_times_free_arrayp) UnitTimes *unit_times = NULL;
+ BootTimes *boot_times = NULL;
+ size_t c = 0;
+ UnitInfo u;
+ int r;
+
+ r = acquire_boot_times(bus, &boot_times);
+ if (r < 0)
+ return r;
+
+ r = bus_call_method(bus, bus_systemd_mgr, "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);
+
+ while ((r = bus_parse_unit_info(reply, &u)) > 0) {
+ UnitTimes *t;
+
+ if (!GREEDY_REALLOC(unit_times, c + 2))
+ return log_oom();
+
+ unit_times[c + 1].has_data = false;
+ t = &unit_times[c];
+ t->name = NULL;
+
+ assert_cc(sizeof(usec_t) == sizeof(uint64_t));
+
+ r = bus_map_all_properties(
+ bus,
+ "org.freedesktop.systemd1",
+ u.unit_path,
+ property_map,
+ BUS_MAP_STRDUP,
+ &error,
+ NULL,
+ t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get timestamp properties of unit %s: %s",
+ u.id, bus_error_message(&error, r));
+
+ subtract_timestamp(&t->activating, boot_times->reverse_offset);
+ subtract_timestamp(&t->activated, boot_times->reverse_offset);
+ subtract_timestamp(&t->deactivating, boot_times->reverse_offset);
+ subtract_timestamp(&t->deactivated, boot_times->reverse_offset);
+
+ if (t->activated >= t->activating)
+ t->time = t->activated - t->activating;
+ else if (t->deactivated >= t->activating)
+ t->time = t->deactivated - t->activating;
+ else
+ t->time = 0;
+
+ if (t->activating == 0)
+ continue;
+
+ t->name = strdup(u.id);
+ if (!t->name)
+ return log_oom();
+
+ t->has_data = true;
+ c++;
+ }
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ *out = TAKE_PTR(unit_times);
+ return c;
+}
diff --git a/src/analyze/analyze-time-data.h b/src/analyze/analyze-time-data.h
new file mode 100644
index 0000000..02ee16a
--- /dev/null
+++ b/src/analyze/analyze-time-data.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sd-bus.h>
+
+#include "time-util.h"
+
+typedef struct BootTimes {
+ usec_t firmware_time;
+ usec_t loader_time;
+ usec_t kernel_time;
+ usec_t kernel_done_time;
+ usec_t initrd_time;
+ usec_t userspace_time;
+ usec_t finish_time;
+ usec_t security_start_time;
+ usec_t security_finish_time;
+ usec_t generators_start_time;
+ usec_t generators_finish_time;
+ usec_t unitsload_start_time;
+ usec_t unitsload_finish_time;
+ usec_t initrd_security_start_time;
+ usec_t initrd_security_finish_time;
+ usec_t initrd_generators_start_time;
+ usec_t initrd_generators_finish_time;
+ usec_t initrd_unitsload_start_time;
+ usec_t initrd_unitsload_finish_time;
+
+ /*
+ * If we're analyzing the user instance, all timestamps will be offset by its own start-up timestamp,
+ * which may be arbitrarily big. With "plot", this causes arbitrarily wide output SVG files which
+ * almost completely consist of empty space. Thus we cancel out this offset.
+ *
+ * This offset is subtracted from times above by acquire_boot_times(), but it still needs to be
+ * subtracted from unit-specific timestamps (so it is stored here for reference).
+ */
+ usec_t reverse_offset;
+} BootTimes;
+
+typedef struct UnitTimes {
+ bool has_data;
+ char *name;
+ usec_t activating;
+ usec_t activated;
+ usec_t deactivated;
+ usec_t deactivating;
+ usec_t time;
+} UnitTimes;
+
+int acquire_boot_times(sd_bus *bus, BootTimes **ret);
+int pretty_boot_time(sd_bus *bus, char **ret);
+
+UnitTimes* unit_times_free_array(UnitTimes *t);
+DEFINE_TRIVIAL_CLEANUP_FUNC(UnitTimes*, unit_times_free_array);
+
+int acquire_time_data(sd_bus *bus, UnitTimes **out);
diff --git a/src/analyze/analyze-time.c b/src/analyze/analyze-time.c
new file mode 100644
index 0000000..c233b1f
--- /dev/null
+++ b/src/analyze/analyze-time.c
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-time.h"
+#include "analyze-time-data.h"
+
+int verb_time(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *buf = NULL;
+ int r;
+
+ r = acquire_bus(&bus, NULL);
+ if (r < 0)
+ return bus_log_connect_error(r, arg_transport);
+
+ r = pretty_boot_time(bus, &buf);
+ if (r < 0)
+ return r;
+
+ puts(buf);
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-time.h b/src/analyze/analyze-time.h
new file mode 100644
index 0000000..a8f8575
--- /dev/null
+++ b/src/analyze/analyze-time.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_time(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-timespan.c b/src/analyze/analyze-timespan.c
new file mode 100644
index 0000000..f244ace
--- /dev/null
+++ b/src/analyze/analyze-timespan.c
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-timespan.h"
+#include "calendarspec.h"
+#include "format-table.h"
+#include "glyph-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+int verb_timespan(int argc, char *argv[], void *userdata) {
+ STRV_FOREACH(input_timespan, strv_skip(argv, 1)) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ usec_t output_usecs;
+ TableCell *cell;
+ int r;
+
+ r = parse_time(*input_timespan, &output_usecs, USEC_PER_SEC);
+ if (r < 0) {
+ log_error_errno(r, "Failed to parse time span '%s': %m", *input_timespan);
+ time_parsing_hint(*input_timespan, /* calendar= */ true, /* timestamp= */ true, /* timespan= */ false);
+ return r;
+ }
+
+ table = table_new("name", "value");
+ if (!table)
+ return log_oom();
+
+ table_set_header(table, false);
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ r = table_set_align_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(table,
+ TABLE_STRING, "Original:",
+ TABLE_STRING, *input_timespan);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_cell_stringf(table, NULL, "%ss:", special_glyph(SPECIAL_GLYPH_MU));
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_add_many(table,
+ TABLE_UINT64, output_usecs,
+ TABLE_STRING, "Human:",
+ TABLE_TIMESPAN, output_usecs,
+ TABLE_SET_COLOR, ansi_highlight());
+ if (r < 0)
+ return table_log_add_error(r);
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return r;
+
+ if (input_timespan[1])
+ putchar('\n');
+ }
+
+ return 0;
+}
diff --git a/src/analyze/analyze-timespan.h b/src/analyze/analyze-timespan.h
new file mode 100644
index 0000000..46d2295
--- /dev/null
+++ b/src/analyze/analyze-timespan.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_timespan(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-timestamp.c b/src/analyze/analyze-timestamp.c
new file mode 100644
index 0000000..79764e0
--- /dev/null
+++ b/src/analyze/analyze-timestamp.c
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-timestamp.h"
+#include "format-table.h"
+#include "terminal-util.h"
+
+static int test_timestamp_one(const char *p) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ TableCell *cell;
+ usec_t usec;
+ int r;
+
+ r = parse_timestamp(p, &usec);
+ if (r < 0) {
+ log_error_errno(r, "Failed to parse \"%s\": %m", p);
+ time_parsing_hint(p, /* calendar= */ true, /* timestamp= */ false, /* timespan= */ true);
+ return r;
+ }
+
+ table = table_new("name", "value");
+ if (!table)
+ return log_oom();
+
+ table_set_header(table, false);
+
+ assert_se(cell = table_get_cell(table, 0, 0));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ r = table_set_align_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ assert_se(cell = table_get_cell(table, 0, 1));
+ r = table_set_ellipsize_percent(table, cell, 100);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(table,
+ TABLE_STRING, "Original form:",
+ TABLE_STRING, p,
+ TABLE_STRING, "Normalized form:",
+ TABLE_TIMESTAMP, usec,
+ TABLE_SET_COLOR, ansi_highlight_blue());
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (!in_utc_timezone()) {
+ r = table_add_many(table,
+ TABLE_STRING, "(in UTC):",
+ TABLE_TIMESTAMP_UTC, usec);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ r = table_add_cell(table, NULL, TABLE_STRING, "UNIX seconds:");
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (usec % USEC_PER_SEC == 0)
+ r = table_add_cell_stringf(table, NULL, "@%"PRI_USEC,
+ usec / USEC_PER_SEC);
+ else
+ r = table_add_cell_stringf(table, NULL, "@%"PRI_USEC".%06"PRI_USEC"",
+ usec / USEC_PER_SEC,
+ usec % USEC_PER_SEC);
+ if (r < 0)
+ return r;
+
+ r = table_add_many(table,
+ TABLE_STRING, "From now:",
+ TABLE_TIMESTAMP_RELATIVE, usec);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return table_print(table, NULL);
+}
+
+int verb_timestamp(int argc, char *argv[], void *userdata) {
+ int r = 0;
+
+ STRV_FOREACH(p, strv_skip(argv, 1)) {
+ int k;
+
+ k = test_timestamp_one(*p);
+ if (r == 0 && k < 0)
+ r = k;
+
+ if (p[1])
+ putchar('\n');
+ }
+
+ return r;
+}
diff --git a/src/analyze/analyze-timestamp.h b/src/analyze/analyze-timestamp.h
new file mode 100644
index 0000000..43e4b57
--- /dev/null
+++ b/src/analyze/analyze-timestamp.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_timestamp(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-unit-files.c b/src/analyze/analyze-unit-files.c
new file mode 100644
index 0000000..ec9be33
--- /dev/null
+++ b/src/analyze/analyze-unit-files.c
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-unit-files.h"
+#include "path-lookup.h"
+#include "strv.h"
+
+static bool strv_fnmatch_strv_or_empty(char* const* patterns, char **strv, int flags) {
+ STRV_FOREACH(s, strv)
+ if (strv_fnmatch_or_empty(patterns, *s, flags))
+ return true;
+
+ return false;
+}
+
+int verb_unit_files(int argc, char *argv[], void *userdata) {
+ _cleanup_hashmap_free_ Hashmap *unit_ids = NULL, *unit_names = NULL;
+ _cleanup_(lookup_paths_free) LookupPaths lp = {};
+ char **patterns = strv_skip(argv, 1);
+ const char *k, *dst;
+ char **v;
+ int r;
+
+ r = lookup_paths_init_or_warn(&lp, arg_scope, 0, NULL);
+ if (r < 0)
+ return r;
+
+ r = unit_file_build_name_map(&lp, NULL, &unit_ids, &unit_names, NULL);
+ if (r < 0)
+ return log_error_errno(r, "unit_file_build_name_map() failed: %m");
+
+ HASHMAP_FOREACH_KEY(dst, k, unit_ids) {
+ if (!strv_fnmatch_or_empty(patterns, k, FNM_NOESCAPE) &&
+ !strv_fnmatch_or_empty(patterns, dst, FNM_NOESCAPE))
+ continue;
+
+ printf("ids: %s → %s\n", k, dst);
+ }
+
+ HASHMAP_FOREACH_KEY(v, k, unit_names) {
+ if (!strv_fnmatch_or_empty(patterns, k, FNM_NOESCAPE) &&
+ !strv_fnmatch_strv_or_empty(patterns, v, FNM_NOESCAPE))
+ continue;
+
+ _cleanup_free_ char *j = strv_join(v, ", ");
+ printf("aliases: %s ← %s\n", k, j);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-unit-files.h b/src/analyze/analyze-unit-files.h
new file mode 100644
index 0000000..c193fd8
--- /dev/null
+++ b/src/analyze/analyze-unit-files.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_unit_files(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-unit-paths.c b/src/analyze/analyze-unit-paths.c
new file mode 100644
index 0000000..c225461
--- /dev/null
+++ b/src/analyze/analyze-unit-paths.c
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-unit-paths.h"
+#include "path-lookup.h"
+#include "strv.h"
+
+int verb_unit_paths(int argc, char *argv[], void *userdata) {
+ _cleanup_(lookup_paths_free) LookupPaths paths = {};
+ int r;
+
+ r = lookup_paths_init_or_warn(&paths, arg_scope, 0, NULL);
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(p, paths.search_path)
+ puts(*p);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/analyze/analyze-unit-paths.h b/src/analyze/analyze-unit-paths.h
new file mode 100644
index 0000000..b8d46e8
--- /dev/null
+++ b/src/analyze/analyze-unit-paths.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_unit_paths(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze-verify-util.c b/src/analyze/analyze-verify-util.c
new file mode 100644
index 0000000..483a474
--- /dev/null
+++ b/src/analyze/analyze-verify-util.c
@@ -0,0 +1,390 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdlib.h>
+
+#include "all-units.h"
+#include "alloc-util.h"
+#include "analyze-verify-util.h"
+#include "bus-error.h"
+#include "bus-util.h"
+#include "log.h"
+#include "manager.h"
+#include "pager.h"
+#include "path-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "unit-name.h"
+#include "unit-serialize.h"
+
+static void log_syntax_callback(const char *unit, int level, void *userdata) {
+ Set **s = ASSERT_PTR(userdata);
+ int r;
+
+ assert(unit);
+
+ if (level > LOG_WARNING)
+ return;
+
+ if (*s == POINTER_MAX)
+ return;
+
+ r = set_put_strdup(s, unit);
+ if (r < 0) {
+ set_free_free(*s);
+ *s = POINTER_MAX;
+ }
+}
+
+int verify_prepare_filename(const char *filename, char **ret) {
+ int r;
+ const char *name;
+ _cleanup_free_ char *abspath = NULL;
+ _cleanup_free_ char *dir = NULL;
+ _cleanup_free_ char *with_instance = NULL;
+ char *c;
+
+ assert(filename);
+ assert(ret);
+
+ r = path_make_absolute_cwd(filename, &abspath);
+ if (r < 0)
+ return r;
+
+ name = basename(abspath);
+ if (!unit_name_is_valid(name, UNIT_NAME_ANY))
+ return -EINVAL;
+
+ if (unit_name_is_valid(name, UNIT_NAME_TEMPLATE)) {
+ r = unit_name_replace_instance(name, "i", &with_instance);
+ if (r < 0)
+ return r;
+ }
+
+ r = path_extract_directory(abspath, &dir);
+ if (r < 0)
+ return r;
+
+ c = path_join(dir, with_instance ?: name);
+ if (!c)
+ return -ENOMEM;
+
+ *ret = c;
+ return 0;
+}
+
+static int find_unit_directory(const char *p, char **ret) {
+ _cleanup_free_ char *a = NULL, *u = NULL, *t = NULL, *d = NULL;
+ int r;
+
+ assert(p);
+ assert(ret);
+
+ r = path_make_absolute_cwd(p, &a);
+ if (r < 0)
+ return r;
+
+ if (access(a, F_OK) >= 0) {
+ r = path_extract_directory(a, &d);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(d);
+ return 0;
+ }
+
+ r = path_extract_filename(a, &u);
+ if (r < 0)
+ return r;
+
+ if (!unit_name_is_valid(u, UNIT_NAME_INSTANCE))
+ return -ENOENT;
+
+ /* If the specified unit is an instance of a template unit, then let's try to find the template unit. */
+ r = unit_name_template(u, &t);
+ if (r < 0)
+ return r;
+
+ r = path_extract_directory(a, &d);
+ if (r < 0)
+ return r;
+
+ free(a);
+ a = path_join(d, t);
+ if (!a)
+ return -ENOMEM;
+
+ if (access(a, F_OK) < 0)
+ return -errno;
+
+ *ret = TAKE_PTR(d);
+ return 0;
+}
+
+int verify_set_unit_path(char **filenames) {
+ _cleanup_strv_free_ char **ans = NULL;
+ _cleanup_free_ char *joined = NULL;
+ const char *old;
+ int r;
+
+ STRV_FOREACH(filename, filenames) {
+ _cleanup_free_ char *t = NULL;
+
+ r = find_unit_directory(*filename, &t);
+ if (r == -ENOMEM)
+ return r;
+ if (r < 0)
+ continue;
+
+ r = strv_consume(&ans, TAKE_PTR(t));
+ if (r < 0)
+ return r;
+ }
+
+ if (strv_isempty(ans))
+ return 0;
+
+ joined = strv_join(strv_uniq(ans), ":");
+ if (!joined)
+ return -ENOMEM;
+
+ /* First, prepend our directories. Second, if some path was specified, use that, and
+ * otherwise use the defaults. Any duplicates will be filtered out in path-lookup.c.
+ * Treat explicit empty path to mean that nothing should be appended. */
+ old = getenv("SYSTEMD_UNIT_PATH");
+ if (!streq_ptr(old, "") &&
+ !strextend_with_separator(&joined, ":", old ?: ""))
+ return -ENOMEM;
+
+ assert_se(set_unit_path(joined) >= 0);
+ return 0;
+}
+
+static int verify_socket(Unit *u) {
+ Unit *service;
+ int r;
+
+ assert(u);
+
+ if (u->type != UNIT_SOCKET)
+ return 0;
+
+ r = socket_load_service_unit(SOCKET(u), -1, &service);
+ if (r < 0)
+ return log_unit_error_errno(u, r, "service unit for the socket cannot be loaded: %m");
+
+ if (service->load_state != UNIT_LOADED)
+ return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOENT),
+ "service %s not loaded, socket cannot be started.", service->id);
+
+ log_unit_debug(u, "using service unit %s.", service->id);
+ return 0;
+}
+
+int verify_executable(Unit *u, const ExecCommand *exec, const char *root) {
+ int r;
+
+ if (!exec)
+ return 0;
+
+ if (exec->flags & EXEC_COMMAND_IGNORE_FAILURE)
+ return 0;
+
+ r = find_executable_full(exec->path, root, NULL, false, NULL, NULL);
+ if (r < 0)
+ return log_unit_error_errno(u, r, "Command %s is not executable: %m", exec->path);
+
+ return 0;
+}
+
+static int verify_executables(Unit *u, const char *root) {
+ ExecCommand *exec;
+ int r = 0, k;
+ unsigned i;
+
+ assert(u);
+
+ exec = u->type == UNIT_SOCKET ? SOCKET(u)->control_command :
+ u->type == UNIT_MOUNT ? MOUNT(u)->control_command :
+ u->type == UNIT_SWAP ? SWAP(u)->control_command : NULL;
+ k = verify_executable(u, exec, root);
+ if (k < 0 && r == 0)
+ r = k;
+
+ if (u->type == UNIT_SERVICE)
+ for (i = 0; i < ELEMENTSOF(SERVICE(u)->exec_command); i++) {
+ k = verify_executable(u, SERVICE(u)->exec_command[i], root);
+ if (k < 0 && r == 0)
+ r = k;
+ }
+
+ if (u->type == UNIT_SOCKET)
+ for (i = 0; i < ELEMENTSOF(SOCKET(u)->exec_command); i++) {
+ k = verify_executable(u, SOCKET(u)->exec_command[i], root);
+ if (k < 0 && r == 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int verify_documentation(Unit *u, bool check_man) {
+ int r = 0, k;
+
+ STRV_FOREACH(p, u->documentation) {
+ log_unit_debug(u, "Found documentation item: %s", *p);
+
+ if (check_man && startswith(*p, "man:")) {
+ k = show_man_page(*p + 4, true);
+ if (k != 0) {
+ if (k < 0)
+ log_unit_error_errno(u, k, "Can't show %s: %m", *p + 4);
+ else {
+ log_unit_error(u, "Command 'man %s' failed with code %d", *p + 4, k);
+ k = -ENOEXEC;
+ }
+ if (r == 0)
+ r = k;
+ }
+ }
+ }
+
+ /* Check remote URLs? */
+
+ return r;
+}
+
+static int verify_unit(Unit *u, bool check_man, const char *root) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r, k;
+
+ assert(u);
+
+ if (DEBUG_LOGGING)
+ unit_dump(u, stdout, "\t");
+
+ log_unit_debug(u, "Creating %s/start job", u->id);
+ r = manager_add_job(u->manager, JOB_START, u, JOB_REPLACE, NULL, &error, NULL);
+ if (r < 0)
+ log_unit_error_errno(u, r, "Failed to create %s/start: %s", u->id, bus_error_message(&error, r));
+
+ k = verify_socket(u);
+ if (k < 0 && r == 0)
+ r = k;
+
+ k = verify_executables(u, root);
+ if (k < 0 && r == 0)
+ r = k;
+
+ k = verify_documentation(u, check_man);
+ if (k < 0 && r == 0)
+ r = k;
+
+ return r;
+}
+
+static void set_destroy_ignore_pointer_max(Set** s) {
+ if (*s == POINTER_MAX)
+ return;
+ set_free_free(*s);
+}
+
+int verify_units(char **filenames, LookupScope scope, bool check_man, bool run_generators, RecursiveErrors recursive_errors, const char *root) {
+ const ManagerTestRunFlags flags =
+ MANAGER_TEST_RUN_MINIMAL |
+ MANAGER_TEST_RUN_ENV_GENERATORS |
+ (recursive_errors == RECURSIVE_ERRORS_NO) * MANAGER_TEST_RUN_IGNORE_DEPENDENCIES |
+ run_generators * MANAGER_TEST_RUN_GENERATORS;
+
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_(set_destroy_ignore_pointer_max) Set *s = NULL;
+ _unused_ _cleanup_(clear_log_syntax_callback) dummy_t dummy;
+ Unit *units[strv_length(filenames)];
+ int r, k, i, count = 0;
+
+ if (strv_isempty(filenames))
+ return 0;
+
+ /* Allow systemd-analyze to hook in a callback function so that it can get
+ * all the required log data from the function itself without having to rely
+ * on a global set variable for the same */
+ set_log_syntax_callback(log_syntax_callback, &s);
+
+ /* set the path */
+ r = verify_set_unit_path(filenames);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set unit load path: %m");
+
+ r = manager_new(scope, flags, &m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize manager: %m");
+
+ log_debug("Starting manager...");
+
+ r = manager_startup(m, /* serialization= */ NULL, /* fds= */ NULL, root);
+ if (r < 0)
+ return r;
+
+ manager_clear_jobs(m);
+
+ log_debug("Loading remaining units from the command line...");
+
+ STRV_FOREACH(filename, filenames) {
+ _cleanup_free_ char *prepared = NULL;
+
+ log_debug("Handling %s...", *filename);
+
+ k = verify_prepare_filename(*filename, &prepared);
+ if (k < 0) {
+ log_error_errno(k, "Failed to prepare filename %s: %m", *filename);
+ if (r == 0)
+ r = k;
+ continue;
+ }
+
+ k = manager_load_startable_unit_or_warn(m, NULL, prepared, &units[count]);
+ if (k < 0) {
+ if (r == 0)
+ r = k;
+ continue;
+ }
+
+ count++;
+ }
+
+ for (i = 0; i < count; i++) {
+ k = verify_unit(units[i], check_man, root);
+ if (k < 0 && r == 0)
+ r = k;
+ }
+
+ if (s == POINTER_MAX)
+ return log_oom();
+
+ if (set_isempty(s) || r != 0)
+ return r;
+
+ /* If all previous verifications succeeded, then either the recursive parsing of all the
+ * associated dependencies with RECURSIVE_ERRORS_YES or the parsing of the specified unit file
+ * with RECURSIVE_ERRORS_NO must have yielded a syntax warning and hence, a non-empty set. */
+ if (IN_SET(recursive_errors, RECURSIVE_ERRORS_YES, RECURSIVE_ERRORS_NO))
+ return -ENOTRECOVERABLE;
+
+ /* If all previous verifications succeeded, then the non-empty set could have resulted from
+ * a syntax warning encountered during the recursive parsing of the specified unit file and
+ * its direct dependencies. Hence, search for any of the filenames in the set and if found,
+ * return a non-zero process exit status. */
+ if (recursive_errors == RECURSIVE_ERRORS_ONE)
+ STRV_FOREACH(filename, filenames)
+ if (set_contains(s, basename(*filename)))
+ return -ENOTRECOVERABLE;
+
+ return 0;
+}
+
+static const char* const recursive_errors_table[_RECURSIVE_ERRORS_MAX] = {
+ [RECURSIVE_ERRORS_NO] = "no",
+ [RECURSIVE_ERRORS_YES] = "yes",
+ [RECURSIVE_ERRORS_ONE] = "one",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(recursive_errors, RecursiveErrors);
diff --git a/src/analyze/analyze-verify-util.h b/src/analyze/analyze-verify-util.h
new file mode 100644
index 0000000..afebaf1
--- /dev/null
+++ b/src/analyze/analyze-verify-util.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "execute.h"
+#include "path-lookup.h"
+
+typedef enum RecursiveErrors {
+ RECURSIVE_ERRORS_YES, /* Look for errors in all associated units */
+ RECURSIVE_ERRORS_NO, /* Don't look for errors in any but the selected unit */
+ RECURSIVE_ERRORS_ONE, /* Look for errors in the selected unit and its direct dependencies */
+ _RECURSIVE_ERRORS_MAX,
+ _RECURSIVE_ERRORS_INVALID = -EINVAL,
+} RecursiveErrors;
+
+int verify_set_unit_path(char **filenames);
+int verify_prepare_filename(const char *filename, char **ret);
+int verify_executable(Unit *u, const ExecCommand *exec, const char *root);
+int verify_units(char **filenames, LookupScope scope, bool check_man, bool run_generators, RecursiveErrors recursive_errors, const char *root);
+
+const char* recursive_errors_to_string(RecursiveErrors i) _const_;
+RecursiveErrors recursive_errors_from_string(const char *s) _pure_;
diff --git a/src/analyze/analyze-verify.c b/src/analyze/analyze-verify.c
new file mode 100644
index 0000000..35e4e1e
--- /dev/null
+++ b/src/analyze/analyze-verify.c
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze.h"
+#include "analyze-verify.h"
+#include "analyze-verify-util.h"
+#include "copy.h"
+#include "rm-rf.h"
+#include "tmpfile-util.h"
+
+static int process_aliases(char *argv[], char *tempdir, char ***ret) {
+ _cleanup_strv_free_ char **filenames = NULL;
+ int r;
+
+ assert(argv);
+ assert(tempdir);
+ assert(ret);
+
+ STRV_FOREACH(filename, strv_skip(argv, 1)) {
+ _cleanup_free_ char *src = NULL, *dst = NULL, *base = NULL;
+ const char *parse_arg;
+
+ parse_arg = *filename;
+ r = extract_first_word(&parse_arg, &src, ":", EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_RETAIN_ESCAPE);
+ if (r < 0)
+ return r;
+
+ if (!parse_arg) {
+ r = strv_consume(&filenames, TAKE_PTR(src));
+ if (r < 0)
+ return r;
+
+ continue;
+ }
+
+ r = path_extract_filename(parse_arg, &base);
+ if (r < 0)
+ return r;
+
+ dst = path_join(tempdir, base);
+ if (!dst)
+ return -ENOMEM;
+
+ r = copy_file(src, dst, 0, 0644, 0, 0, COPY_REFLINK);
+ if (r < 0)
+ return r;
+
+ r = strv_consume(&filenames, TAKE_PTR(dst));
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(filenames);
+ return 0;
+}
+
+int verb_verify(int argc, char *argv[], void *userdata) {
+ _cleanup_strv_free_ char **filenames = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *tempdir = NULL;
+ int r;
+
+ r = mkdtemp_malloc("/tmp/systemd-analyze-XXXXXX", &tempdir);
+ if (r < 0)
+ return log_error_errno(r, "Failed to setup working directory: %m");
+
+ r = process_aliases(argv, tempdir, &filenames);
+ if (r < 0)
+ return log_error_errno(r, "Couldn't process aliases: %m");
+
+ return verify_units(filenames, arg_scope, arg_man, arg_generators, arg_recursive_errors, arg_root);
+}
diff --git a/src/analyze/analyze-verify.h b/src/analyze/analyze-verify.h
new file mode 100644
index 0000000..4892c9a
--- /dev/null
+++ b/src/analyze/analyze-verify.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int verb_verify(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c
new file mode 100644
index 0000000..52d81f9
--- /dev/null
+++ b/src/analyze/analyze.c
@@ -0,0 +1,614 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2013 Simon Peeters
+***/
+
+#include <getopt.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "analyze.h"
+#include "analyze-blame.h"
+#include "analyze-calendar.h"
+#include "analyze-capability.h"
+#include "analyze-cat-config.h"
+#include "analyze-condition.h"
+#include "analyze-critical-chain.h"
+#include "analyze-dot.h"
+#include "analyze-dump.h"
+#include "analyze-exit-status.h"
+#include "analyze-filesystems.h"
+#include "analyze-inspect-elf.h"
+#include "analyze-log-control.h"
+#include "analyze-plot.h"
+#include "analyze-security.h"
+#include "analyze-service-watchdogs.h"
+#include "analyze-syscall-filter.h"
+#include "analyze-time.h"
+#include "analyze-time-data.h"
+#include "analyze-timespan.h"
+#include "analyze-timestamp.h"
+#include "analyze-unit-files.h"
+#include "analyze-unit-paths.h"
+#include "analyze-compare-versions.h"
+#include "analyze-verify.h"
+#include "bus-error.h"
+#include "bus-locator.h"
+#include "bus-map-properties.h"
+#include "bus-unit-util.h"
+#include "calendarspec.h"
+#include "cap-list.h"
+#include "capability-util.h"
+#include "conf-files.h"
+#include "copy.h"
+#include "def.h"
+#include "exit-status.h"
+#include "extract-word.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "filesystems.h"
+#include "format-table.h"
+#include "glob-util.h"
+#include "hashmap.h"
+#include "locale-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "nulstr-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "rm-rf.h"
+#if HAVE_SECCOMP
+# include "seccomp-util.h"
+#endif
+#include "sort-util.h"
+#include "special.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "terminal-util.h"
+#include "time-util.h"
+#include "tmpfile-util.h"
+#include "unit-name.h"
+#include "util.h"
+#include "verb-log-control.h"
+#include "verbs.h"
+#include "version.h"
+
+DotMode arg_dot = DEP_ALL;
+char **arg_dot_from_patterns = NULL, **arg_dot_to_patterns = NULL;
+usec_t arg_fuzz = 0;
+PagerFlags arg_pager_flags = 0;
+BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
+const char *arg_host = NULL;
+LookupScope arg_scope = LOOKUP_SCOPE_SYSTEM;
+RecursiveErrors arg_recursive_errors = _RECURSIVE_ERRORS_INVALID;
+bool arg_man = true;
+bool arg_generators = false;
+char *arg_root = NULL;
+static char *arg_image = NULL;
+char *arg_security_policy = NULL;
+bool arg_offline = false;
+unsigned arg_threshold = 100;
+unsigned arg_iterations = 1;
+usec_t arg_base_time = USEC_INFINITY;
+char *arg_unit = NULL;
+JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+bool arg_quiet = false;
+char *arg_profile = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_dot_from_patterns, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_dot_to_patterns, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_security_policy, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_unit, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_profile, freep);
+
+int acquire_bus(sd_bus **bus, bool *use_full_bus) {
+ bool user = arg_scope != LOOKUP_SCOPE_SYSTEM;
+ int r;
+
+ if (use_full_bus && *use_full_bus) {
+ r = bus_connect_transport(arg_transport, arg_host, user, bus);
+ if (IN_SET(r, 0, -EHOSTDOWN))
+ return r;
+
+ *use_full_bus = false;
+ }
+
+ return bus_connect_transport_systemd(arg_transport, arg_host, user, bus);
+}
+
+int bus_get_unit_property_strv(sd_bus *bus, const char *path, const char *property, char ***strv) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(property);
+ assert(strv);
+
+ r = sd_bus_get_property_strv(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Unit",
+ property,
+ &error,
+ strv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get unit property %s: %s", property, bus_error_message(&error, r));
+
+ return 0;
+}
+
+void time_parsing_hint(const char *p, bool calendar, bool timestamp, bool timespan) {
+ if (calendar && calendar_spec_from_string(p, NULL) >= 0)
+ log_notice("Hint: this expression is a valid calendar specification. "
+ "Use 'systemd-analyze calendar \"%s\"' instead?", p);
+ if (timestamp && parse_timestamp(p, NULL) >= 0)
+ log_notice("Hint: this expression is a valid timestamp. "
+ "Use 'systemd-analyze timestamp \"%s\"' instead?", p);
+ if (timespan && parse_time(p, NULL, USEC_PER_SEC) >= 0)
+ log_notice("Hint: this expression is a valid timespan. "
+ "Use 'systemd-analyze timespan \"%s\"' instead?", p);
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+ _cleanup_free_ char *link = NULL, *dot_link = NULL;
+ int r;
+
+ pager_open(arg_pager_flags);
+
+ r = terminal_urlify_man("systemd-analyze", "1", &link);
+ if (r < 0)
+ return log_oom();
+
+ /* Not using terminal_urlify_man() for this, since we don't want the "man page" text suffix in this case. */
+ r = terminal_urlify("man:dot(1)", "dot(1)", &dot_link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] COMMAND ...\n\n"
+ "%sProfile systemd, show unit dependencies, check unit files.%s\n"
+ "\nCommands:\n"
+ " [time] Print time required to boot the machine\n"
+ " blame Print list of running units ordered by\n"
+ " time to init\n"
+ " critical-chain [UNIT...] Print a tree of the time critical chain\n"
+ " of units\n"
+ " plot Output SVG graphic showing service\n"
+ " initialization\n"
+ " dot [UNIT...] Output dependency graph in %s format\n"
+ " dump [PATTERN...] Output state serialization of service\n"
+ " manager\n"
+ " cat-config Show configuration file and drop-ins\n"
+ " unit-files List files and symlinks for units\n"
+ " unit-paths List load directories for units\n"
+ " exit-status [STATUS...] List exit status definitions\n"
+ " capability [CAP...] List capability definitions\n"
+ " syscall-filter [NAME...] List syscalls in seccomp filters\n"
+ " filesystems [NAME...] List known filesystems\n"
+ " condition CONDITION... Evaluate conditions and asserts\n"
+ " compare-versions VERSION1 [OP] VERSION2\n"
+ " Compare two version strings\n"
+ " verify FILE... Check unit files for correctness\n"
+ " calendar SPEC... Validate repetitive calendar time\n"
+ " events\n"
+ " timestamp TIMESTAMP... Validate a timestamp\n"
+ " timespan SPAN... Validate a time span\n"
+ " security [UNIT...] Analyze security of unit\n"
+ " inspect-elf FILE... Parse and print ELF package metadata\n"
+ "\nOptions:\n"
+ " --recursive-errors=MODE Control which units are verified\n"
+ " --offline=BOOL Perform a security review on unit file(s)\n"
+ " --threshold=N Exit with a non-zero status when overall\n"
+ " exposure level is over threshold value\n"
+ " --security-policy=PATH Use custom JSON security policy instead\n"
+ " of built-in one\n"
+ " --json=pretty|short|off Generate JSON output of the security\n"
+ " analysis table\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --system Operate on system systemd instance\n"
+ " --user Operate on user systemd instance\n"
+ " --global Operate on global user configuration\n"
+ " -H --host=[USER@]HOST Operate on remote host\n"
+ " -M --machine=CONTAINER Operate on local container\n"
+ " --order Show only order in the graph\n"
+ " --require Show only requirement in the graph\n"
+ " --from-pattern=GLOB Show only origins in the graph\n"
+ " --to-pattern=GLOB Show only destinations in the graph\n"
+ " --fuzz=SECONDS Also print services which finished SECONDS\n"
+ " earlier than the latest in the branch\n"
+ " --man[=BOOL] Do [not] check for existence of man pages\n"
+ " --generators[=BOOL] Do [not] run unit generators\n"
+ " (requires privileges)\n"
+ " --iterations=N Show the specified number of iterations\n"
+ " --base-time=TIMESTAMP Calculate calendar times relative to\n"
+ " specified time\n"
+ " --profile=name|PATH Include the specified profile in the\n"
+ " security review of the unit(s)\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " -q --quiet Do not emit hints\n"
+ " --root=PATH Operate on an alternate filesystem root\n"
+ " --image=PATH Operate on disk image as filesystem root\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ dot_link,
+ link);
+
+ /* When updating this list, including descriptions, apply changes to
+ * shell-completion/bash/systemd-analyze and shell-completion/zsh/_systemd-analyze too. */
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_ORDER,
+ ARG_REQUIRE,
+ ARG_ROOT,
+ ARG_IMAGE,
+ ARG_SYSTEM,
+ ARG_USER,
+ ARG_GLOBAL,
+ ARG_DOT_FROM_PATTERN,
+ ARG_DOT_TO_PATTERN,
+ ARG_FUZZ,
+ ARG_NO_PAGER,
+ ARG_MAN,
+ ARG_GENERATORS,
+ ARG_ITERATIONS,
+ ARG_BASE_TIME,
+ ARG_RECURSIVE_ERRORS,
+ ARG_OFFLINE,
+ ARG_THRESHOLD,
+ ARG_SECURITY_POLICY,
+ ARG_JSON,
+ ARG_PROFILE,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "quiet", no_argument, NULL, 'q' },
+ { "order", no_argument, NULL, ARG_ORDER },
+ { "require", no_argument, NULL, ARG_REQUIRE },
+ { "root", required_argument, NULL, ARG_ROOT },
+ { "image", required_argument, NULL, ARG_IMAGE },
+ { "recursive-errors", required_argument, NULL, ARG_RECURSIVE_ERRORS },
+ { "offline", required_argument, NULL, ARG_OFFLINE },
+ { "threshold", required_argument, NULL, ARG_THRESHOLD },
+ { "security-policy", required_argument, NULL, ARG_SECURITY_POLICY },
+ { "system", no_argument, NULL, ARG_SYSTEM },
+ { "user", no_argument, NULL, ARG_USER },
+ { "global", no_argument, NULL, ARG_GLOBAL },
+ { "from-pattern", required_argument, NULL, ARG_DOT_FROM_PATTERN },
+ { "to-pattern", required_argument, NULL, ARG_DOT_TO_PATTERN },
+ { "fuzz", required_argument, NULL, ARG_FUZZ },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "man", optional_argument, NULL, ARG_MAN },
+ { "generators", optional_argument, NULL, ARG_GENERATORS },
+ { "host", required_argument, NULL, 'H' },
+ { "machine", required_argument, NULL, 'M' },
+ { "iterations", required_argument, NULL, ARG_ITERATIONS },
+ { "base-time", required_argument, NULL, ARG_BASE_TIME },
+ { "unit", required_argument, NULL, 'U' },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "profile", required_argument, NULL, ARG_PROFILE },
+ {}
+ };
+
+ int r, c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hH:M:U:q", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ return help(0, NULL, NULL);
+
+ case ARG_VERSION:
+ return version();
+
+ case 'q':
+ arg_quiet = true;
+ break;
+
+ case ARG_RECURSIVE_ERRORS:
+ if (streq(optarg, "help")) {
+ DUMP_STRING_TABLE(recursive_errors, RecursiveErrors, _RECURSIVE_ERRORS_MAX);
+ return 0;
+ }
+ r = recursive_errors_from_string(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Unknown mode passed to --recursive-errors='%s'.", optarg);
+
+ arg_recursive_errors = r;
+ break;
+
+ case ARG_ROOT:
+ r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_IMAGE:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_SYSTEM:
+ arg_scope = LOOKUP_SCOPE_SYSTEM;
+ break;
+
+ case ARG_USER:
+ arg_scope = LOOKUP_SCOPE_USER;
+ break;
+
+ case ARG_GLOBAL:
+ arg_scope = LOOKUP_SCOPE_GLOBAL;
+ break;
+
+ case ARG_ORDER:
+ arg_dot = DEP_ORDER;
+ break;
+
+ case ARG_REQUIRE:
+ arg_dot = DEP_REQUIRE;
+ break;
+
+ case ARG_DOT_FROM_PATTERN:
+ if (strv_extend(&arg_dot_from_patterns, optarg) < 0)
+ return log_oom();
+
+ break;
+
+ case ARG_DOT_TO_PATTERN:
+ if (strv_extend(&arg_dot_to_patterns, optarg) < 0)
+ return log_oom();
+
+ break;
+
+ case ARG_FUZZ:
+ r = parse_sec(optarg, &arg_fuzz);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case 'H':
+ arg_transport = BUS_TRANSPORT_REMOTE;
+ arg_host = optarg;
+ break;
+
+ case 'M':
+ arg_transport = BUS_TRANSPORT_MACHINE;
+ arg_host = optarg;
+ break;
+
+ case ARG_MAN:
+ r = parse_boolean_argument("--man", optarg, &arg_man);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_GENERATORS:
+ r = parse_boolean_argument("--generators", optarg, &arg_generators);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_OFFLINE:
+ r = parse_boolean_argument("--offline", optarg, &arg_offline);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_THRESHOLD:
+ r = safe_atou(optarg, &arg_threshold);
+ if (r < 0 || arg_threshold > 100)
+ return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Failed to parse threshold: %s", optarg);
+
+ break;
+
+ case ARG_SECURITY_POLICY:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_security_policy);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+ break;
+
+ case ARG_ITERATIONS:
+ r = safe_atou(optarg, &arg_iterations);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse iterations: %s", optarg);
+
+ break;
+
+ case ARG_BASE_TIME:
+ r = parse_timestamp(optarg, &arg_base_time);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --base-time= parameter: %s", optarg);
+
+ break;
+
+ case ARG_PROFILE:
+ if (isempty(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Profile file name is empty");
+
+ if (is_path(optarg)) {
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_profile);
+ if (r < 0)
+ return r;
+ if (!endswith(arg_profile, ".conf"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Profile file name must end with .conf: %s", arg_profile);
+ } else {
+ r = free_and_strdup(&arg_profile, optarg);
+ if (r < 0)
+ return log_oom();
+ }
+
+ break;
+
+ case 'U': {
+ _cleanup_free_ char *mangled = NULL;
+
+ r = unit_name_mangle(optarg, UNIT_NAME_MANGLE_WARN, &mangled);
+ if (r < 0)
+ return log_error_errno(r, "Failed to mangle unit name %s: %m", optarg);
+
+ free_and_replace(arg_unit, mangled);
+ break;
+ }
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (arg_offline && !streq_ptr(argv[optind], "security"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option --offline= is only supported for security right now.");
+
+ if (arg_json_format_flags != JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option --json= is only supported for security and inspect-elf right now.");
+
+ if (arg_threshold != 100 && !streq_ptr(argv[optind], "security"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option --threshold= is only supported for security right now.");
+
+ if (arg_scope == LOOKUP_SCOPE_GLOBAL &&
+ !STR_IN_SET(argv[optind] ?: "time", "dot", "unit-paths", "verify"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option --global only makes sense with verbs dot, unit-paths, verify.");
+
+ if (streq_ptr(argv[optind], "cat-config") && arg_scope == LOOKUP_SCOPE_USER)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option --user is not supported for cat-config right now.");
+
+ if (arg_security_policy && !streq_ptr(argv[optind], "security"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option --security-policy= is only supported for security.");
+
+ if ((arg_root || arg_image) && (!STRPTR_IN_SET(argv[optind], "cat-config", "verify", "condition")) &&
+ (!(streq_ptr(argv[optind], "security") && arg_offline)))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Options --root= and --image= are only supported for cat-config, verify, condition and security when used with --offline= right now.");
+
+ /* Having both an image and a root is not supported by the code */
+ if (arg_root && arg_image)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
+
+ if (arg_unit && !streq_ptr(argv[optind], "condition"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --unit= is only supported for condition");
+
+ if (streq_ptr(argv[optind], "condition") && !arg_unit && optind >= argc - 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments for condition");
+
+ if (streq_ptr(argv[optind], "condition") && arg_unit && optind < argc - 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No conditions can be passed if --unit= is used.");
+
+ return 1; /* work to do */
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *unlink_dir = NULL;
+
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "time", VERB_ANY, 1, VERB_DEFAULT, verb_time },
+ { "blame", VERB_ANY, 1, 0, verb_blame },
+ { "critical-chain", VERB_ANY, VERB_ANY, 0, verb_critical_chain },
+ { "plot", VERB_ANY, 1, 0, verb_plot },
+ { "dot", VERB_ANY, VERB_ANY, 0, verb_dot },
+ /* ↓ The following seven verbs are deprecated, from here … ↓ */
+ { "log-level", VERB_ANY, 2, 0, verb_log_control },
+ { "log-target", VERB_ANY, 2, 0, verb_log_control },
+ { "set-log-level", 2, 2, 0, verb_log_control },
+ { "get-log-level", VERB_ANY, 1, 0, verb_log_control },
+ { "set-log-target", 2, 2, 0, verb_log_control },
+ { "get-log-target", VERB_ANY, 1, 0, verb_log_control },
+ { "service-watchdogs", VERB_ANY, 2, 0, verb_service_watchdogs },
+ /* ↑ … until here ↑ */
+ { "dump", VERB_ANY, VERB_ANY, 0, verb_dump },
+ { "cat-config", 2, VERB_ANY, 0, verb_cat_config },
+ { "unit-files", VERB_ANY, VERB_ANY, 0, verb_unit_files },
+ { "unit-paths", 1, 1, 0, verb_unit_paths },
+ { "exit-status", VERB_ANY, VERB_ANY, 0, verb_exit_status },
+ { "syscall-filter", VERB_ANY, VERB_ANY, 0, verb_syscall_filters },
+ { "capability", VERB_ANY, VERB_ANY, 0, verb_capabilities },
+ { "filesystems", VERB_ANY, VERB_ANY, 0, verb_filesystems },
+ { "condition", VERB_ANY, VERB_ANY, 0, verb_condition },
+ { "compare-versions", 3, 4, 0, verb_compare_versions },
+ { "verify", 2, VERB_ANY, 0, verb_verify },
+ { "calendar", 2, VERB_ANY, 0, verb_calendar },
+ { "timestamp", 2, VERB_ANY, 0, verb_timestamp },
+ { "timespan", 2, VERB_ANY, 0, verb_timespan },
+ { "security", VERB_ANY, VERB_ANY, 0, verb_security },
+ { "inspect-elf", 2, VERB_ANY, 0, verb_elf_inspection },
+ {}
+ };
+
+ int r;
+
+ setlocale(LC_ALL, "");
+ setlocale(LC_NUMERIC, "C"); /* we want to format/parse floats in C style */
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ /* Open up and mount the image */
+ if (arg_image) {
+ assert(!arg_root);
+
+ r = mount_image_privately_interactively(
+ arg_image,
+ DISSECT_IMAGE_GENERIC_ROOT |
+ DISSECT_IMAGE_RELAX_VAR_CHECK |
+ DISSECT_IMAGE_READ_ONLY,
+ &unlink_dir,
+ &loop_device);
+ if (r < 0)
+ return r;
+
+ arg_root = strdup(unlink_dir);
+ if (!arg_root)
+ return log_oom();
+ }
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/analyze/analyze.h b/src/analyze/analyze.h
new file mode 100644
index 0000000..da12058
--- /dev/null
+++ b/src/analyze/analyze.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "analyze-verify-util.h"
+#include "bus-util.h"
+#include "json.h"
+#include "pager.h"
+#include "time-util.h"
+#include "unit-file.h"
+
+typedef enum DotMode {
+ DEP_ALL,
+ DEP_ORDER,
+ DEP_REQUIRE,
+} DotMode;
+
+extern DotMode arg_dot;
+extern char **arg_dot_from_patterns, **arg_dot_to_patterns;
+extern usec_t arg_fuzz;
+extern PagerFlags arg_pager_flags;
+extern BusTransport arg_transport;
+extern const char *arg_host;
+extern LookupScope arg_scope;
+extern RecursiveErrors arg_recursive_errors;
+extern bool arg_man;
+extern bool arg_generators;
+extern char *arg_root;
+extern char *arg_security_policy;
+extern bool arg_offline;
+extern unsigned arg_threshold;
+extern unsigned arg_iterations;
+extern usec_t arg_base_time;
+extern char *arg_unit;
+extern JsonFormatFlags arg_json_format_flags;
+extern bool arg_quiet;
+extern char *arg_profile;
+
+int acquire_bus(sd_bus **bus, bool *use_full_bus);
+
+int bus_get_unit_property_strv(sd_bus *bus, const char *path, const char *property, char ***strv);
+
+void time_parsing_hint(const char *p, bool calendar, bool timestamp, bool timespan);
diff --git a/src/analyze/meson.build b/src/analyze/meson.build
new file mode 100644
index 0000000..24ef941
--- /dev/null
+++ b/src/analyze/meson.build
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+systemd_analyze_sources = files(
+ 'analyze-blame.c',
+ 'analyze-blame.h',
+ 'analyze-calendar.c',
+ 'analyze-calendar.h',
+ 'analyze-capability.c',
+ 'analyze-capability.h',
+ 'analyze-cat-config.c',
+ 'analyze-cat-config.h',
+ 'analyze-compare-versions.c',
+ 'analyze-compare-versions.h',
+ 'analyze-condition.c',
+ 'analyze-condition.h',
+ 'analyze-critical-chain.c',
+ 'analyze-critical-chain.h',
+ 'analyze-dot.c',
+ 'analyze-dot.h',
+ 'analyze-dump.c',
+ 'analyze-dump.h',
+ 'analyze-exit-status.c',
+ 'analyze-exit-status.h',
+ 'analyze-filesystems.c',
+ 'analyze-filesystems.h',
+ 'analyze-inspect-elf.c',
+ 'analyze-inspect-elf.h',
+ 'analyze-log-control.c',
+ 'analyze-log-control.h',
+ 'analyze-plot.c',
+ 'analyze-plot.h',
+ 'analyze-security.c',
+ 'analyze-security.h',
+ 'analyze-service-watchdogs.c',
+ 'analyze-service-watchdogs.h',
+ 'analyze-syscall-filter.c',
+ 'analyze-syscall-filter.h',
+ 'analyze-time.c',
+ 'analyze-time.h',
+ 'analyze-time-data.c',
+ 'analyze-time-data.h',
+ 'analyze-timespan.c',
+ 'analyze-timespan.h',
+ 'analyze-timestamp.c',
+ 'analyze-timestamp.h',
+ 'analyze-unit-files.c',
+ 'analyze-unit-files.h',
+ 'analyze-unit-paths.c',
+ 'analyze-unit-paths.h',
+ 'analyze-verify.c',
+ 'analyze-verify.h',
+ 'analyze-verify-util.c',
+ 'analyze-verify-util.h',
+ 'analyze.c')
+
+tests += [
+ [files('test-verify.c',
+ 'analyze-verify-util.c',
+ 'analyze-verify-util.h'),
+ [libcore,
+ libshared],
+ [],
+ core_includes],
+]
diff --git a/src/analyze/test-verify.c b/src/analyze/test-verify.c
new file mode 100644
index 0000000..d37e54b
--- /dev/null
+++ b/src/analyze/test-verify.c
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "analyze-verify-util.h"
+#include "tests.h"
+
+TEST(verify_nonexistent) {
+ /* Negative cases */
+ assert_se(verify_executable(NULL, &(ExecCommand) {.flags = EXEC_COMMAND_IGNORE_FAILURE, .path = (char*) "/non/existent"}, NULL) == 0);
+ assert_se(verify_executable(NULL, &(ExecCommand) {.path = (char*) "/non/existent"}, NULL) < 0);
+
+ /* Ordinary cases */
+ assert_se(verify_executable(NULL, &(ExecCommand) {.path = (char*) "/bin/echo"}, NULL) == 0);
+ assert_se(verify_executable(NULL, &(ExecCommand) {.flags = EXEC_COMMAND_IGNORE_FAILURE, .path = (char*) "/bin/echo"}, NULL) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);