From 39dea1245e50fc61b24ca0e16c32063b028b7ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Wed, 8 Apr 2026 20:50:54 +0200 Subject: [PATCH] feat: rework the --batch json output entirely --- DEVELOPMENT.md | 36 +++- src/libs/002_core_globals.sh | 3 +- src/libs/230_util_optparse.sh | 4 +- src/libs/250_output_emitters.sh | 293 +++++++++++++++++++++++++++++++- src/libs/380_hw_microcode.sh | 5 +- src/libs/400_hw_check.sh | 10 ++ src/main.sh | 35 +++- src/vulns/CVE-2018-3639.sh | 18 +- 8 files changed, 384 insertions(+), 20 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b128eaf..8d54875 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -84,8 +84,11 @@ sudo ./spectre-meltdown-checker.sh --variant l1tf --variant taa # Run specific tests that we might have just added (CVE name) sudo ./spectre-meltdown-checker.sh --cve CVE-2018-3640 --cve CVE-2022-40982 -# Batch JSON mode (CI validates exactly 19 CVEs in output) -sudo ./spectre-meltdown-checker.sh --batch json | jq '.[] | .CVE' | wc -l # must be 19 +# Batch JSON mode (comprehensive output) +sudo ./spectre-meltdown-checker.sh --batch json | python3 -m json.tool + +# Batch JSON terse mode (legacy flat array) +sudo ./spectre-meltdown-checker.sh --batch json-terse | python3 -m json.tool # Update microcode firmware database sudo ./spectre-meltdown-checker.sh --update-fwdb @@ -105,7 +108,25 @@ The entire tool is a single bash script with no external script dependencies. Ke - **Microcode database** (embedded): Intel/AMD microcode version lookup via `read_mcedb`/`read_inteldb`; updated automatically via `.github/workflows/autoupdate.yml` - **Kernel analysis** (~line 1568): `extract_kernel`, `try_decompress` - extracts and inspects kernel images (handles gzip, bzip2, xz, lz4, zstd compression) - **Vulnerability checks**: 19 `check_CVE__()` functions, each with `_linux()` and `_bsd()` variants. Uses whitelist logic (assumes affected unless proven otherwise) -- **Main flow** (~line 6668): Parse options → detect CPU → loop through requested CVEs → output results (text/json/nrpe/prometheus) → cleanup +- **Batch output emitters** (`src/libs/250_output_emitters.sh`): `_emit_json_full`, `_emit_json_terse`, `_emit_text`, `_emit_nrpe`, `_emit_prometheus`, plus JSON section builders (`_build_json_meta`, `_build_json_system`, `_build_json_cpu`, `_build_json_cpu_microcode`) +- **Main flow** (~line 6668): Parse options → detect CPU → loop through requested CVEs → output results (text/json/json-terse/nrpe/prometheus) → cleanup + +### JSON Batch Output Formats + +Two JSON formats are available via `--batch`: + +- **`--batch json`** (comprehensive): A top-level object with five sections: + - `meta` — script version, format version, timestamp, run mode flags (`run_as_root`, `reduced_accuracy`, `mocked`, `paranoid`, `sysfs_only`, `no_hw`, `extra`) + - `system` — kernel release/version/arch/cmdline, CPU count, SMT status, hypervisor host detection + - `cpu` — vendor, model name, family/model/stepping, CPUID, codename, ARM fields (`arm_part_list`, `arm_arch_list`), plus a `capabilities` sub-object containing all `cap_*` hardware flags as booleans/nulls/strings + - `cpu_microcode` — `installed_version`, `latest_version`, `microcode_up_to_date`, `is_blacklisted`, firmware DB source/info + - `vulnerabilities` — array of per-CVE objects: `cve`, `name`, `aliases`, `cpu_affected`, `status`, `vulnerable`, `info`, `sysfs_status`, `sysfs_message` + +- **`--batch json-terse`** (legacy): A flat array of objects with four fields: `NAME`, `CVE`, `VULNERABLE` (bool/null), `INFOS`. This is the original format, preserved for backward compatibility. + +The comprehensive format is built in two phases: static sections (`meta`, `system`, `cpu`, `cpu_microcode`) are assembled after `check_cpu()` completes, and per-CVE entries are accumulated during the main CVE loop via `_emit_json_full()`. The sysfs data for each CVE is captured by `sys_interface_check()` into `g_json_cve_sysfs_status`/`g_json_cve_sysfs_msg` globals, which are read by the emitter and reset after each CVE to prevent cross-CVE leakage. CPU affection is determined via the already-cached `is_cpu_affected()`. + +When adding new `cap_*` variables (for a new CVE or updated hardware support), they must be added to `_build_json_cpu()` in `src/libs/250_output_emitters.sh`. Per-CVE data is handled automatically. ## Key Design Principles @@ -315,6 +336,8 @@ When populating the CPU model list, use the **most recent version** of the Linux **Important**: Do not confuse hardware immunity bits with *mitigation* capability bits. A hardware immunity bit (e.g. `GDS_NO`, `TSA_SQ_NO`) declares that the CPU design is architecturally free of the vulnerability - it belongs here in `is_cpu_affected()`. A mitigation capability bit (e.g. `VERW_CLEAR`, `MD_CLEAR`) indicates that updated microcode provides a mechanism to work around a vulnerability the CPU *does* have - it belongs in the `check_CVE_YYYY_NNNNN_linux()` function (Phase 2), where it is used to determine whether mitigations are in place. +**JSON output**: If the new CVE introduces new `cap_*` variables in `check_cpu()` (whether immunity bits or mitigation bits), these must also be added to the `_build_json_cpu()` function in `src/libs/250_output_emitters.sh`, inside the `capabilities` sub-object. Use the same name as the shell variable without the `cap_` prefix (e.g. `cap_tsa_sq_no` becomes `"tsa_sq_no"` in JSON), and emit it via `_json_cap`. The per-CVE vulnerability data (affection, status, sysfs) is handled automatically by the existing `_emit_json_full()` function and requires no changes when adding a new CVE. + ### Step 3: Implement the Linux Check The `_linux()` function follows a standard algorithm with four phases: @@ -748,7 +771,11 @@ CVEs that need VMM context should call `check_has_vmm` early in their `_linux()` 3. **Update `dist/README.md`**: Add the CVE in **both** tables — the "Supported CVEs" reference table at the top (CVE link, description, alias) **and** the "Am I at risk?" matrix (with the correct leak/mitigation indicators per boundary). Also add a detailed description paragraph in the `
` section at the bottom. 4. **Build** the monolithic script with `make`. 5. **Test live**: Run the built script and confirm your CVE appears in the output and reports a sensible status. -6. **Test batch JSON**: Run with `--batch json` and verify the CVE appears in the output. +6. **Test batch JSON**: Run with `--batch json` and pipe through `python3 -m json.tool` to verify: + - The output is valid JSON. + - The new CVE appears in the `vulnerabilities` array with correct `cve`, `name`, `aliases`, `cpu_affected`, `status`, `vulnerable`, `info`, `sysfs_status`, and `sysfs_message` fields. + - If new `cap_*` variables were added in `check_cpu()`, they appear in `cpu.capabilities` (see Step 2 JSON note). + - Run with `--batch json-terse` as well to verify backward-compatible output. 7. **Test offline**: Run with `--kernel`/`--config`/`--map` pointing to a kernel image and verify the offline code path reports correctly. 8. **Test `--variant` and `--cve`**: Run with `--variant ` and `--cve CVE-YYYY-NNNNN` separately to confirm both selection methods work and produce the same output. 9. **Lint**: Run `shellcheck` on the monolithic script and fix any warnings. @@ -760,6 +787,7 @@ CVEs that need VMM context should call `check_has_vmm` early in their `_linux()` - **Always handle both live and offline modes** - use `$opt_live` to branch, and print `N/A "not testable in offline mode"` for runtime-only checks when offline. - **Use `explain()`** when reporting VULN to give actionable remediation advice (see "Cross-Cutting Features" above). - **Handle `--paranoid` and `--vmm`** when the CVE has stricter mitigation tiers or VMM-specific aspects (see "Cross-Cutting Features" above). +- **Keep JSON output in sync** - when adding new `cap_*` variables, add them to `_build_json_cpu()` in `src/libs/250_output_emitters.sh` (see Step 2 JSON note above). Per-CVE fields are handled automatically. - **All indentation must use 4 spaces** (CI enforces this via `fmt-check`; the vim modeline `et` enables expandtab). - **Stay POSIX-compatible** - no bashisms, no GNU-only flags in portable code paths. diff --git a/src/libs/002_core_globals.sh b/src/libs/002_core_globals.sh index bbfe58e..469f374 100644 --- a/src/libs/002_core_globals.sh +++ b/src/libs/002_core_globals.sh @@ -43,7 +43,8 @@ show_usage() { so that invoked tools will be prefixed with this (i.e. aarch64-linux-gnu-objdump) --batch text produce machine readable output, this is the default if --batch is specified alone --batch short produce only one line with the vulnerabilities separated by spaces - --batch json produce JSON output formatted for Puppet, Ansible, Chef... + --batch json produce comprehensive JSON output with system, CPU, and vulnerability details + --batch json-terse produce a terse JSON array of per-CVE results (legacy format) --batch nrpe produce machine readable output formatted for NRPE --batch prometheus produce output for consumption by prometheus-node-exporter diff --git a/src/libs/230_util_optparse.sh b/src/libs/230_util_optparse.sh index 501146a..7de9896 100644 --- a/src/libs/230_util_optparse.sh +++ b/src/libs/230_util_optparse.sh @@ -116,7 +116,7 @@ while [ -n "${1:-}" ]; do opt_no_color=1 shift case "$1" in - text | short | nrpe | json | prometheus) + text | short | nrpe | json | json-terse | prometheus) opt_batch_format="$1" shift ;; @@ -124,7 +124,7 @@ while [ -n "${1:-}" ]; do '') ;; # allow nothing at all *) echo "$0: error: unknown batch format '$1'" >&2 - echo "$0: error: --batch expects a format from: text, nrpe, json" >&2 + echo "$0: error: --batch expects a format from: text, nrpe, json, json-terse" >&2 exit 255 ;; esac diff --git a/src/libs/250_output_emitters.sh b/src/libs/250_output_emitters.sh index 5d6b653..0e48929 100644 --- a/src/libs/250_output_emitters.sh +++ b/src/libs/250_output_emitters.sh @@ -1,4 +1,245 @@ # vim: set ts=4 sw=4 sts=4 et: +# --- JSON helper functions --- + +# Escape a string for use in a JSON value (handles backslashes, double quotes, newlines, tabs) +# Args: $1=string +# Prints: escaped string (without surrounding quotes) +_json_escape() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | tr '\n' ' ' +} + +# Convert a shell capability value to a JSON token +# Args: $1=value (1=true, 0=false, -1/empty=null, other string=quoted string) +# Prints: JSON token +_json_cap() { + case "${1:-}" in + 1) printf 'true' ;; + 0) printf 'false' ;; + -1|'') printf 'null' ;; + *) printf '"%s"' "$(_json_escape "$1")" ;; + esac +} + +# Emit a JSON string value or null +# Args: $1=string (empty=null) +# Prints: JSON token ("escaped string" or null) +_json_str() { + if [ -n "${1:-}" ]; then + printf '"%s"' "$(_json_escape "$1")" + else + printf 'null' + fi +} + +# Emit a JSON number value or null +# Args: $1=number (empty=null) +# Prints: JSON token +_json_num() { + if [ -n "${1:-}" ]; then + printf '%s' "$1" + else + printf 'null' + fi +} + +# Emit a JSON boolean value or null +# Args: $1=value (1/0/empty) +# Prints: JSON token +_json_bool() { + case "${1:-}" in + 1) printf 'true' ;; + 0) printf 'false' ;; + *) printf 'null' ;; + esac +} + +# --- JSON section builders (comprehensive format) --- + +# Build the "meta" section of the comprehensive JSON output +# Sets: g_json_meta +# shellcheck disable=SC2034 +_build_json_meta() { + local timestamp mode + timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo "unknown") + if [ "$opt_live" = 1 ]; then + mode="live" + else + mode="offline" + fi + local run_as_root + if [ "$(id -u)" -eq 0 ]; then + run_as_root='true' + else + run_as_root='false' + fi + g_json_meta=$(printf '{"script_version":%s,"format_version":1,"timestamp":%s,"os":%s,"mode":"%s","run_as_root":%s,"reduced_accuracy":%s,"paranoid":%s,"sysfs_only":%s,"no_hw":%s,"extra":%s}' \ + "$(_json_str "$VERSION")" \ + "$(_json_str "$timestamp")" \ + "$(_json_str "$g_os")" \ + "$mode" \ + "$run_as_root" \ + "$(_json_bool "${g_bad_accuracy:-0}")" \ + "$(_json_bool "$opt_paranoid")" \ + "$(_json_bool "$opt_sysfs_only")" \ + "$(_json_bool "$opt_no_hw")" \ + "$(_json_bool "$opt_extra")") +} + +# Build the "system" section of the comprehensive JSON output +# Sets: g_json_system +# shellcheck disable=SC2034 +_build_json_system() { + local kernel_release kernel_version kernel_arch smt_val + if [ "$opt_live" = 1 ]; then + kernel_release=$(uname -r) + kernel_version=$(uname -v) + kernel_arch=$(uname -m) + else + kernel_release='' + kernel_version='' + kernel_arch='' + fi + # SMT detection + is_cpu_smt_enabled + smt_val=$? + case $smt_val in + 0) smt_val='true' ;; + 1) smt_val='false' ;; + *) smt_val='null' ;; + esac + g_json_system=$(printf '{"kernel_release":%s,"kernel_version":%s,"kernel_arch":%s,"kernel_image":%s,"kernel_config":%s,"kernel_version_string":%s,"kernel_cmdline":%s,"cpu_count":%s,"smt_enabled":%s,"hypervisor_host":%s,"hypervisor_host_reason":%s}' \ + "$(_json_str "$kernel_release")" \ + "$(_json_str "$kernel_version")" \ + "$(_json_str "$kernel_arch")" \ + "$(_json_str "${opt_kernel:-}")" \ + "$(_json_str "${opt_config:-}")" \ + "$(_json_str "${g_kernel_version:-}")" \ + "$(_json_str "${g_kernel_cmdline:-}")" \ + "$(_json_num "${g_max_core_id:+$((g_max_core_id + 1))}")" \ + "$smt_val" \ + "$(_json_bool "${g_has_vmm:-}")" \ + "$(_json_str "${g_has_vmm_reason:-}")") +} + +# Build the "cpu" section of the comprehensive JSON output +# Sets: g_json_cpu +# shellcheck disable=SC2034 +_build_json_cpu() { + local cpuid_hex ucode_hex codename caps + if [ -n "${cpu_cpuid:-}" ]; then + cpuid_hex=$(printf '0x%08x' "$cpu_cpuid") + else + cpuid_hex='' + fi + if [ -n "${cpu_ucode:-}" ]; then + ucode_hex=$(printf '0x%x' "$cpu_ucode") + else + ucode_hex='' + fi + codename='' + if is_intel; then + codename=$(get_intel_codename 2>/dev/null || true) + fi + # Build capabilities sub-object + caps=$(printf '{"spec_ctrl":%s,"ibrs":%s,"ibpb":%s,"ibpb_ret":%s,"stibp":%s,"ssbd":%s,"l1d_flush":%s,"md_clear":%s,"arch_capabilities":%s,"rdcl_no":%s,"ibrs_all":%s,"rsba":%s,"l1dflush_no":%s,"ssb_no":%s,"mds_no":%s,"taa_no":%s,"pschange_msc_no":%s,"tsx_ctrl_msr":%s,"tsx_ctrl_rtm_disable":%s,"tsx_ctrl_cpuid_clear":%s,"gds_ctrl":%s,"gds_no":%s,"gds_mitg_dis":%s,"gds_mitg_lock":%s,"rfds_no":%s,"rfds_clear":%s,"its_no":%s,"sbdr_ssdp_no":%s,"fbsdp_no":%s,"psdp_no":%s,"fb_clear":%s,"rtm":%s,"tsx_force_abort":%s,"tsx_force_abort_rtm_disable":%s,"tsx_force_abort_cpuid_clear":%s,"sgx":%s,"srbds":%s,"srbds_on":%s,"amd_ssb_no":%s,"hygon_ssb_no":%s,"ipred":%s,"rrsba":%s,"bhi":%s,"tsa_sq_no":%s,"tsa_l1_no":%s,"verw_clear":%s,"autoibrs":%s,"sbpb":%s,"avx2":%s,"avx512":%s}' \ + "$(_json_cap "${cap_spec_ctrl:-}")" \ + "$(_json_cap "${cap_ibrs:-}")" \ + "$(_json_cap "${cap_ibpb:-}")" \ + "$(_json_cap "${cap_ibpb_ret:-}")" \ + "$(_json_cap "${cap_stibp:-}")" \ + "$(_json_cap "${cap_ssbd:-}")" \ + "$(_json_cap "${cap_l1df:-}")" \ + "$(_json_cap "${cap_md_clear:-}")" \ + "$(_json_cap "${cap_arch_capabilities:-}")" \ + "$(_json_cap "${cap_rdcl_no:-}")" \ + "$(_json_cap "${cap_ibrs_all:-}")" \ + "$(_json_cap "${cap_rsba:-}")" \ + "$(_json_cap "${cap_l1dflush_no:-}")" \ + "$(_json_cap "${cap_ssb_no:-}")" \ + "$(_json_cap "${cap_mds_no:-}")" \ + "$(_json_cap "${cap_taa_no:-}")" \ + "$(_json_cap "${cap_pschange_msc_no:-}")" \ + "$(_json_cap "${cap_tsx_ctrl_msr:-}")" \ + "$(_json_cap "${cap_tsx_ctrl_rtm_disable:-}")" \ + "$(_json_cap "${cap_tsx_ctrl_cpuid_clear:-}")" \ + "$(_json_cap "${cap_gds_ctrl:-}")" \ + "$(_json_cap "${cap_gds_no:-}")" \ + "$(_json_cap "${cap_gds_mitg_dis:-}")" \ + "$(_json_cap "${cap_gds_mitg_lock:-}")" \ + "$(_json_cap "${cap_rfds_no:-}")" \ + "$(_json_cap "${cap_rfds_clear:-}")" \ + "$(_json_cap "${cap_its_no:-}")" \ + "$(_json_cap "${cap_sbdr_ssdp_no:-}")" \ + "$(_json_cap "${cap_fbsdp_no:-}")" \ + "$(_json_cap "${cap_psdp_no:-}")" \ + "$(_json_cap "${cap_fb_clear:-}")" \ + "$(_json_cap "${cap_rtm:-}")" \ + "$(_json_cap "${cap_tsx_force_abort:-}")" \ + "$(_json_cap "${cap_tsx_force_abort_rtm_disable:-}")" \ + "$(_json_cap "${cap_tsx_force_abort_cpuid_clear:-}")" \ + "$(_json_cap "${cap_sgx:-}")" \ + "$(_json_cap "${cap_srbds:-}")" \ + "$(_json_cap "${cap_srbds_on:-}")" \ + "$(_json_cap "${cap_amd_ssb_no:-}")" \ + "$(_json_cap "${cap_hygon_ssb_no:-}")" \ + "$(_json_cap "${cap_ipred:-}")" \ + "$(_json_cap "${cap_rrsba:-}")" \ + "$(_json_cap "${cap_bhi:-}")" \ + "$(_json_cap "${cap_tsa_sq_no:-}")" \ + "$(_json_cap "${cap_tsa_l1_no:-}")" \ + "$(_json_cap "${cap_verw_clear:-}")" \ + "$(_json_cap "${cap_autoibrs:-}")" \ + "$(_json_cap "${cap_sbpb:-}")" \ + "$(_json_cap "${cap_avx2:-}")" \ + "$(_json_cap "${cap_avx512:-}")") + + g_json_cpu=$(printf '{"vendor":%s,"friendly_name":%s,"family":%s,"model":%s,"stepping":%s,"cpuid":%s,"platform_id":%s,"hybrid":%s,"codename":%s,"arm_part_list":%s,"arm_arch_list":%s,"capabilities":%s}' \ + "$(_json_str "${cpu_vendor:-}")" \ + "$(_json_str "${cpu_friendly_name:-}")" \ + "$(_json_num "${cpu_family:-}")" \ + "$(_json_num "${cpu_model:-}")" \ + "$(_json_num "${cpu_stepping:-}")" \ + "$(_json_str "$cpuid_hex")" \ + "$(_json_num "${cpu_platformid:-}")" \ + "$(_json_bool "${cpu_hybrid:-}")" \ + "$(_json_str "$codename")" \ + "$(_json_str "${cpu_part_list:-}")" \ + "$(_json_str "${cpu_arch_list:-}")" \ + "$caps") +} + +# Build the "cpu_microcode" section of the comprehensive JSON output +# Sets: g_json_cpu_microcode +# shellcheck disable=SC2034 +_build_json_cpu_microcode() { + local ucode_uptodate ucode_hex latest_hex blacklisted + if [ -n "${cpu_ucode:-}" ]; then + ucode_hex=$(printf '0x%x' "$cpu_ucode") + else + ucode_hex='' + fi + is_latest_known_ucode + case $? in + 0) ucode_uptodate='true' ;; + 1) ucode_uptodate='false' ;; + *) ucode_uptodate='null' ;; + esac + if is_ucode_blacklisted; then + blacklisted='true' + else + blacklisted='false' + fi + latest_hex="${ret_is_latest_known_ucode_version:-}" + g_json_cpu_microcode=$(printf '{"installed_version":%s,"latest_version":%s,"microcode_up_to_date":%s,"is_blacklisted":%s,"message":%s,"db_source":%s,"db_info":%s}' \ + "$(_json_str "$ucode_hex")" \ + "$(_json_str "$latest_hex")" \ + "$ucode_uptodate" \ + "$blacklisted" \ + "$(_json_str "${ret_is_latest_known_ucode_latest:-}")" \ + "$(_json_str "${g_mcedb_source:-}")" \ + "$(_json_str "${g_mcedb_info:-}")") +} + # --- Format-specific batch emitters --- # Emit a single CVE result as plain text @@ -16,28 +257,62 @@ _emit_short() { g_short_output="${g_short_output}$1 " } -# Append a CVE result as a JSON object to the batch output buffer +# Append a CVE result as a terse JSON object to the batch output buffer # Args: $1=cve $2=aka $3=status(UNK|VULN|OK) $4=description # Sets: g_json_output # Callers: pvulnstatus -_emit_json() { +_emit_json_terse() { local is_vuln esc_name esc_infos case "$3" in UNK) is_vuln="null" ;; VULN) is_vuln="true" ;; OK) is_vuln="false" ;; *) - echo "$0: error: unknown status '$3' passed to _emit_json()" >&2 + echo "$0: error: unknown status '$3' passed to _emit_json_terse()" >&2 exit 255 ;; esac - # escape backslashes and double quotes for valid JSON strings - esc_name=$(printf '%s' "$2" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') - esc_infos=$(printf '%s' "$4" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') + esc_name=$(_json_escape "$2") + esc_infos=$(_json_escape "$4") [ -z "$g_json_output" ] && g_json_output='[' g_json_output="${g_json_output}{\"NAME\":\"$esc_name\",\"CVE\":\"$1\",\"VULNERABLE\":$is_vuln,\"INFOS\":\"$esc_infos\"}," } +# Append a CVE result as a comprehensive JSON object to the batch output buffer +# Args: $1=cve $2=aka $3=status(UNK|VULN|OK) $4=description +# Sets: g_json_vulns +# Callers: pvulnstatus +_emit_json_full() { + local is_vuln esc_name esc_infos aliases cpu_affected sysfs_status sysfs_msg + case "$3" in + UNK) is_vuln="null" ;; + VULN) is_vuln="true" ;; + OK) is_vuln="false" ;; + *) + echo "$0: error: unknown status '$3' passed to _emit_json_full()" >&2 + exit 255 + ;; + esac + esc_name=$(_json_escape "$2") + esc_infos=$(_json_escape "$4") + aliases=$(_cve_registry_field "$1" 4) + + # CPU affection status (cached, cheap) + if is_cpu_affected "$1" 2>/dev/null; then + cpu_affected='true' + else + cpu_affected='false' + fi + + # sysfs status: use the value captured by this CVE's check function, then clear it + # so it doesn't leak into the next CVE that might not call sys_interface_check + sysfs_status="${g_json_cve_sysfs_status:-}" + sysfs_msg="${g_json_cve_sysfs_msg:-}" + + : "${g_json_vulns:=}" + g_json_vulns="${g_json_vulns}{\"cve\":\"$1\",\"name\":\"$esc_name\",\"aliases\":$(_json_str "$aliases"),\"cpu_affected\":$cpu_affected,\"status\":\"$3\",\"vulnerable\":$is_vuln,\"info\":\"$esc_infos\",\"sysfs_status\":$(_json_str "$sysfs_status"),\"sysfs_message\":$(_json_str "$sysfs_msg")}," +} + # Append vulnerable CVE IDs to the NRPE output buffer # Args: $1=cve $2=aka $3=status $4=description # Sets: g_nrpe_vuln @@ -85,7 +360,8 @@ pvulnstatus() { case "$opt_batch_format" in text) _emit_text "$1" "$aka" "$2" "$3" ;; short) _emit_short "$1" "$aka" "$2" "$3" ;; - json) _emit_json "$1" "$aka" "$2" "$3" ;; + json) _emit_json_full "$1" "$aka" "$2" "$3" ;; + json-terse) _emit_json_terse "$1" "$aka" "$2" "$3" ;; nrpe) _emit_nrpe "$1" "$aka" "$2" "$3" ;; prometheus) _emit_prometheus "$1" "$aka" "$2" "$3" ;; *) @@ -93,6 +369,9 @@ pvulnstatus() { exit 255 ;; esac + # reset per-CVE sysfs globals so they don't leak into the next CVE + g_json_cve_sysfs_status='' + g_json_cve_sysfs_msg='' fi _record_result "$1" "$2" diff --git a/src/libs/380_hw_microcode.sh b/src/libs/380_hw_microcode.sh index 9245392..a486f1a 100644 --- a/src/libs/380_hw_microcode.sh +++ b/src/libs/380_hw_microcode.sh @@ -33,11 +33,12 @@ read_inteldb() { } # Check whether the CPU is running the latest known microcode version -# Sets: ret_is_latest_known_ucode_latest +# Sets: ret_is_latest_known_ucode_latest, ret_is_latest_known_ucode_version # Returns: 0=latest, 1=outdated, 2=unknown is_latest_known_ucode() { local brand_prefix tuple pfmask ucode ucode_date parse_cpu_details + ret_is_latest_known_ucode_version='' if [ "$cpu_cpuid" = 0 ]; then ret_is_latest_known_ucode_latest="couldn't get your cpuid" return 2 @@ -64,6 +65,8 @@ is_latest_known_ucode() { ucode_date=$(echo "$tuple" | cut -d, -f5 | sed -E 's=(....)(..)(..)=\1/\2/\3=') pr_debug "is_latest_known_ucode: with cpuid $cpu_cpuid has ucode $cpu_ucode, last known is $ucode from $ucode_date" ret_is_latest_known_ucode_latest=$(printf "latest version is 0x%x dated $ucode_date according to $g_mcedb_info" "$ucode") + # shellcheck disable=SC2034 + ret_is_latest_known_ucode_version=$(printf "0x%x" "$ucode") if [ "$cpu_ucode" -ge "$ucode" ]; then return 0 else diff --git a/src/libs/400_hw_check.sh b/src/libs/400_hw_check.sh index bc5f52d..a6d8b66 100644 --- a/src/libs/400_hw_check.sh +++ b/src/libs/400_hw_check.sh @@ -312,9 +312,14 @@ sys_interface_check() { g_mockme=$(printf "%b\n%b" "$g_mockme" "SMC_MOCK_SYSFS_$(basename "$file")='$ret_sys_interface_check_fullmsg'") fi if [ "$mode" = silent ]; then + # capture sysfs message for JSON even in silent mode + # shellcheck disable=SC2034 + g_json_cve_sysfs_msg="$ret_sys_interface_check_fullmsg" return 0 elif [ "$mode" = quiet ]; then pr_info "* Information from the /sys interface: $ret_sys_interface_check_fullmsg" + # shellcheck disable=SC2034 + g_json_cve_sysfs_msg="$ret_sys_interface_check_fullmsg" return 0 fi pr_info_nol "* Mitigated according to the /sys interface: " @@ -334,6 +339,11 @@ sys_interface_check() { ret_sys_interface_check_status=UNK pstatus yellow UNKNOWN "$ret_sys_interface_check_fullmsg" fi + # capture for JSON full output (read by _emit_json_full via pvulnstatus) + # shellcheck disable=SC2034 + g_json_cve_sysfs_status="$ret_sys_interface_check_status" + # shellcheck disable=SC2034 + g_json_cve_sysfs_msg="$ret_sys_interface_check_fullmsg" pr_debug "sys_interface_check: $file=$msg (re=$regex)" return 0 } diff --git a/src/main.sh b/src/main.sh index 2b405c5..4b99f07 100644 --- a/src/main.sh +++ b/src/main.sh @@ -1,6 +1,12 @@ # vim: set ts=4 sw=4 sts=4 et: check_kernel_info + +# Build JSON meta and system sections early (after kernel info is resolved) +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json" ]; then + _build_json_meta +fi + pr_info if [ "$opt_no_hw" = 0 ] && [ -z "$opt_arch_prefix" ]; then @@ -10,6 +16,15 @@ if [ "$opt_no_hw" = 0 ] && [ -z "$opt_arch_prefix" ]; then pr_info fi +# Build JSON system/cpu/microcode sections (after check_cpu has populated cap_* vars and VMM detection) +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json" ]; then + _build_json_system + if [ "$opt_no_hw" = 0 ] && [ -z "$opt_arch_prefix" ]; then + _build_json_cpu + _build_json_cpu_microcode + fi +fi + # now run the checks the user asked for for cve in $g_supported_cve_list; do if [ "$opt_cve_all" = 1 ] || echo "$opt_cve_list" | grep -qw "$cve"; then @@ -80,10 +95,28 @@ if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "short" ]; then _pr_echo 0 "${g_short_output% }" fi -if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json" ]; then +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json-terse" ]; then _pr_echo 0 "${g_json_output%?}]" fi +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json" ]; then + # Assemble the comprehensive JSON output from pre-built sections + # Inject mocked flag into meta (g_mocked can be set at any point during the run) + g_json_meta="${g_json_meta%\}},\"mocked\":$(_json_bool "${g_mocked:-0}")}" + _json_final='{' + _json_final="${_json_final}\"meta\":${g_json_meta:-null}" + _json_final="${_json_final},\"system\":${g_json_system:-null}" + _json_final="${_json_final},\"cpu\":${g_json_cpu:-null}" + _json_final="${_json_final},\"cpu_microcode\":${g_json_cpu_microcode:-null}" + if [ -n "${g_json_vulns:-}" ]; then + _json_final="${_json_final},\"vulnerabilities\":[${g_json_vulns%,}]" + else + _json_final="${_json_final},\"vulnerabilities\":[]" + fi + _json_final="${_json_final}}" + _pr_echo 0 "$_json_final" +fi + if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "prometheus" ]; then echo "# TYPE specex_vuln_status untyped" echo "# HELP specex_vuln_status Exposure of system to speculative execution vulnerabilities" diff --git a/src/vulns/CVE-2018-3639.sh b/src/vulns/CVE-2018-3639.sh index b0d1bae..7d46421 100644 --- a/src/vulns/CVE-2018-3639.sh +++ b/src/vulns/CVE-2018-3639.sh @@ -121,11 +121,21 @@ check_CVE_2018_3639_linux() { fi else if [ -n "$kernel_ssb" ]; then - pvulnstatus "$cve" VULN "Your CPU doesn't support SSBD" - explain "Your kernel is recent enough to use the CPU microcode features for mitigation, but your CPU microcode doesn't actually provide the necessary features for the kernel to use. The microcode of your CPU hence needs to be upgraded. This is usually done at boot time by your kernel (the upgrade is not persistent across reboots which is why it's done at each boot). If you're using a distro, make sure you are up to date, as microcode updates are usually shipped alongside with the distro kernel. Availability of a microcode update for you CPU model depends on your CPU vendor. You can usually find out online if a microcode update is available for your CPU by searching for your CPUID (indicated in the Hardware Check section)." + if [ "$cpu_vendor" = ARM ] || [ "$cpu_vendor" = CAVIUM ] || [ "$cpu_vendor" = PHYTIUM ]; then + pvulnstatus "$cve" VULN "no SSB mitigation is active on your system" + explain "ARM CPUs mitigate SSB either through a hardware SSBS bit (ARMv8.5+ CPUs) or through firmware support for SMCCC ARCH_WORKAROUND_2. Your kernel reports SSB status but neither mechanism appears to be active. For CPUs predating ARMv8.5 (such as Cortex-A57 or Cortex-A72), check with your board or SoC vendor for a firmware update that provides SMCCC ARCH_WORKAROUND_2 support." + else + pvulnstatus "$cve" VULN "Your CPU doesn't support SSBD" + explain "Your kernel is recent enough to use the CPU microcode features for mitigation, but your CPU microcode doesn't actually provide the necessary features for the kernel to use. The microcode of your CPU hence needs to be upgraded. This is usually done at boot time by your kernel (the upgrade is not persistent across reboots which is why it's done at each boot). If you're using a distro, make sure you are up to date, as microcode updates are usually shipped alongside with the distro kernel. Availability of a microcode update for you CPU model depends on your CPU vendor. You can usually find out online if a microcode update is available for your CPU by searching for your CPUID (indicated in the Hardware Check section)." + fi else - pvulnstatus "$cve" VULN "Neither your CPU nor your kernel support SSBD" - explain "Both your CPU microcode and your kernel are lacking support for mitigation. If you're using a distro kernel, upgrade your distro to get the latest kernel available. Otherwise, recompile the kernel from recent-enough sources. The microcode of your CPU also needs to be upgraded. This is usually done at boot time by your kernel (the upgrade is not persistent across reboots which is why it's done at each boot). If you're using a distro, make sure you are up to date, as microcode updates are usually shipped alongside with the distro kernel. Availability of a microcode update for you CPU model depends on your CPU vendor. You can usually find out online if a microcode update is available for your CPU by searching for your CPUID (indicated in the Hardware Check section)." + if [ "$cpu_vendor" = ARM ] || [ "$cpu_vendor" = CAVIUM ] || [ "$cpu_vendor" = PHYTIUM ]; then + pvulnstatus "$cve" VULN "your kernel and firmware do not support SSB mitigation" + explain "ARM SSB mitigation requires kernel support (CONFIG_ARM64_SSBD) combined with either a hardware SSBS bit (ARMv8.5+ CPUs) or firmware support for SMCCC ARCH_WORKAROUND_2. Ensure you are running a recent kernel compiled with CONFIG_ARM64_SSBD. For CPUs predating ARMv8.5, also check with your board or SoC vendor for a firmware update providing SMCCC ARCH_WORKAROUND_2 support." + else + pvulnstatus "$cve" VULN "Neither your CPU nor your kernel support SSBD" + explain "Both your CPU microcode and your kernel are lacking support for mitigation. If you're using a distro kernel, upgrade your distro to get the latest kernel available. Otherwise, recompile the kernel from recent-enough sources. The microcode of your CPU also needs to be upgraded. This is usually done at boot time by your kernel (the upgrade is not persistent across reboots which is why it's done at each boot). If you're using a distro, make sure you are up to date, as microcode updates are usually shipped alongside with the distro kernel. Availability of a microcode update for you CPU model depends on your CPU vendor. You can usually find out online if a microcode update is available for your CPU by searching for your CPUID (indicated in the Hardware Check section)." + fi fi fi else