diff --git a/src/libs/002_core_globals.sh b/src/libs/002_core_globals.sh index 469f374..7aa5d8a 100644 --- a/src/libs/002_core_globals.sh +++ b/src/libs/002_core_globals.sh @@ -46,7 +46,8 @@ show_usage() { --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 + --batch prometheus produce Prometheus metrics (smc_* schema, recommended) + --batch prometheus-legacy produce legacy Prometheus output (specex_vuln_status, deprecated) --variant VARIANT specify which variant you'd like to check, by default all variants are checked. can be used multiple times (e.g. --variant 3a --variant l1tf) @@ -138,6 +139,12 @@ opt_intel_db=1 g_critical=0 g_unknown=0 g_nrpe_vuln='' +g_smc_vuln_output='' +g_smc_ok_count=0 +g_smc_vuln_count=0 +g_smc_unk_count=0 +g_smc_system_info_line='' +g_smc_cpu_info_line='' # CVE Registry: single source of truth for all CVE metadata. # Fields: cve_id|json_key_name|affected_var_suffix|complete_name_and_aliases diff --git a/src/libs/230_util_optparse.sh b/src/libs/230_util_optparse.sh index 7de9896..b58fef6 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 | json-terse | prometheus) + text | short | nrpe | json | json-terse | prometheus | prometheus-legacy) 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, json-terse" >&2 + echo "$0: error: --batch expects a format from: text, short, nrpe, json, json-terse, prometheus, prometheus-legacy" >&2 exit 255 ;; esac diff --git a/src/libs/250_output_emitters.sh b/src/libs/250_output_emitters.sh index 0e48929..02c3507 100644 --- a/src/libs/250_output_emitters.sh +++ b/src/libs/250_output_emitters.sh @@ -8,6 +8,13 @@ _json_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | tr '\n' ' ' } +# Escape a string for use as a Prometheus label value (handles backslashes, double quotes, newlines) +# Args: $1=string +# Prints: escaped string (without surrounding quotes) +_prom_escape() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/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 @@ -321,17 +328,124 @@ _emit_nrpe() { [ "$3" = VULN ] && g_nrpe_vuln="$g_nrpe_vuln $1" } -# Append a CVE result as a Prometheus metric to the batch output buffer +# Append a CVE result as a legacy Prometheus metric to the batch output buffer # Args: $1=cve $2=aka $3=status $4=description # Sets: g_prometheus_output # Callers: pvulnstatus -_emit_prometheus() { +_emit_prometheus_legacy() { local esc_info # escape backslashes and double quotes for Prometheus label values esc_info=$(printf '%s' "$4" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g') g_prometheus_output="${g_prometheus_output:+$g_prometheus_output\n}specex_vuln_status{name=\"$2\",cve=\"$1\",status=\"$3\",info=\"$esc_info\"} 1" } +# Append a CVE result as a Prometheus gauge to the new-format batch output buffer +# Status is encoded numerically: 0=not_vulnerable, 1=vulnerable, 2=unknown +# Args: $1=cve $2=aka $3=status(UNK|VULN|OK) $4=description +# Sets: g_smc_vuln_output, g_smc_ok_count, g_smc_vuln_count, g_smc_unk_count +# Callers: pvulnstatus +_emit_prometheus() { + local numeric_status cpu_affected full_name esc_name + case "$3" in + OK) numeric_status=0 ; g_smc_ok_count=$((g_smc_ok_count + 1)) ;; + VULN) numeric_status=1 ; g_smc_vuln_count=$((g_smc_vuln_count + 1)) ;; + UNK) numeric_status=2 ; g_smc_unk_count=$((g_smc_unk_count + 1)) ;; + *) + echo "$0: error: unknown status '$3' passed to _emit_prometheus()" >&2 + exit 255 + ;; + esac + if is_cpu_affected "$1" 2>/dev/null; then + cpu_affected='true' + else + cpu_affected='false' + fi + # use the complete CVE name (field 4) rather than the short aka key (field 2) + full_name=$(_cve_registry_field "$1" 4) + esc_name=$(_prom_escape "$full_name") + g_smc_vuln_output="${g_smc_vuln_output:+$g_smc_vuln_output\n}smc_vulnerability_status{cve=\"$1\",name=\"$esc_name\",cpu_affected=\"$cpu_affected\"} $numeric_status" +} + +# Build the smc_system_info Prometheus metric line +# Sets: g_smc_system_info_line +# Callers: src/main.sh (after check_cpu / check_cpu_vulnerabilities) +# shellcheck disable=SC2034 +_build_prometheus_system_info() { + local kernel_release kernel_arch hypervisor_host sys_labels + if [ "$opt_live" = 1 ]; then + kernel_release=$(uname -r 2>/dev/null || true) + kernel_arch=$(uname -m 2>/dev/null || true) + else + kernel_release='' + kernel_arch='' + fi + case "${g_has_vmm:-}" in + 1) hypervisor_host='true' ;; + 0) hypervisor_host='false' ;; + *) hypervisor_host='' ;; + esac + sys_labels='' + [ -n "$kernel_release" ] && sys_labels="${sys_labels:+$sys_labels,}kernel_release=\"$(_prom_escape "$kernel_release")\"" + [ -n "$kernel_arch" ] && sys_labels="${sys_labels:+$sys_labels,}kernel_arch=\"$(_prom_escape "$kernel_arch")\"" + [ -n "$hypervisor_host" ] && sys_labels="${sys_labels:+$sys_labels,}hypervisor_host=\"$hypervisor_host\"" + [ -n "$sys_labels" ] && g_smc_system_info_line="smc_system_info{$sys_labels} 1" +} + +# Build the smc_cpu_info Prometheus metric line +# Sets: g_smc_cpu_info_line +# Callers: src/main.sh (after check_cpu / check_cpu_vulnerabilities) +# shellcheck disable=SC2034 +_build_prometheus_cpu_info() { + local cpuid_hex ucode_hex ucode_latest_hex ucode_uptodate ucode_blacklisted codename smt_val cpu_labels + 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 + is_latest_known_ucode + case $? in + 0) ucode_uptodate='true' ;; + 1) ucode_uptodate='false' ;; + *) ucode_uptodate='' ;; + esac + ucode_latest_hex="${ret_is_latest_known_ucode_version:-}" + if is_ucode_blacklisted; then + ucode_blacklisted='true' + else + ucode_blacklisted='false' + fi + codename='' + if is_intel; then + codename=$(get_intel_codename 2>/dev/null || true) + fi + is_cpu_smt_enabled + case $? in + 0) smt_val='true' ;; + 1) smt_val='false' ;; + *) smt_val='' ;; + esac + cpu_labels='' + [ -n "${cpu_vendor:-}" ] && cpu_labels="${cpu_labels:+$cpu_labels,}vendor=\"$(_prom_escape "$cpu_vendor")\"" + [ -n "${cpu_friendly_name:-}" ] && cpu_labels="${cpu_labels:+$cpu_labels,}model=\"$(_prom_escape "$cpu_friendly_name")\"" + [ -n "${cpu_family:-}" ] && cpu_labels="${cpu_labels:+$cpu_labels,}family=\"$cpu_family\"" + [ -n "${cpu_model:-}" ] && cpu_labels="${cpu_labels:+$cpu_labels,}model_id=\"$cpu_model\"" + [ -n "${cpu_stepping:-}" ] && cpu_labels="${cpu_labels:+$cpu_labels,}stepping=\"$cpu_stepping\"" + [ -n "$cpuid_hex" ] && cpu_labels="${cpu_labels:+$cpu_labels,}cpuid=\"$cpuid_hex\"" + [ -n "$codename" ] && cpu_labels="${cpu_labels:+$cpu_labels,}codename=\"$(_prom_escape "$codename")\"" + [ -n "$smt_val" ] && cpu_labels="${cpu_labels:+$cpu_labels,}smt=\"$smt_val\"" + [ -n "$ucode_hex" ] && cpu_labels="${cpu_labels:+$cpu_labels,}microcode=\"$ucode_hex\"" + [ -n "$ucode_latest_hex" ] && cpu_labels="${cpu_labels:+$cpu_labels,}microcode_latest=\"$ucode_latest_hex\"" + [ -n "$ucode_uptodate" ] && cpu_labels="${cpu_labels:+$cpu_labels,}microcode_up_to_date=\"$ucode_uptodate\"" + # always emit microcode_blacklisted when we have microcode info (it's a boolean, never omit) + [ -n "$ucode_hex" ] && cpu_labels="${cpu_labels:+$cpu_labels,}microcode_blacklisted=\"$ucode_blacklisted\"" + [ -n "$cpu_labels" ] && g_smc_cpu_info_line="smc_cpu_info{$cpu_labels} 1" +} + # Update global state used to determine the program exit code # Args: $1=cve $2=status(UNK|VULN|OK) # Sets: g_unknown, g_critical @@ -364,6 +478,7 @@ pvulnstatus() { json-terse) _emit_json_terse "$1" "$aka" "$2" "$3" ;; nrpe) _emit_nrpe "$1" "$aka" "$2" "$3" ;; prometheus) _emit_prometheus "$1" "$aka" "$2" "$3" ;; + prometheus-legacy) _emit_prometheus_legacy "$1" "$aka" "$2" "$3" ;; *) echo "$0: error: invalid batch format '$opt_batch_format' specified" >&2 exit 255 diff --git a/src/main.sh b/src/main.sh index 4b99f07..ca3dd09 100644 --- a/src/main.sh +++ b/src/main.sh @@ -25,6 +25,14 @@ if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json" ]; then fi fi +# Build Prometheus info metric lines (same timing requirement as JSON builders above) +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "prometheus" ]; then + _build_prometheus_system_info + if [ "$opt_no_hw" = 0 ] && [ -z "$opt_arch_prefix" ]; then + _build_prometheus_cpu_info + 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 @@ -117,12 +125,59 @@ if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json" ]; then _pr_echo 0 "$_json_final" fi -if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "prometheus" ]; then +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "prometheus-legacy" ]; then echo "# TYPE specex_vuln_status untyped" echo "# HELP specex_vuln_status Exposure of system to speculative execution vulnerabilities" printf "%b\n" "$g_prometheus_output" fi +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "prometheus" ]; then + prom_run_as_root='false' + [ "$(id -u)" -eq 0 ] && prom_run_as_root='true' + prom_mode='offline' + [ "$opt_live" = 1 ] && prom_mode='live' + prom_paranoid='false' + [ "$opt_paranoid" = 1 ] && prom_paranoid='true' + prom_sysfs_only='false' + [ "$opt_sysfs_only" = 1 ] && prom_sysfs_only='true' + prom_reduced_accuracy='false' + [ "${g_bad_accuracy:-0}" = 1 ] && prom_reduced_accuracy='true' + prom_mocked='false' + [ "${g_mocked:-0}" = 1 ] && prom_mocked='true' + echo "# HELP smc_build_info spectre-meltdown-checker script metadata (always 1)" + echo "# TYPE smc_build_info gauge" + printf 'smc_build_info{version="%s",mode="%s",run_as_root="%s",paranoid="%s",sysfs_only="%s",reduced_accuracy="%s",mocked="%s"} 1\n' \ + "$(_prom_escape "$VERSION")" \ + "$prom_mode" \ + "$prom_run_as_root" \ + "$prom_paranoid" \ + "$prom_sysfs_only" \ + "$prom_reduced_accuracy" \ + "$prom_mocked" + if [ -n "${g_smc_system_info_line:-}" ]; then + echo "# HELP smc_system_info Operating system and kernel metadata (always 1)" + echo "# TYPE smc_system_info gauge" + echo "$g_smc_system_info_line" + fi + if [ -n "${g_smc_cpu_info_line:-}" ]; then + echo "# HELP smc_cpu_info CPU hardware and microcode metadata (always 1)" + echo "# TYPE smc_cpu_info gauge" + echo "$g_smc_cpu_info_line" + fi + echo "# HELP smc_vulnerability_status Vulnerability check result per CVE: 0=not_vulnerable, 1=vulnerable, 2=unknown" + echo "# TYPE smc_vulnerability_status gauge" + printf "%b\n" "$g_smc_vuln_output" + echo "# HELP smc_vulnerable_count Number of CVEs with vulnerable status" + echo "# TYPE smc_vulnerable_count gauge" + echo "smc_vulnerable_count $g_smc_vuln_count" + echo "# HELP smc_unknown_count Number of CVEs with unknown status" + echo "# TYPE smc_unknown_count gauge" + echo "smc_unknown_count $g_smc_unk_count" + echo "# HELP smc_last_scan_timestamp_seconds Unix timestamp when this scan completed" + echo "# TYPE smc_last_scan_timestamp_seconds gauge" + echo "smc_last_scan_timestamp_seconds $(date +%s 2>/dev/null || echo 0)" +fi + # exit with the proper exit code [ "$g_critical" = 1 ] && exit 2 # critical [ "$g_unknown" = 1 ] && exit 3 # unknown