#!/bin/bash # KNEL-Football Automated ISO Validation Harness # Boots ISO in QEMU VM with serial console, runs automated checks # Reference: PRD FR-001 through FR-012 # Copyright © 2026 Known Element Enterprises LLC set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR readonly ISO_PATH="${SCRIPT_DIR}/output/knel-football-secure.iso" readonly VM_DISK="${SCRIPT_DIR}/tmp/validation-vm.qcow2" readonly SERIAL_LOG="${SCRIPT_DIR}/tmp/validation-serial.log" readonly SCREENSHOT_DIR="${SCRIPT_DIR}/tmp/validation-screenshots" readonly TIMEOUT_BOOT=120 # shellcheck disable=SC2034 readonly VALIDATION_USER="football" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' pass_count=0 fail_count=0 skip_count=0 log_pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((pass_count++)); } log_fail() { echo -e "${RED}[FAIL]${NC} $1"; ((fail_count++)); } log_skip() { echo -e "${YELLOW}[SKIP]${NC} $1"; ((skip_count++)); } log_info() { echo -e "[INFO] $1"; } cleanup() { log_info "Cleaning up VM..." if [ -n "${QEMU_PID:-}" ]; then kill "$QEMU_PID" 2>/dev/null || true wait "$QEMU_PID" 2>/dev/null || true fi rm -f "$VM_DISK" } trap cleanup EXIT # ============================================================================= # Phase 0: Pre-flight checks # ============================================================================= phase0_preflight() { echo "" echo "==========================================" echo " Phase 0: Pre-flight Checks" echo "==========================================" if [ ! -f "$ISO_PATH" ]; then log_fail "ISO not found at $ISO_PATH" return 1 fi log_pass "ISO exists: $(du -h "$ISO_PATH" | cut -f1)" if [ -f "${ISO_PATH}.sha256" ]; then log_info "Verifying SHA256 checksum..." if sha256sum -c "${ISO_PATH}.sha256" 2>/dev/null; then log_pass "SHA256 checksum valid" else log_fail "SHA256 checksum INVALID" fi else log_skip "No SHA256 checksum file" fi if [ -f "${ISO_PATH}.md5" ]; then if md5sum -c "${ISO_PATH}.md5" 2>/dev/null; then log_pass "MD5 checksum valid" else log_fail "MD5 checksum INVALID" fi fi # Check for QEMU if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then log_fail "qemu-system-x86_64 not found" return 1 fi log_pass "QEMU available" # Check for OVMF local ovmf_code="" for f in /usr/share/OVMF/OVMF_CODE_4M.secboot.fd /usr/share/OVMF/OVMF_CODE_4M.fd /usr/share/qemu/OVMF_CODE.fd; do if [ -f "$f" ]; then ovmf_code="$f" break fi done if [ -n "$ovmf_code" ]; then log_pass "OVMF firmware: $ovmf_code" else log_fail "No OVMF firmware found" return 1 fi # Create disk image rm -f "$VM_DISK" qemu-img create -f qcow2 "$VM_DISK" 10G log_pass "VM disk created" # Create screenshot dir mkdir -p "$SCREENSHOT_DIR" } # ============================================================================= # Phase 1: Static ISO analysis (no boot needed) # ============================================================================= phase1_static_analysis() { echo "" echo "==========================================" echo " Phase 1: Static ISO Analysis" echo "==========================================" local iso_size iso_size=$(stat -f%z "$ISO_PATH" 2>/dev/null || stat -c%s "$ISO_PATH" 2>/dev/null || echo 0) # Check ISO size is reasonable (200MB - 2GB) if [ "$iso_size" -gt 200000000 ] && [ "$iso_size" -lt 2500000000 ]; then log_pass "ISO size reasonable: $(echo "scale=0; $iso_size / 1048576" | bc)MB" else log_fail "ISO size unusual: $iso_size bytes" fi # Check ISO is a valid ISO9660 image if file "$ISO_PATH" | grep -qi "ISO 9660\|DOS/MBR\|bootable"; then log_pass "ISO is valid bootable image" elif command -v isoinfo >/dev/null 2>&1 && isoinfo -d -i "$ISO_PATH" >/dev/null 2>&1; then log_pass "ISO is valid ISO9660" else log_fail "ISO does not appear to be a valid bootable image" fi # Check ISO has EFI boot capability if command -v isoinfo >/dev/null 2>&1; then if isoinfo -l -i "$ISO_PATH" 2>/dev/null | grep -qi "EFI\|BOOT"; then log_pass "ISO contains EFI boot files" else log_fail "ISO missing EFI boot files" fi else log_skip "isoinfo not available for EFI check" fi # Check for Debian installer files in ISO if command -v isoinfo >/dev/null 2>&1; then if isoinfo -l -i "$ISO_PATH" 2>/dev/null | grep -qi "install\|d-i\|debian"; then log_pass "ISO contains Debian installer" else log_fail "ISO missing Debian installer" fi else log_skip "isoinfo not available for installer check" fi # Verify ISO contains config hooks (by mounting) local mount_point="${SCRIPT_DIR}/tmp/iso-mount" mkdir -p "$mount_point" if mount -o loop,ro "$ISO_PATH" "$mount_point" 2>/dev/null; then log_pass "ISO mounts successfully" # Check for pool directory (live-build structure) if [ -d "$mount_point/pool" ] || [ -d "$mount_point/dists" ]; then log_pass "ISO has Debian repository structure" fi # Check for bootloader if [ -d "$mount_point/boot" ] || [ -f "$mount_point/boot/grub/grub.cfg" ] || \ [ -f "$mount_point/EFI/BOOT/BOOTX64.EFI" ] || [ -d "$mount_point/EFI" ]; then log_pass "ISO has bootloader" else log_fail "ISO missing bootloader" fi umount "$mount_point" 2>/dev/null || true else log_skip "Cannot mount ISO (needs root or fuse)" fi } # ============================================================================= # Phase 2: Boot test in QEMU # ============================================================================= phase2_boot_test() { echo "" echo "==========================================" echo " Phase 2: QEMU Boot Test" echo "==========================================" local ovmf_code="" local ovmf_vars="" for f in /usr/share/OVMF/OVMF_CODE_4M.secboot.fd /usr/share/OVMF/OVMF_CODE_4M.fd; do if [ -f "$f" ]; then ovmf_code="$f" ovmf_vars="/usr/share/OVMF/OVMF_VARS_4M.fd" break fi done # Copy OVMF vars for this VM (writable copy) local vm_vars="${SCRIPT_DIR}/tmp/validation-ovmf-vars.fd" cp "$ovmf_vars" "$vm_vars" # Boot QEMU with serial console log_info "Starting QEMU VM..." rm -f "$SERIAL_LOG" qemu-system-x86_64 \ -machine q35,accel=kvm \ -cpu host \ -m 2048 \ -smp 2 \ -drive if=pflash,format=raw,readonly=on,file="$ovmf_code" \ -drive if=pflash,format=raw,file="$vm_vars" \ -drive file="$VM_DISK",format=qcow2,if=virtio \ -cdrom "$ISO_PATH" \ -boot d \ -net nic -net user \ -serial file:"$SERIAL_LOG" \ -display none \ -no-reboot \ &>/dev/null & QEMU_PID=$! log_info "QEMU PID: $QEMU_PID" log_info "Waiting for boot (up to ${TIMEOUT_BOOT}s)..." # Wait for boot activity on serial console local elapsed=0 local booted=false while [ $elapsed -lt $TIMEOUT_BOOT ]; do if [ -f "$SERIAL_LOG" ] && [ -s "$SERIAL_LOG" ]; then # Check for signs of successful boot if grep -qi "login\|GRUB\|boot\|Linux version\|Debian GNU" "$SERIAL_LOG" 2>/dev/null; then booted=true break fi fi sleep 5 elapsed=$((elapsed + 5)) echo -n "." done echo "" if $booted; then log_pass "VM booted within ${elapsed}s" # Check what booted if grep -qi "GRUB\|GNU GRUB" "$SERIAL_LOG" 2>/dev/null; then log_pass "GRUB bootloader loaded" fi if grep -qi "Linux version\|Debian GNU" "$SERIAL_LOG" 2>/dev/null; then log_pass "Linux kernel loaded" fi if grep -qi "debian installer\|Install\|d-i" "$SERIAL_LOG" 2>/dev/null; then log_pass "Debian Installer started" fi # Check for encryption prompt if grep -qi "crypt\|LUKS\|unlock\|passphrase" "$SERIAL_LOG" 2>/dev/null; then log_pass "Encryption prompt detected" fi # Check for secure boot if grep -qi "secure boot\|secureboot" "$SERIAL_LOG" 2>/dev/null; then log_pass "Secure Boot referenced in boot log" fi # Check for kernel lockdown if grep -qi "lockdown\|module.sig" "$SERIAL_LOG" 2>/dev/null; then log_pass "Kernel lockdown parameters detected" fi # Check for security hardening if grep -qi "security hardening\|knel\|applying security" "$SERIAL_LOG" 2>/dev/null; then log_pass "Security hardening hooks executed" fi # Check for firewall if grep -qi "firewall\|nftables" "$SERIAL_LOG" 2>/dev/null; then log_pass "Firewall setup detected" fi else log_fail "VM did not boot within ${TIMEOUT_BOOT}s" fi # Dump serial log for analysis if [ -f "$SERIAL_LOG" ] && [ -s "$SERIAL_LOG" ]; then log_info "Serial log captured (${SERIAL_LOG}): $(wc -l < "$SERIAL_LOG") lines" else log_info "No serial output captured" fi } # ============================================================================= # Phase 3: Report # ============================================================================= phase3_report() { echo "" echo "==========================================" echo " Validation Report" echo "==========================================" echo "" echo " PASS: $pass_count" echo " FAIL: $fail_count" echo " SKIP: $skip_count" echo " TOTAL: $((pass_count + fail_count + skip_count))" echo "" if [ $fail_count -eq 0 ]; then echo -e " ${GREEN}STATUS: ALL CHECKS PASSED${NC}" else echo -e " ${RED}STATUS: $fail_count FAILURES DETECTED${NC}" fi echo "" if [ -f "$SERIAL_LOG" ]; then echo " Serial log: $SERIAL_LOG" fi echo " Screenshots: $SCREENSHOT_DIR" echo "==========================================" return $fail_count } # ============================================================================= # Main # ============================================================================= main() { echo "" echo "==========================================" echo " KNEL-Football ISO Validation Harness" echo "==========================================" echo " ISO: $ISO_PATH" echo " Date: $(date)" echo "" phase0_preflight || { echo "Pre-flight failed. Aborting."; exit 1; } phase1_static_analysis phase2_boot_test phase3_report } main "$@"