diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml
index 8e8f776377..fb872c4990 100644
--- a/man/systemd-analyze.xml
+++ b/man/systemd-analyze.xml
@@ -1109,6 +1109,22 @@ Service b@0.service not loaded, b.socket cannot be started.
+
+
+
+ With the security command, generate a JSON formatted
+ output of the security analysis table. The format is a JSON array with objects
+ containing the following fields: set which indicates if the setting has
+ been enabled or not, name which is what is used to refer to the setting,
+ json_field which is the JSON compatible identifier of the setting,
+ description which is an outline of the setting state, and
+ exposure which is a number in the range 0.0…10.0, where a higher value
+ corresponds to a higher security threat. The JSON version of the table is printed to standard
+ output. The MODE passed to the option can be one of three:
+ which is the default, and
+ which respectively output a prettified or shorted JSON version of the security table.
+
+
diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze
index 6bed5e73e8..3022d4058b 100644
--- a/shell-completion/bash/systemd-analyze
+++ b/shell-completion/bash/systemd-analyze
@@ -145,7 +145,7 @@ _systemd_analyze() {
elif __contains_word "$verb" ${VERBS[SECURITY]}; then
if [[ $cur = -* ]]; then
- comps='--help --version --no-pager --system --user -H --host -M --machine --offline --threshold --security-policy'
+ comps='--help --version --no-pager --system --user -H --host -M --machine --offline --threshold --security-policy --json=off --json=pretty --json=short'
else
if __contains_word "--user" ${COMP_WORDS[*]}; then
mode=--user
diff --git a/shell-completion/zsh/_systemd-analyze b/shell-completion/zsh/_systemd-analyze
index 3b77c3b938..75d35e115a 100644
--- a/shell-completion/zsh/_systemd-analyze
+++ b/shell-completion/zsh/_systemd-analyze
@@ -93,6 +93,7 @@ _arguments \
'--offline=[Perform a security review of the specified unit file(s)]:BOOL' \
'--threshold=[Set a value to compare the overall security exposure level with]: NUMBER' \
'--security-policy=[Allow user to use customized requirements to compare unit file(s) against]: PATH' \
+ '--json=[Generate a JSON output of the security analysis table]:MODE:(pretty short off)' \
'--no-pager[Do not pipe output into a pager]' \
'--man=[Do (not) check for existence of man pages]:boolean:(1 0)' \
'--order[When generating graph for dot, show only order]' \
diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c
index adb73ab7a9..b880642bb9 100644
--- a/src/analyze/analyze-security.c
+++ b/src/analyze/analyze-security.c
@@ -1709,7 +1709,9 @@ static int assess(const SecurityInfo *info,
Table *overview_table,
AnalyzeSecurityFlags flags,
unsigned threshold,
- JsonVariant *policy) {
+ JsonVariant *policy,
+ PagerFlags pager_flags,
+ JsonFormatFlags json_format_flags) {
static const struct {
uint64_t exposure;
@@ -1732,15 +1734,19 @@ static int assess(const SecurityInfo *info,
int r;
if (!FLAGS_SET(flags, ANALYZE_SECURITY_SHORT)) {
- details_table = table_new(" ", "name", "description", "weight", "badness", "range", "exposure");
+ details_table = table_new(" ", "name", "json_field", "description", "weight", "badness", "range", "exposure");
if (!details_table)
return log_oom();
+ r = table_set_json_field_name(details_table, 0, "set");
+ if (r < 0)
+ return log_error_errno(r, "Failed to set JSON field name of column 0: %m");
+
(void) table_set_sort(details_table, (size_t) 3, (size_t) 1);
(void) table_set_reverse(details_table, 3, true);
if (getenv_bool("SYSTEMD_ANALYZE_DEBUG") <= 0)
- (void) table_set_display(details_table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 6);
+ (void) table_set_display(details_table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 7);
}
for (i = 0; i < ELEMENTSOF(security_assessor_table); i++) {
@@ -1774,23 +1780,23 @@ static int assess(const SecurityInfo *info,
}
if (details_table) {
- const char *checkmark, *description, *color = NULL;
- const char *id = a->id;
+ const char *description, *color = NULL;
+ int checkmark;
if (badness == UINT64_MAX) {
- checkmark = " ";
+ checkmark = -1;
description = access_description_na(a, policy);
color = NULL;
} else if (badness == a->range) {
- checkmark = special_glyph(SPECIAL_GLYPH_CROSS_MARK);
+ checkmark = 0;
description = access_description_bad(a, policy);
color = ansi_highlight_red();
} else if (badness == 0) {
- checkmark = special_glyph(SPECIAL_GLYPH_CHECK_MARK);
+ checkmark = 1;
description = access_description_good(a, policy);
color = ansi_highlight_green();
} else {
- checkmark = special_glyph(SPECIAL_GLYPH_CROSS_MARK);
+ checkmark = 0;
description = NULL;
color = ansi_highlight_red();
}
@@ -1798,16 +1804,24 @@ static int assess(const SecurityInfo *info,
if (d)
description = d;
- if (json_variant_by_key(policy, a->json_field) != NULL)
- id = a->json_field;
+ if (checkmark < 0) {
+ r = table_add_many(details_table, TABLE_EMPTY);
+ if (r < 0)
+ return table_log_add_error(r);
+ } else {
+ r = table_add_many(details_table,
+ TABLE_BOOLEAN_CHECKMARK, checkmark > 0,
+ TABLE_SET_MINIMUM_WIDTH, 1,
+ TABLE_SET_MAXIMUM_WIDTH, 1,
+ TABLE_SET_ELLIPSIZE_PERCENT, 0,
+ TABLE_SET_COLOR, color);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
r = table_add_many(details_table,
- TABLE_STRING, checkmark,
- TABLE_SET_MINIMUM_WIDTH, 1,
- TABLE_SET_MAXIMUM_WIDTH, 1,
- TABLE_SET_ELLIPSIZE_PERCENT, 0,
- TABLE_SET_COLOR, color,
- TABLE_STRING, id, TABLE_SET_URL, a->url,
+ TABLE_STRING, a->id, TABLE_SET_URL, a->url,
+ TABLE_STRING, a->json_field,
TABLE_STRING, description,
TABLE_UINT64, weight, TABLE_SET_ALIGN_PERCENT, 100,
TABLE_UINT64, badness, TABLE_SET_ALIGN_PERCENT, 100,
@@ -1829,14 +1843,14 @@ static int assess(const SecurityInfo *info,
TableCell *cell;
uint64_t x;
- assert_se(weight = table_get_at(details_table, row, 3));
- assert_se(badness = table_get_at(details_table, row, 4));
- assert_se(range = table_get_at(details_table, row, 5));
+ assert_se(weight = table_get_at(details_table, row, 4));
+ assert_se(badness = table_get_at(details_table, row, 5));
+ assert_se(range = table_get_at(details_table, row, 6));
if (*badness == UINT64_MAX || *badness == 0)
continue;
- assert_se(cell = table_get_cell(details_table, row, 6));
+ assert_se(cell = table_get_cell(details_table, row, 7));
x = DIV_ROUND_UP(DIV_ROUND_UP(*badness * *weight * 100U, *range), weight_sum);
xsprintf(buf, "%" PRIu64 ".%" PRIu64, x / 10, x % 10);
@@ -1846,7 +1860,13 @@ static int assess(const SecurityInfo *info,
return log_error_errno(r, "Failed to update cell in table: %m");
}
- r = table_print(details_table, stdout);
+ if (json_format_flags & JSON_FORMAT_OFF) {
+ r = table_hide_column_from_display(details_table, (size_t) 2);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set columns to display: %m");
+ }
+
+ r = table_print_with_pager(details_table, json_format_flags, pager_flags, /* show_header= */true);
if (r < 0)
return log_error_errno(r, "Failed to output table: %m");
}
@@ -1859,7 +1879,7 @@ static int assess(const SecurityInfo *info,
assert(i < ELEMENTSOF(badness_table));
- if (details_table) {
+ if (details_table && (json_format_flags & JSON_FORMAT_OFF)) {
_cleanup_free_ char *clickable = NULL;
const char *name;
@@ -2386,7 +2406,9 @@ static int analyze_security_one(sd_bus *bus,
Table *overview_table,
AnalyzeSecurityFlags flags,
unsigned threshold,
- JsonVariant *policy) {
+ JsonVariant *policy,
+ PagerFlags pager_flags,
+ JsonFormatFlags json_format_flags) {
_cleanup_(security_info_freep) SecurityInfo *info = security_info_new();
if (!info)
@@ -2403,7 +2425,7 @@ static int analyze_security_one(sd_bus *bus,
if (r < 0)
return r;
- r = assess(info, overview_table, flags, threshold, policy);
+ r = assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
if (r < 0)
return r;
@@ -2589,7 +2611,12 @@ static int get_security_info(Unit *u, ExecContext *c, CGroupContext *g, Security
return 0;
}
-static int offline_security_check(Unit *u, unsigned threshold, JsonVariant *policy) {
+static int offline_security_check(Unit *u,
+ unsigned threshold,
+ JsonVariant *policy,
+ PagerFlags pager_flags,
+ JsonFormatFlags json_format_flags) {
+
_cleanup_(table_unrefp) Table *overview_table = NULL;
AnalyzeSecurityFlags flags = 0;
_cleanup_(security_info_freep) SecurityInfo *info = NULL;
@@ -2604,7 +2631,7 @@ static int offline_security_check(Unit *u, unsigned threshold, JsonVariant *poli
if (r < 0)
return r;
- return assess(info, overview_table, flags, threshold, policy);
+ return assess(info, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
}
static int offline_security_checks(char **filenames,
@@ -2613,7 +2640,9 @@ static int offline_security_checks(char **filenames,
bool check_man,
bool run_generators,
unsigned threshold,
- const char *root) {
+ const char *root,
+ PagerFlags pager_flags,
+ JsonFormatFlags json_format_flags) {
const ManagerTestRunFlags flags =
MANAGER_TEST_RUN_MINIMAL |
@@ -2673,7 +2702,7 @@ static int offline_security_checks(char **filenames,
}
for (size_t i = 0; i < count; i++) {
- k = offline_security_check(units[i], threshold, policy);
+ k = offline_security_check(units[i], threshold, policy, pager_flags, json_format_flags);
if (k < 0 && r == 0)
r = k;
}
@@ -2690,6 +2719,8 @@ int analyze_security(sd_bus *bus,
bool offline,
unsigned threshold,
const char *root,
+ JsonFormatFlags json_format_flags,
+ PagerFlags pager_flags,
AnalyzeSecurityFlags flags) {
_cleanup_(table_unrefp) Table *overview_table = NULL;
@@ -2698,7 +2729,7 @@ int analyze_security(sd_bus *bus,
assert(bus);
if (offline)
- return offline_security_checks(units, policy, scope, check_man, run_generators, threshold, root);
+ return offline_security_checks(units, policy, scope, check_man, run_generators, threshold, root, pager_flags, json_format_flags);
if (strv_length(units) != 1) {
overview_table = table_new("unit", "exposure", "predicate", "happy");
@@ -2758,7 +2789,7 @@ int analyze_security(sd_bus *bus,
flags |= ANALYZE_SECURITY_SHORT|ANALYZE_SECURITY_ONLY_LOADED|ANALYZE_SECURITY_ONLY_LONG_RUNNING;
STRV_FOREACH(i, list) {
- r = analyze_security_one(bus, *i, overview_table, flags, threshold, policy);
+ r = analyze_security_one(bus, *i, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
if (r < 0 && ret >= 0)
ret = r;
}
@@ -2793,7 +2824,7 @@ int analyze_security(sd_bus *bus,
} else
name = mangled;
- r = analyze_security_one(bus, name, overview_table, flags, threshold, policy);
+ r = analyze_security_one(bus, name, overview_table, flags, threshold, policy, pager_flags, json_format_flags);
if (r < 0 && ret >= 0)
ret = r;
}
@@ -2805,10 +2836,9 @@ int analyze_security(sd_bus *bus,
fflush(stdout);
}
- r = table_print(overview_table, stdout);
+ r = table_print_with_pager(overview_table, json_format_flags, pager_flags, /* show_header= */true);
if (r < 0)
return log_error_errno(r, "Failed to output table: %m");
}
-
return ret;
}
diff --git a/src/analyze/analyze-security.h b/src/analyze/analyze-security.h
index 8ad5a689f5..492881c385 100644
--- a/src/analyze/analyze-security.h
+++ b/src/analyze/analyze-security.h
@@ -6,6 +6,7 @@
#include "sd-bus.h"
#include "json.h"
+#include "pager.h"
#include "unit-file.h"
typedef enum AnalyzeSecurityFlags {
@@ -23,4 +24,6 @@ int analyze_security(sd_bus *bus,
bool offline,
unsigned threshold,
const char *root,
+ JsonFormatFlags json_format_flags,
+ PagerFlags pager_flags,
AnalyzeSecurityFlags flags);
diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c
index 68b9941afe..6039219a35 100644
--- a/src/analyze/analyze.c
+++ b/src/analyze/analyze.c
@@ -97,6 +97,7 @@ static unsigned arg_threshold = 100;
static unsigned arg_iterations = 1;
static usec_t arg_base_time = USEC_INFINITY;
static char *arg_unit = NULL;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
STATIC_DESTRUCTOR_REGISTER(arg_dot_from_patterns, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_dot_to_patterns, strv_freep);
@@ -2196,6 +2197,8 @@ static int do_security(int argc, char *argv[], void *userdata) {
arg_offline,
arg_threshold,
arg_root,
+ arg_json_format_flags,
+ arg_pager_flags,
/*flags=*/ 0);
}
@@ -2250,6 +2253,8 @@ static int help(int argc, char *argv[], void *userdata) {
" --version Show package version\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"
@@ -2303,6 +2308,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_OFFLINE,
ARG_THRESHOLD,
ARG_SECURITY_POLICY,
+ ARG_JSON,
};
static const struct option options[] = {
@@ -2330,6 +2336,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "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 },
{}
};
@@ -2454,6 +2461,12 @@ static int parse_argv(int argc, char *argv[]) {
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)
@@ -2489,6 +2502,10 @@ static int parse_argv(int argc, char *argv[]) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Option --offline= is only supported for security right now.");
+ if (arg_json_format_flags != JSON_FORMAT_OFF && !streq_ptr(argv[optind], "security"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option --json= is only supported for security 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.");
diff --git a/src/shared/format-table.c b/src/shared/format-table.c
index 5390eddcd6..806c24ad73 100644
--- a/src/shared/format-table.c
+++ b/src/shared/format-table.c
@@ -267,6 +267,7 @@ static size_t table_data_size(TableDataType type, const void *data) {
case TABLE_STRV_WRAPPED:
return sizeof(char **);
+ case TABLE_BOOLEAN_CHECKMARK:
case TABLE_BOOLEAN:
return sizeof(bool);
@@ -848,6 +849,7 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) {
data = va_arg(ap, char * const *);
break;
+ case TABLE_BOOLEAN_CHECKMARK:
case TABLE_BOOLEAN:
buffer.b = va_arg(ap, int);
data = &buffer.b;
@@ -1443,6 +1445,9 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas
case TABLE_BOOLEAN:
return yes_no(d->boolean);
+ case TABLE_BOOLEAN_CHECKMARK:
+ return special_glyph(d->boolean ? SPECIAL_GLYPH_CHECK_MARK : SPECIAL_GLYPH_CROSS_MARK);
+
case TABLE_TIMESTAMP:
case TABLE_TIMESTAMP_UTC:
case TABLE_TIMESTAMP_RELATIVE: {
@@ -2488,6 +2493,7 @@ static int table_data_to_json(TableData *d, JsonVariant **ret) {
case TABLE_STRV_WRAPPED:
return json_variant_new_array_strv(ret, d->strv);
+ case TABLE_BOOLEAN_CHECKMARK:
case TABLE_BOOLEAN:
return json_variant_new_boolean(ret, d->boolean);
diff --git a/src/shared/format-table.h b/src/shared/format-table.h
index 2b189f8892..6f60669406 100644
--- a/src/shared/format-table.h
+++ b/src/shared/format-table.h
@@ -16,6 +16,7 @@ typedef enum TableDataType {
TABLE_STRV_WRAPPED,
TABLE_PATH,
TABLE_BOOLEAN,
+ TABLE_BOOLEAN_CHECKMARK,
TABLE_TIMESTAMP,
TABLE_TIMESTAMP_UTC,
TABLE_TIMESTAMP_RELATIVE,