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,