diff --git a/ALPHA_RELEASE_PLAN.md b/ALPHA_RELEASE_PLAN.md index dc7880e..2fe0267 100644 --- a/ALPHA_RELEASE_PLAN.md +++ b/ALPHA_RELEASE_PLAN.md @@ -66,8 +66,11 @@ working scripts, working `pf` tasks, documentation, and at least smoke tests. - DISA STIG helper (`dod-info`, `dod-stig-check`, `dod-secure-config`). ### 1.7 UUEFI host-side helpers -- `uuefi-install`, `uuefi-apply`, `uuefi-report` (require root / - `efibootmgr`). +- `uuefi-report` (read-only). +- `uuefi-install` / `uuefi-apply` remain present for maintainer testing but + are gated for `v0.1.0-alpha` unless + `PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1` is set. They are not part of + the supported end-user alpha surface until broader host-side validation lands. ### 1.8 CLIs and runners - `./pf` — symlink to the vendored `pf-runner/pf` launcher. This is the @@ -214,7 +217,8 @@ These are concrete, small bugs that block a clean alpha experience. | 4.3 | `scripts/maintenance/lint.sh` wrote to `out/lint/*.log` without `mkdir -p`. | Already fixed. | | 4.4 | `scripts/esp-packaging/esp-package-enroll.sh` had the wrong `source` path. | Already fixed. | | 4.5 | `docs/AGENTS.md` still warns about the `esp-package.sh` `cd` bug. | Update doc to reflect post-fix state. | -| 4.6 | `./phoenixboot list` shows "wrapper commands" only when `pf` is unavailable, but the wording is good. | Keep as-is; verified. | +| 4.6 | `./phoenixboot list` / `./phoenixboot-dod list` were treating a broken `pf` runtime as "available" and surfacing a raw `pf` error. | Probe `pf version` first, then fall back to curated wrapper output plus actionable install hints when the runner is present but unusable. | +| 4.7 | `uuefi-install.sh` could silently fall back to `BootX64.efi`, and `uuefi-install` / `uuefi-apply` lacked broad host-side validation. | Refuse the fallback, gate the mutating host-side helpers behind `PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1`, and keep `uuefi-report` as the safe default. | The end-to-end "does anything work without `pf` installed" smoke for the alpha is `./phoenixboot help`, `./phoenixboot status`, `./phoenixboot list`, diff --git a/README.md b/README.md index 00cb2c3..a28e437 100644 --- a/README.md +++ b/README.md @@ -242,11 +242,11 @@ Tools for managing UEFI boot entries: - `os-boot-clean`: Clean stale UEFI boot entries - `os-mok-enroll`: Enroll MOK keys for module signing - `os-mok-list-keys`: List available MOK certificates -- `uuefi-install`: Install UUEFI.efi to system ESP -- `uuefi-apply`: Set BootNext for one-time UUEFI boot +- `uuefi-install`: Install UUEFI.efi to system ESP (**alpha-gated pending broader host-side validation**) +- `uuefi-apply`: Set BootNext for one-time UUEFI boot (**alpha-gated pending broader host-side validation**) - `uuefi-report`: Display system security status -**Status**: Scripts implemented, tested on real hardware +**Status**: `uuefi-report` is shipped for alpha; host-side mutating helpers stay gated unless `PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1` is set during deliberate maintainer testing #### 4. **QEMU Testing** Comprehensive QEMU-based testing: diff --git a/components/core/core.pf b/components/core/core.pf index cffbd18..8e45929 100644 --- a/components/core/core.pf +++ b/components/core/core.pf @@ -199,12 +199,12 @@ end # --- UUEFI Operations --- task uuefi-install - describe Install UUEFI.efi to system ESP + describe Install UUEFI.efi to system ESP (gated for alpha; set PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1 to override) shell bash scripts/uefi-tools/uuefi-install.sh end task uuefi-apply - describe UUEFI apply (set BootNext; optionally create entry) + describe UUEFI apply (set BootNext; gated for alpha; set PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1 to override) shell bash scripts/uefi-tools/uuefi-apply.sh end diff --git a/components/core/scripts/testing/test-phoenixboot-cli.sh b/components/core/scripts/testing/test-phoenixboot-cli.sh index 1123e3e..f0b1016 100755 --- a/components/core/scripts/testing/test-phoenixboot-cli.sh +++ b/components/core/scripts/testing/test-phoenixboot-cli.sh @@ -254,6 +254,24 @@ else fi fi +# Test 22: DoD list command should either work or fail loudly with install context +echo "[TEST 22] Testing phoenixboot-dod list..." +dod_list_output=$(./phoenixboot-dod list 2>&1 || true) +if echo "$dod_list_output" | grep -qi "Attempted pf.py arguments: list\|PhoenixBoot - Department of Defense"; then + pass "phoenixboot-dod list provides curated output or clear install context" +else + fail "phoenixboot-dod list did not provide clear install context" +fi + +# Test 23: Unknown DoD commands should fail with delegation context +echo "[TEST 23] Testing phoenixboot-dod unknown command handling..." +dod_unknown_output=$(timeout 5 ./phoenixboot-dod invalid_command_xyz 2>&1 || true) +if echo "$dod_unknown_output" | grep -qi "Attempted pf.py arguments: invalid_command_xyz\|phoenixboot-dod ERROR"; then + pass "Unknown DoD commands fail loudly with delegation context" +else + fail "Unknown DoD command handling did not emit clear contextual output" +fi + # Summary echo echo "=======================" diff --git a/components/core/scripts/testing/test-uuefi-host-helpers.sh b/components/core/scripts/testing/test-uuefi-host-helpers.sh new file mode 100755 index 0000000..1ae6207 --- /dev/null +++ b/components/core/scripts/testing/test-uuefi-host-helpers.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Focused regression test for alpha-gated UUEFI host-side helpers. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if PROJECT_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)"; then + : +else + PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +fi +cd "$PROJECT_ROOT" + +PASSED=0 +FAILED=0 + +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +pass() { + echo -e " ${GREEN}✓${NC} $1" + PASSED=$((PASSED + 1)) +} + +fail() { + echo -e " ${RED}✗${NC} $1" + FAILED=$((FAILED + 1)) +} + +echo "Testing UUEFI host-side helper safety" +echo "=====================================" +echo + +INSTALL_SCRIPT="$PROJECT_ROOT/scripts/uefi-tools/uuefi-install.sh" +APPLY_SCRIPT="$PROJECT_ROOT/scripts/uefi-tools/uuefi-apply.sh" + +echo "[TEST 1] uuefi-install is alpha-gated by default..." +install_output=$(bash "$INSTALL_SCRIPT" 2>&1 || true) +if echo "$install_output" | grep -q "gated off for the alpha release"; then + pass "uuefi-install refuses to run without explicit opt-in" +else + fail "uuefi-install did not enforce the alpha gate" +fi + +echo "[TEST 2] uuefi-apply is alpha-gated by default..." +apply_output=$(bash "$APPLY_SCRIPT" 2>&1 || true) +if echo "$apply_output" | grep -q "gated off for the alpha release"; then + pass "uuefi-apply refuses to run without explicit opt-in" +else + fail "uuefi-apply did not enforce the alpha gate" +fi + +echo "[TEST 3] uuefi-install refuses BootX64 placeholder fallback..." +missing_binary_output=$(PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1 UUEFI_SRC=/definitely/missing/UUEFI.efi bash "$INSTALL_SCRIPT" 2>&1 || true) +if echo "$missing_binary_output" | grep -q "Refusing to fall back to BootX64.efi"; then + pass "uuefi-install requires a real UUEFI binary" +else + fail "uuefi-install still allows a BootX64 placeholder fallback" +fi + +echo +echo "=======================" +echo "Test Summary" +echo "=======================" +echo -e "${GREEN}Passed:${NC} ${PASSED}" +echo -e "${RED}Failed:${NC} ${FAILED}" +echo + +if [ "$FAILED" -eq 0 ]; then + echo -e "${GREEN}✓ All UUEFI host-helper tests passed!${NC}" + exit 0 +else + echo -e "${RED}✗ Some UUEFI host-helper tests failed${NC}" + exit 1 +fi diff --git a/components/core/scripts/uefi-tools/README.md b/components/core/scripts/uefi-tools/README.md index 2c89f24..1451ced 100644 --- a/components/core/scripts/uefi-tools/README.md +++ b/components/core/scripts/uefi-tools/README.md @@ -4,8 +4,8 @@ Scripts for UEFI operations and diagnostics. ## UUEFI Operations -- `uuefi-install.sh` - Install UUEFI to system ESP -- `uuefi-apply.sh` - Set BootNext for one-time UUEFI boot +- `uuefi-install.sh` - Install UUEFI to system ESP (**alpha-gated pending broader host-side validation**) +- `uuefi-apply.sh` - Set BootNext for one-time UUEFI boot (**alpha-gated pending broader host-side validation**) - `uuefi-report.sh` - Display UEFI system status - `host-uuefi-once.sh` - Boot UUEFI once on host @@ -17,6 +17,9 @@ Scripts for UEFI operations and diagnostics. ## Usage ```bash +# Alpha safety gate for host-side mutating helpers +export PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1 + # Install UUEFI sudo ./scripts/uefi-tools/uuefi-install.sh diff --git a/components/core/scripts/uefi-tools/uuefi-apply.sh b/components/core/scripts/uefi-tools/uuefi-apply.sh index c2e4bd7..e47f04c 100755 --- a/components/core/scripts/uefi-tools/uuefi-apply.sh +++ b/components/core/scripts/uefi-tools/uuefi-apply.sh @@ -3,8 +3,6 @@ set -euo pipefail cd "$(dirname "$0")/../.." source includes/lib/common.sh -info "☠ UUEFI apply (set BootNext for selected app)" - # Dry-run mode: UUEFI_DRYRUN=1 DRY=${UUEFI_DRYRUN:-} run() { if [ -n "$DRY" ]; then echo "DRYRUN: $*"; else "$@"; fi } @@ -12,6 +10,15 @@ need_sudo() { if [ -n "$DRY" ]; then echo sudo -n "$@" || true; else sudo -n "$@"; fi } +require_alpha_opt_in() { + [ "${PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST:-0}" = "1" ] && return 0 + die "uuefi-apply is gated off for the alpha release pending broader host-side validation. Use uuefi-report for read-only inspection, track follow-up in docs/TODO.md, and only override with PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1 during deliberate maintainer testing." +} + +require_alpha_opt_in + +info "☠ UUEFI apply (set BootNext for selected app)" + if ! command -v efibootmgr >/dev/null 2>&1; then die "efibootmgr not installed" fi diff --git a/components/core/scripts/uefi-tools/uuefi-install.sh b/components/core/scripts/uefi-tools/uuefi-install.sh index 73321f4..5d46c17 100755 --- a/components/core/scripts/uefi-tools/uuefi-install.sh +++ b/components/core/scripts/uefi-tools/uuefi-install.sh @@ -3,14 +3,21 @@ set -euo pipefail cd "$(dirname "$0")/../.." source includes/lib/common.sh -info "☠ Installing UUEFI.efi to system ESP" - # Config EESP=${EESP:-/boot/efi} UUEFI_SRC=${UUEFI_SRC:-staging/boot/UUEFI.efi} OUT_DIR="out/uuefi" ensure_dir "$OUT_DIR" +require_alpha_opt_in() { + [ "${PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST:-0}" = "1" ] && return 0 + die "uuefi-install is gated off for the alpha release pending broader host-side validation. Use uuefi-report for read-only inspection, track follow-up in docs/TODO.md, and only override with PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1 during deliberate maintainer testing." +} + +require_alpha_opt_in + +info "☠ Installing UUEFI.efi to system ESP" + need_sudo() { if sudo -n true 2>/dev/null; then sudo -n "$@" @@ -19,15 +26,9 @@ need_sudo() { fi } -# Resolve source binary (fallback to BootX64.efi if UUEFI.efi missing) +# Resolve source binary if [ ! -f "$UUEFI_SRC" ]; then - warn "UUEFI source not found at $UUEFI_SRC" - if [ -f out/staging/BootX64.efi ]; then - warn "Falling back to out/staging/BootX64.efi as UUEFI placeholder" - UUEFI_SRC="out/staging/BootX64.efi" - else - die "No UUEFI.efi or out/staging/BootX64.efi present. Build or provide UUEFI.efi first." - fi + die "UUEFI source not found at $UUEFI_SRC. Refusing to fall back to BootX64.efi for alpha safety; provide the real UUEFI.efi binary explicitly." fi # Locate ESP mount @@ -116,4 +117,3 @@ echo " APP=UUEFI EFI_DISK=$EFI_DISK EFI_PART=$EFI_PART ./pf.py uuefi-apply" else echo " APP=UUEFI EFI_DISK=/dev/sdX EFI_PART=1 ./pf.py uuefi-apply" fi - diff --git a/docs/README.md b/docs/README.md index 304690d..720f1a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -230,11 +230,11 @@ Tools for managing UEFI boot entries: - `os-boot-clean`: Clean stale UEFI boot entries - `os-mok-enroll`: Enroll MOK keys for module signing - `os-mok-list-keys`: List available MOK certificates -- `uuefi-install`: Install UUEFI.efi to system ESP -- `uuefi-apply`: Set BootNext for one-time UUEFI boot +- `uuefi-install`: Install UUEFI.efi to system ESP (**alpha-gated pending broader host-side validation**) +- `uuefi-apply`: Set BootNext for one-time UUEFI boot (**alpha-gated pending broader host-side validation**) - `uuefi-report`: Display system security status -**Status**: Scripts implemented, tested on real hardware +**Status**: `uuefi-report` is shipped for alpha; host-side mutating helpers stay gated unless `PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1` is set during deliberate maintainer testing #### 4. **QEMU Testing** Comprehensive QEMU-based testing: diff --git a/docs/TODO.md b/docs/TODO.md index 62f5736..ef58b33 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -225,10 +225,12 @@ The UUEFI diagnostic tool has significantly evolved. The issue noted it was "rea - [ ] Install `efitools` (`cert-to-efi-sig-list`) to unblock `secure-make-auth` and downstream tasks - [ ] Install `qemu-system-x86_64` + `ovmf` + `mtools` to enable QEMU test suite +- [ ] Add broader host-side validation for `uuefi-install` / `uuefi-apply` before removing the alpha gate (`PHOENIXBOOT_ALPHA_ALLOW_UNTESTED_UUEFI_HOST=1`) +- [ ] Add end-to-end coverage for the DoD CLI delegation paths (`list` / unknown-command fallback) in CI, not just the main CLI smoke script - [ ] Install EDK2 toolchain to enable `build-build` and UEFI binary compilation - [ ] Fix `pf` binary installation so `pf.py list` and all task-list UX works correctly (see `.pf_fix.py` for patch details) - [ ] Add `pf` binary availability check to `scripts/maintenance/toolchain-check.sh` -- [ ] Investigate `test-cli` test 6 failure: `phoenixboot list` falls through to `pf.py` which requires `pf` binary +- [x] Make `phoenixboot` / `phoenixboot-dod` fail loudly with install hints when `pf` exists but is not actually runnable ## Modularity Follow-up diff --git a/pb b/pb new file mode 120000 index 0000000..ce90bad --- /dev/null +++ b/pb @@ -0,0 +1 @@ +phoenixboot \ No newline at end of file diff --git a/phoenixboot b/phoenixboot index 9bdfa94..ba7d032 100755 --- a/phoenixboot +++ b/phoenixboot @@ -104,6 +104,11 @@ run_pf() { "Then retry. See ALPHA_RELEASE_PLAN.md and docs/AGENTS.md for more." fi + if ! pf_runner_probe; then + print_pf_runner_unavailable "$*" + exit 127 + fi + CURRENT_ACTION="running pf task(s): $*" "$PF_SCRIPT" "$@" } @@ -142,8 +147,60 @@ require_existing_task_paths() { fi } +pf_runner_launcher() { + if [ -x "${PHOENIX_ROOT:-.}/pf" ]; then + printf '%s\n' "${PHOENIX_ROOT}/pf" + return 0 + fi + + command -v pf 2>/dev/null || return 1 +} + +pf_runner_probe() { + if [ "${PF_RUNNER_PROBED:-0}" = "1" ]; then + [ "${PF_RUNNER_PROBE_STATUS:-1}" -eq 0 ] + return + fi + + PF_RUNNER_PROBED=1 + PF_RUNNER_PROBE_STATUS=1 + PF_RUNNER_PROBE_OUTPUT="" + PF_RUNNER_LAUNCHER="" + + if ! PF_RUNNER_LAUNCHER="$(pf_runner_launcher)"; then + PF_RUNNER_PROBE_OUTPUT="No vendored ./pf launcher or PATH-installed pf executable was found." + return 1 + fi + + set +e + PF_RUNNER_PROBE_OUTPUT="$("$PF_RUNNER_LAUNCHER" version 2>&1)" + PF_RUNNER_PROBE_STATUS=$? + set -e + + [ "$PF_RUNNER_PROBE_STATUS" -eq 0 ] +} + has_pf_runner() { - [ -x "${PHOENIX_ROOT:-.}/pf" ] || command -v pf >/dev/null 2>&1 + pf_runner_probe >/dev/null 2>&1 +} + +print_pf_runner_unavailable() { + pf_runner_probe >/dev/null 2>&1 || true + + local attempted_args="$1" + local launcher="${PF_RUNNER_LAUNCHER:-$PHOENIX_ROOT/pf}" + local probe_summary="${PF_RUNNER_PROBE_OUTPUT:-Unknown pf runner failure.}" + + probe_summary="$(printf '%s\n' "$probe_summary" | head -n 1)" + + echo "phoenixboot ERROR: cannot execute PhoenixBoot task(s)." >&2 + echo " - Attempted pf.py arguments: $attempted_args" >&2 + echo " - Probe command: $launcher version" >&2 + echo " - Probe result: $probe_summary" >&2 + echo " - Install hints:" >&2 + echo " - If the vendored launcher is missing, restore it with: git checkout -- pf-runner pf" >&2 + echo " - If the launcher exists but python deps are missing, run: pip install --user -e ./pf-runner" >&2 + echo " - You can also point PF_PYTHON at a working Python 3 interpreter if needed." >&2 } print_builtin_list() { @@ -262,8 +319,7 @@ case "$cmd" in else print_builtin_list echo - echo "Full pf task listing is unavailable because the 'pf' runner is not installed." >&2 - echo "Install pf-runner or make 'pf' available in PATH to list all project tasks." >&2 + print_pf_runner_unavailable "list" fi ;; diff --git a/phoenixboot-dod b/phoenixboot-dod index db93612..758d654 100755 --- a/phoenixboot-dod +++ b/phoenixboot-dod @@ -156,7 +156,64 @@ dod_preflight() { # --------------------------------------------------------------------------- # run_pf: wrapper that always passes DOD_MODE to pf.py # --------------------------------------------------------------------------- +pf_runner_launcher() { + if [ -x "${PHOENIX_ROOT:-.}/pf" ]; then + printf '%s\n' "${PHOENIX_ROOT}/pf" + return 0 + fi + + command -v pf 2>/dev/null || return 1 +} + +pf_runner_probe() { + if [ "${PF_RUNNER_PROBED:-0}" = "1" ]; then + [ "${PF_RUNNER_PROBE_STATUS:-1}" -eq 0 ] + return + fi + + PF_RUNNER_PROBED=1 + PF_RUNNER_PROBE_STATUS=1 + PF_RUNNER_PROBE_OUTPUT="" + PF_RUNNER_LAUNCHER="" + + if ! PF_RUNNER_LAUNCHER="$(pf_runner_launcher)"; then + PF_RUNNER_PROBE_OUTPUT="No vendored ./pf launcher or PATH-installed pf executable was found." + return 1 + fi + + set +e + PF_RUNNER_PROBE_OUTPUT="$("$PF_RUNNER_LAUNCHER" version 2>&1)" + PF_RUNNER_PROBE_STATUS=$? + set -e + + [ "$PF_RUNNER_PROBE_STATUS" -eq 0 ] +} + +print_pf_runner_unavailable() { + pf_runner_probe >/dev/null 2>&1 || true + + local attempted_args="$1" + local launcher="${PF_RUNNER_LAUNCHER:-$PHOENIX_ROOT/pf}" + local probe_summary="${PF_RUNNER_PROBE_OUTPUT:-Unknown pf runner failure.}" + + probe_summary="$(printf '%s\n' "$probe_summary" | head -n 1)" + + echo "phoenixboot-dod ERROR: cannot execute PhoenixBoot task(s)." >&2 + echo " - Attempted pf.py arguments: $attempted_args" >&2 + echo " - Probe command: $launcher version" >&2 + echo " - Probe result: $probe_summary" >&2 + echo " - Install hints:" >&2 + echo " - If the vendored launcher is missing, restore it with: git checkout -- pf-runner pf" >&2 + echo " - If the launcher exists but python deps are missing, run: pip install --user -e ./pf-runner" >&2 + echo " - You can also point PF_PYTHON at a working Python 3 interpreter if needed." >&2 + dod_audit_log "ABORT: pf runner unavailable for arguments: $attempted_args" +} + run_pf() { + if ! pf_runner_probe; then + print_pf_runner_unavailable "$*" + exit 127 + fi DOD_MODE=1 ./pf.py "$@" } @@ -512,7 +569,13 @@ case "$cmd" in ;; list) - run_pf list + if pf_runner_probe; then + run_pf list + else + usage + echo >&2 + print_pf_runner_unavailable "list" + fi ;; test)