diff --git a/scripts/tty-menu.sh b/scripts/tty-menu.sh new file mode 100644 index 0000000..1f2d784 --- /dev/null +++ b/scripts/tty-menu.sh @@ -0,0 +1,174 @@ +#!/bin/sh +# ─────────────────────────────────────────────────────────── +# tty-menu.sh — Serial Console Menu for the Alpine Docker Host +# +# Launched by agetty on ttyS0. The user interacts directly +# with the Ansible container from the serial console. +# +# Options: +# 1-4 Run playbooks (docker run ansible-node) +# 5 Download reports as tar.gz +# 6 View latest report summary +# 7 Shell into Ansible container +# 8 Shell on Docker host +# 0 Shutdown VM +# ─────────────────────────────────────────────────────────── + +PLAYBOOKS_DIR="/ansible/playbooks" +REPORTS_DIR="/ansible/reports" +INVENTORY="/ansible/inventory/inventory.ini" +IMAGE="ansible-node" +WEB_PORT="8080" + +# ── Colour helpers ──────────────────────────────────────── +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +clear + +while true; do + # Determine IP for web UI hint + IP=$(ip -4 addr show scope global 2>/dev/null | \ + grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1) + [ -z "$IP" ] && IP="(no network)" + + echo "" + echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}${BOLD}║ IEC 62443-3-3 SL2 Compliance Validator ║${NC}" + echo -e "${GREEN}${BOLD}╠══════════════════════════════════════════════════╣${NC}" + echo -e "${GREEN}${BOLD}║${NC} Web UI: ${CYAN}http://${IP}:${WEB_PORT}${GREEN} ${NC}${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}╠══════════════════════════════════════════════════╣${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}1)${NC} Run all tests (site.yml) ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}2)${NC} Run FR1 — Auth & Identification ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}3)${NC} Run FR2 — Use Control ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}4)${NC} Run FR5 — Restricted Data Flow ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}5)${NC} Create reports archive (tar.gz) ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}6)${NC} View latest report summary ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}7)${NC} Shell — Ansible container ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}8)${NC} Shell — Docker host ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}║${NC} ${BOLD}0)${NC} Shutdown VM ${GREEN}${BOLD}║${NC}" + echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════╝${NC}" + echo "" + printf " Choice [0-8]: " + read -r CHOICE + + docker_run() { + local pb="$1"; shift + echo "" + echo -e "${YELLOW}Running: ${pb}${NC}" + echo "═══════════════════════════════════════════" + docker run --rm -it \ + -v "${PLAYBOOKS_DIR}:/ansible/playbooks:ro" \ + -v "${REPORTS_DIR}:/ansible/reports" \ + -v "$(dirname "${INVENTORY}"):/ansible/inventory:ro" \ + "${IMAGE}" \ + "/ansible/playbooks/${pb}" \ + -i /ansible/inventory/inventory.ini "$@" + echo "" + echo -e "${GREEN}Done. Reports: ${REPORTS_DIR}${NC}" + echo "Press Enter to continue..." + read -r _ + } + + case "$CHOICE" in + 1) docker_run "site.yml" ;; + 2) docker_run "suites/fr1_auth.yml" ;; + 3) docker_run "suites/fr2_use_control.yml" ;; + 4) docker_run "suites/fr5_data_flow.yml" ;; + 5) + echo "" + echo -e "${YELLOW}Available reports:${NC}" + ls -lh "${REPORTS_DIR}"/*.json 2>/dev/null || echo " No reports yet." + echo "" + printf " Enter filename (or Enter for all as tar.gz): " + read -r FN + if [ -n "$FN" ]; then + OUT="/tmp/${FN}" + cp "${REPORTS_DIR}/${FN}" "$OUT" 2>/dev/null && \ + echo -e " Written: ${GREEN}${OUT}${NC}" || \ + echo -e "${RED} Not found: ${FN}${NC}" + else + OUTF="/tmp/reports-$(date +%Y%m%d-%H%M).tar.gz" + cd "${REPORTS_DIR}" && tar czf "$OUTF" *.json 2>/dev/null && \ + echo -e " Created: ${GREEN}${OUTF}${NC} ($(du -sh "$OUTF" | cut -f1))" + fi + echo " Transfer via: scp ansible@:/tmp/reports-*.tar.gz ." + echo "" + echo "Press Enter to continue..." + read -r _ + ;; + 6) + LATEST=$(ls -t "${REPORTS_DIR}"/*.json 2>/dev/null | head -1) + if [ -z "$LATEST" ]; then + echo -e "${RED} No reports yet.${NC}" + else + echo "" + echo -e "${YELLOW}Latest: $(basename "$LATEST")${NC}" + echo "═══════════════════════════════════════════" + python3 -c " +import json +with open('$LATEST') as f: + r = json.load(f) +s = r['summary'] +print(f'Total: {s[\"total\"]} | Passed: {s[\"passed\"]} | Failed: {s[\"failed\"]}') +print(f'Compliance rate: {s[\"passed\"]/s[\"total\"]*100:.1f}%') +print() +for t in r['results']: + icon = '\u2705' if t['passed'] == True else ('\u274c' if t['passed'] == False else '\U0001f50d') + print(f' {icon} [{t[\"test_id\"]}] {t[\"description\"]}') +print() +if r.get('failures'): + print('Failures:') + for f in r['failures']: + print(f' \u274c {f[\"test_id\"]}: {f[\"description\"]}') + print(f' Expected: {f[\"expected\"]}') + print(f' Actual: {f[\"actual\"]}') + print(f' Fix: {f[\"remediation\"]}') +" 2>/dev/null || echo " Error reading report" + fi + echo "" + echo "Press Enter to continue..." + read -r _ + ;; + 7) + echo "" + echo -e "${YELLOW}Ansible container shell (type 'exit' to return)${NC}" + echo "═══════════════════════════════════════════" + docker run --rm -it \ + -v "${PLAYBOOKS_DIR}:/ansible/playbooks:ro" \ + -v "${REPORTS_DIR}:/ansible/reports" \ + -v "$(dirname "${INVENTORY}"):/ansible/inventory:ro" \ + "${IMAGE}" /bin/bash 2>/dev/null || \ + docker run --rm -it \ + -v "${PLAYBOOKS_DIR}:/ansible/playbooks:ro" \ + -v "${REPORTS_DIR}:/ansible/reports" \ + -v "$(dirname "${INVENTORY}"):/ansible/inventory:ro" \ + "${IMAGE}" /bin/sh + ;; + 8) + echo "" + echo -e "${YELLOW}Host shell (type 'exit' to return to menu)${NC}" + echo "═══════════════════════════════════════════" + /bin/bash 2>/dev/null || /bin/sh + ;; + 0) + echo "" + echo -e "${RED}Shutting down...${NC}" + sudo poweroff 2>/dev/null || poweroff + exit 0 + ;; + *) + echo -e "${RED}Invalid choice${NC}" + sleep 1 + ;; + esac +done