#!/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 "$@"