From 74a6e181ce388e2ec0d384020cf2ec0e56f189f5 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Mon, 13 Jul 2026 09:15:00 +0200 Subject: [PATCH] feat: collect hardware and device identity --- scripts/lib/hardware_collectors.sh | 61 ++++ scripts/modules/10_hardware.sh | 550 +++++++++++++++++++++++++++++ 2 files changed, 611 insertions(+) create mode 100644 scripts/lib/hardware_collectors.sh diff --git a/scripts/lib/hardware_collectors.sh b/scripts/lib/hardware_collectors.sh new file mode 100644 index 0000000..c1a8705 --- /dev/null +++ b/scripts/lib/hardware_collectors.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +get_cpu_model() { + awk -F: ' + $1 ~ /^model name[[:space:]]*$/ { + value = $2 + sub(/^[[:space:]]*/, "", value) + print value + exit + } + ' /proc/cpuinfo +} + +get_cpu_count() { + getconf _NPROCESSORS_ONLN +} + +get_memory_bytes() { + awk ' + $1 == "MemTotal:" { + print $2 * 1024 + exit + } + ' /proc/meminfo +} + +build_storage_devices_json() { + lsblk \ + --bytes \ + --json \ + --nodeps \ + --output NAME,TYPE,MODEL,SERIAL,SIZE,TRAN | + jq ' + [ + .blockdevices[] + | select(.type == "disk") + | { + name: .name, + model: ( + if .model == null or .model == "" + then null + else (.model | gsub("^[[:space:]]+|[[:space:]]+$"; "")) + end + ), + serial: ( + if .serial == null or .serial == "" + then null + else (.serial | gsub("^[[:space:]]+|[[:space:]]+$"; "")) + end + ), + size_bytes: .size, + transport: ( + if .tran == null or .tran == "" + then null + else .tran + end + ) + } + ] + ' +} diff --git a/scripts/modules/10_hardware.sh b/scripts/modules/10_hardware.sh index e69de29..56e9183 100755 --- a/scripts/modules/10_hardware.sh +++ b/scripts/modules/10_hardware.sh @@ -0,0 +1,550 @@ +#!/usr/bin/env bash + +# Tuxflotte Installer +# Phase 2 – Hardware- und Geräteidentität +# +# Ermittelt: +# - DMI-/SMBIOS-Daten +# - System-UUID und Seriennummer +# - CPU-Architektur +# - physische Netzwerkinterfaces und MAC-Adressen +# - TPM-Verfügbarkeit +# - UEFI- und Secure-Boot-Status +# +# Ausgabe: +# /run/tuxflotte/hardware/hardware.json + +set -Eeuo pipefail + +SCRIPT_DIR="$( + cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && + pwd +)" +readonly SCRIPT_DIR + +readonly COLLECTORS_FILE="${SCRIPT_DIR}/../lib/hardware_collectors.sh" + +if [[ ! -r "${COLLECTORS_FILE}" ]]; then + printf '[%s] FEHLER: Collector-Library nicht gefunden: %s\n' \ + "${0##*/}" \ + "${COLLECTORS_FILE}" >&2 + exit 1 +fi + +# shellcheck source=../lib/hardware_collectors.sh +source "${COLLECTORS_FILE}" + +readonly SCRIPT_NAME="${0##*/}" + +readonly RUNTIME_DIR="/run/tuxflotte/hardware" +readonly HARDWARE_FILE="${RUNTIME_DIR}/hardware.json" + +readonly SYS_DMI_DIR="/sys/class/dmi/id" +readonly SYS_NET_DIR="/sys/class/net" +readonly EFI_VARS_DIR="/sys/firmware/efi/efivars" + +log() { + printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2 +} + +warn() { + printf '[%s] WARNUNG: %s\n' "${SCRIPT_NAME}" "$*" >&2 +} + +fatal() { + printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2 + exit 1 +} + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + fatal "Das Hardwaremodul muss als root ausgeführt werden." + fi +} + +require_command() { + local command_name="$1" + + command -v "${command_name}" >/dev/null 2>&1 || + fatal "Benötigtes Programm nicht gefunden: ${command_name}" +} + +prepare_runtime_directory() { + install -d \ + --mode=0700 \ + --owner=root \ + --group=root \ + "${RUNTIME_DIR}" + + rm -f -- "${HARDWARE_FILE}" +} + +read_trimmed_file() { + local file="$1" + local value + + if [[ ! -r "${file}" ]]; then + return 0 + fi + + value="$(tr -d '\000' <"${file}")" + + value="$( + printf '%s' "${value}" | + sed \ + -e 's/^[[:space:]]*//' \ + -e 's/[[:space:]]*$//' + )" + + printf '%s' "${value}" +} + +read_dmi_value() { + local name="$1" + + read_trimmed_file "${SYS_DMI_DIR}/${name}" +} + +normalize_uuid() { + local value="$1" + + value="${value,,}" + + case "${value}" in + ""|\ + "none"|\ + "not specified"|\ + "to be filled by o.e.m."|\ + "00000000-0000-0000-0000-000000000000"|\ + "ffffffff-ffff-ffff-ffff-ffffffffffff") + return 0 + ;; + esac + + printf '%s' "${value}" +} + +normalize_serial() { + local value="$1" + local normalized + + normalized="${value,,}" + + case "${normalized}" in + ""|\ + "none"|\ + "unknown"|\ + "not specified"|\ + "default string"|\ + "system serial number"|\ + "to be filled by o.e.m.") + return 0 + ;; + esac + + printf '%s' "${value}" +} + +get_machine_id() { + local candidate + + for candidate in \ + /etc/machine-id \ + /var/lib/dbus/machine-id + do + if [[ -r "${candidate}" ]]; then + read_trimmed_file "${candidate}" + return 0 + fi + done +} + +get_architecture() { + uname -m +} + +get_boot_mode() { + if [[ -d /sys/firmware/efi ]]; then + printf 'uefi' + else + printf 'bios' + fi +} + +get_secure_boot_state() { + local secure_boot_file + local value + + if [[ ! -d /sys/firmware/efi ]]; then + printf 'unsupported' + return 0 + fi + + secure_boot_file="$( + find "${EFI_VARS_DIR}" \ + -maxdepth 1 \ + -type f \ + -name 'SecureBoot-*' \ + -print \ + -quit 2>/dev/null || true + )" + + if [[ -z "${secure_boot_file}" || ! -r "${secure_boot_file}" ]]; then + printf 'unknown' + return 0 + fi + + value="$( + od \ + --address-radix=n \ + --format=u1 \ + --skip-bytes=4 \ + --read-bytes=1 \ + "${secure_boot_file}" 2>/dev/null | + tr -d '[:space:]' + )" + + case "${value}" in + 1) + printf 'enabled' + ;; + 0) + printf 'disabled' + ;; + *) + printf 'unknown' + ;; + esac +} + +get_tpm_version() { + if [[ ! -e /dev/tpm0 && ! -e /dev/tpmrm0 ]]; then + printf 'none' + return 0 + fi + + if [[ -r /sys/class/tpm/tpm0/tpm_version_major ]]; then + read_trimmed_file /sys/class/tpm/tpm0/tpm_version_major + return 0 + fi + + if [[ -r /sys/class/tpm/tpm0/device/description ]]; then + local description + + description="$( + read_trimmed_file /sys/class/tpm/tpm0/device/description + )" + + case "${description}" in + *2.0*) + printf '2' + ;; + *1.2*) + printf '1.2' + ;; + *) + printf 'unknown' + ;; + esac + + return 0 + fi + + printf 'unknown' +} + +interface_is_physical() { + local interface="$1" + + [[ "${interface}" != "lo" ]] || return 1 + [[ -e "${SYS_NET_DIR}/${interface}/device" ]] || return 1 + [[ -r "${SYS_NET_DIR}/${interface}/address" ]] || return 1 +} + +get_interface_type() { + local interface="$1" + + if [[ -d "${SYS_NET_DIR}/${interface}/wireless" ]]; then + printf 'wifi' + else + printf 'ethernet' + fi +} + +build_network_interfaces_json() { + local interface + local mac + local type + local -a interfaces=() + + for interface_path in "${SYS_NET_DIR}"/*; do + [[ -e "${interface_path}" ]] || continue + + interface="${interface_path##*/}" + + interface_is_physical "${interface}" || continue + + mac="$(read_trimmed_file "${interface_path}/address")" + type="$(get_interface_type "${interface}")" + + [[ -n "${mac}" ]] || continue + + interfaces+=("$( + jq \ + --null-input \ + --arg name "${interface}" \ + --arg type "${type}" \ + --arg mac "${mac,,}" \ + '{ + name: $name, + type: $type, + mac: $mac + }' + )") + done + + if [[ "${#interfaces[@]}" -eq 0 ]]; then + printf '[]' + return 0 + fi + + printf '%s\n' "${interfaces[@]}" | + jq --slurp 'sort_by(.type, .name)' +} + +build_device_fingerprint() { + local system_uuid="$1" + local system_serial="$2" + local board_serial="$3" + local interfaces_json="$4" + local identity_material + local mac_addresses + + mac_addresses="$( + jq \ + --raw-output \ + '.[].mac // empty' \ + <<<"${interfaces_json}" | + tr '[:upper:]' '[:lower:]' | + sort -u | + paste -sd ',' - + )" + + identity_material="$( + printf 'system_uuid=%s\n' "${system_uuid,,}" + printf 'system_serial=%s\n' "${system_serial,,}" + printf 'board_serial=%s\n' "${board_serial,,}" + printf 'mac_addresses=%s\n' "${mac_addresses}" + )" + + printf '%s' "${identity_material}" | + sha256sum | + awk '{ print $1 }' +} + +build_hardware_json() { + local system_uuid + local system_serial + local machine_id + local manufacturer + local product_name + local product_version + local board_vendor + local board_name + local board_serial + local bios_vendor + local bios_version + local architecture + local boot_mode + local secure_boot + local tpm_version + local interfaces_json + local device_fingerprint + local cpu_model + local cpu_count + local memory_bytes + local storage_devices_json + + system_uuid="$(normalize_uuid "$(read_dmi_value product_uuid)")" + system_serial="$(normalize_serial "$(read_dmi_value product_serial)")" + machine_id="$(get_machine_id)" + + manufacturer="$(read_dmi_value sys_vendor)" + product_name="$(read_dmi_value product_name)" + product_version="$(read_dmi_value product_version)" + + board_vendor="$(read_dmi_value board_vendor)" + board_name="$(read_dmi_value board_name)" + board_serial="$(normalize_serial "$(read_dmi_value board_serial)")" + + bios_vendor="$(read_dmi_value bios_vendor)" + bios_version="$(read_dmi_value bios_version)" + + architecture="$(get_architecture)" + boot_mode="$(get_boot_mode)" + secure_boot="$(get_secure_boot_state)" + tpm_version="$(get_tpm_version)" + cpu_model="$(get_cpu_model)" + cpu_count="$(get_cpu_count)" + memory_bytes="$(get_memory_bytes)" + + interfaces_json="$(build_network_interfaces_json)" + storage_devices_json="$(build_storage_devices_json)" + device_fingerprint="$( + build_device_fingerprint \ + "${system_uuid}" \ + "${system_serial}" \ + "${board_serial}" \ + "${interfaces_json}" + )" + + jq \ + --null-input \ + --arg schema_version "1" \ + --arg device_fingerprint "${device_fingerprint}" \ + --arg system_uuid "${system_uuid}" \ + --arg system_serial "${system_serial}" \ + --arg machine_id "${machine_id}" \ + --arg manufacturer "${manufacturer}" \ + --arg product_name "${product_name}" \ + --arg product_version "${product_version}" \ + --arg board_vendor "${board_vendor}" \ + --arg board_name "${board_name}" \ + --arg board_serial "${board_serial}" \ + --arg bios_vendor "${bios_vendor}" \ + --arg bios_version "${bios_version}" \ + --arg architecture "${architecture}" \ + --arg boot_mode "${boot_mode}" \ + --arg secure_boot "${secure_boot}" \ + --arg tpm_version "${tpm_version}" \ + --arg cpu_model "${cpu_model}" \ + --argjson cpu_count "${cpu_count}" \ + --argjson memory_bytes "${memory_bytes}" \ + --argjson network_interfaces "${interfaces_json}" \ + --argjson storage_devices "${storage_devices_json}" \ + '{ + schema_version: ($schema_version | tonumber), + + identity: { + device_fingerprint: $device_fingerprint, + system_uuid: ( + if $system_uuid == "" then null else $system_uuid end + ), + system_serial: ( + if $system_serial == "" then null else $system_serial end + ), + board_serial: ( + if $board_serial == "" then null else $board_serial end + ), + machine_id: ( + if $machine_id == "" then null else $machine_id end + ) + }, + + system: { + manufacturer: ( + if $manufacturer == "" then null else $manufacturer end + ), + product_name: ( + if $product_name == "" then null else $product_name end + ), + product_version: ( + if $product_version == "" then null else $product_version end + ), + architecture: $architecture, + cpu: { + model: ( + if $cpu_model == "" then null else $cpu_model end + ), + logical_count: $cpu_count + }, + memory_bytes: $memory_bytes, + }, + + mainboard: { + vendor: ( + if $board_vendor == "" then null else $board_vendor end + ), + name: ( + if $board_name == "" then null else $board_name end + ) + }, + + firmware: { + bios_vendor: ( + if $bios_vendor == "" then null else $bios_vendor end + ), + bios_version: ( + if $bios_version == "" then null else $bios_version end + ), + boot_mode: $boot_mode, + secure_boot: $secure_boot + }, + + security: { + tpm_version: $tpm_version + }, + + network_interfaces: $network_interfaces, + storage_devices: $storage_devices + }' +} + +validate_hardware_identity() { + local uuid + local serial + local board_serial + local mac_count + + uuid="$(jq -r '.identity.system_uuid // empty' "${HARDWARE_FILE}")" + serial="$(jq -r '.identity.system_serial // empty' "${HARDWARE_FILE}")" + board_serial="$(jq -r '.identity.board_serial // empty' "${HARDWARE_FILE}")" + mac_count="$(jq '.network_interfaces | length' "${HARDWARE_FILE}")" + + if [[ -z "${uuid}" && + -z "${serial}" && + -z "${board_serial}" && + "${mac_count}" -eq 0 ]]; then + fatal "Es konnte kein stabiles Hardwaremerkmal ermittelt werden." + fi + + if [[ -z "${uuid}" ]]; then + warn "Das Gerät stellt keine verwertbare System-UUID bereit." + fi + + if [[ -z "${serial}" ]]; then + warn "Das Gerät stellt keine verwertbare Systemseriennummer bereit." + fi +} + +main() { + require_root + require_command jq + require_command uname + require_command sed + require_command find + require_command od + require_command sha256sum + require_command sort + require_command paste + require_command tr + require_command awk + + prepare_runtime_directory + + log "Ermittle Hardware- und Geräteidentität." + + umask 077 + build_hardware_json >"${HARDWARE_FILE}" + + chmod 0600 "${HARDWARE_FILE}" + + jq --exit-status . "${HARDWARE_FILE}" >/dev/null || + fatal "Die erzeugte Hardwaredatei enthält kein gültiges JSON." + + validate_hardware_identity + + log "Hardwareinformationen wurden unter ${HARDWARE_FILE} gespeichert." +} + +main "$@"