- src/build-iso.sh: Replace undefined $VERSION with correct filename. The success check referenced $PROJECT_NAME-v$VERSION.iso but $VERSION was never defined, causing the build to always report failure. - scripts/validate-iso.sh: Replace deprecated `-net nic -net user` with modern `-nic user` QEMU networking syntax. 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
364 lines
12 KiB
Bash
Executable File
364 lines
12 KiB
Bash
Executable File
#!/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=180
|
|
# 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=$((pass_count + 1)); }
|
|
log_fail() { echo -e "${RED}[FAIL]${NC} $1"; fail_count=$((fail_count + 1)); }
|
|
log_skip() { echo -e "${YELLOW}[SKIP]${NC} $1"; skip_count=$((skip_count + 1)); }
|
|
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 (cd "$(dirname "$ISO_PATH")" && sha256sum -c "$(basename "$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 (cd "$(dirname "$ISO_PATH")" && md5sum -c "$(basename "$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
|
|
|
|
# Cache isoinfo listing for reuse
|
|
local iso_listing=""
|
|
if command -v isoinfo >/dev/null 2>&1; then
|
|
iso_listing=$(isoinfo -l -i "$ISO_PATH" 2>/dev/null || true)
|
|
fi
|
|
|
|
# Check ISO has EFI boot capability
|
|
if [ -n "$iso_listing" ]; then
|
|
if echo "$iso_listing" | 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 [ -n "$iso_listing" ]; then
|
|
if echo "$iso_listing" | grep -qi "install\|d-i\|debian\|pool"; then
|
|
log_pass "ISO contains Debian installer/repository"
|
|
else
|
|
log_fail "ISO missing Debian installer/repository"
|
|
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 \
|
|
-nic 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
|
|
# UEFI BdsDxe messages confirm firmware loaded the boot device
|
|
# GRUB/Linux require serial console config to appear here
|
|
if grep -qi "GNU GRUB\|Linux version\|Debian GNU\|login:\|BdsDxe: starting" "$SERIAL_LOG" 2>/dev/null; then
|
|
booted=true
|
|
break
|
|
fi
|
|
fi
|
|
sleep 5
|
|
elapsed=$((elapsed + 5))
|
|
echo -n "."
|
|
done
|
|
echo ""
|
|
|
|
if $booted; then
|
|
# Distinguish UEFI-only boot from full OS boot
|
|
if grep -qi "GNU GRUB" "$SERIAL_LOG" 2>/dev/null; then
|
|
log_pass "GRUB bootloader loaded (serial console)"
|
|
elif grep -qi "BdsDxe: starting" "$SERIAL_LOG" 2>/dev/null; then
|
|
log_pass "UEFI firmware booted ISO (GRUB uses VGA, not serial)"
|
|
log_skip "GRUB/Linux serial output (add console=ttyS0 for serial)"
|
|
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 "$@"
|