#!/usr/bin/env bash # ©AngelaMos | 2026 # install.sh # # One-shot installer for rveng, a self-hosted reverse-engineering learning # platform. Takes a fresh machine to the app built, running, and reachable in a # browser, with zero further steps, whether run from a clone or piped from a # domain via curl. rveng is a Docker service, so the deliverable is the running # app, not a binary on PATH. set -euo pipefail # ============================================================================ # CONFIG # ============================================================================ REPO_OWNER="CarterPerez-dev" REPO_NAME="Cybersecurity-Projects" PROJECT_SUBDIR="PROJECTS/advanced/rveng" TAGLINE="interactive reverse-engineering learning platform" REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}.git" DEFAULT_BRANCH="main" HOST_PORT="${RVENG_PORT:-8790}" # ============================================================================ # Colors — gated so `| bash`, logs, and CI stay clean # ============================================================================ if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then BOLD=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GREEN=$'\033[32m' YELLOW=$'\033[33m'; CYAN=$'\033[36m'; RESET=$'\033[0m' else BOLD=""; DIM=""; RED=""; GREEN=""; YELLOW=""; CYAN=""; RESET="" fi info() { printf '%s\n' " ${CYAN}+${RESET} $*" >&2; } ok() { printf '%s\n' " ${GREEN}+${RESET} $*" >&2; } warn() { printf '%s\n' " ${YELLOW}!${RESET} $*" >&2; } die() { printf '%s\n' " ${RED}x $*${RESET}" >&2; exit 1; } header(){ printf '\n%s\n\n' "${BOLD}${CYAN}--- $* ---${RESET}" >&2; } have() { command -v "$1" >/dev/null 2>&1; } trap 'printf "%s\n" "${RED}x install failed${RESET}" >&2' ERR banner() { printf '%s' "${CYAN}${BOLD}" >&2 cat >&2 <<'ART' _ __ __ __ ___ _ __ __ _ | '__/ \ / // -_)| '_ \ / _` | |_| \_/\_/ \___||_.__/ \__, | |___/ ART printf '%s\n' "${RESET}" >&2 printf '%s\n' " ${DIM}${TAGLINE}${RESET}" >&2 } # ============================================================================ # Privilege + package-manager fan # ============================================================================ SUDO="" if [ "$(id -u)" -ne 0 ]; then if have sudo; then SUDO="sudo"; fi fi pkg_install() { if have apt-get; then $SUDO apt-get update -y || warn "apt update had errors; continuing" $SUDO apt-get install -y --no-install-recommends "$@" elif have dnf; then $SUDO dnf install -y "$@" elif have pacman; then $SUDO pacman -S --needed --noconfirm "$@" elif have zypper; then $SUDO zypper install -y "$@" elif have apk; then $SUDO apk add "$@" elif have brew; then brew install "$@" else die "no known package manager; install manually: $*"; fi } # ============================================================================ # Args # ============================================================================ usage() { cat >&2 </dev/null || warn "pull failed; using existing clone" else info "cloning ${REPO_URL}" git clone --depth 1 --branch "$DEFAULT_BRANCH" --quiet "$REPO_URL" "$cache" \ || die "clone failed from ${REPO_URL}" fi printf '%s\n' "$cache/$PROJECT_SUBDIR" } # ============================================================================ # Docker — the only real dependency; install it if missing # ============================================================================ ensure_docker() { if ! have docker; then if [ "$OS" = "darwin" ]; then die "Docker not found. Install Docker Desktop for Mac, then re-run." fi info "installing Docker via get.docker.com" download_docker fi if ! docker info >/dev/null 2>&1; then if have systemctl; then info "starting the Docker daemon" $SUDO systemctl start docker 2>/dev/null || true fi fi docker info >/dev/null 2>&1 || die "Docker is installed but the daemon is not running; start it and re-run" if ! docker compose version >/dev/null 2>&1; then info "installing the Docker Compose plugin" pkg_install docker-compose-plugin || die "install the Docker Compose v2 plugin, then re-run" fi ok "docker $(docker --version | awk '{print $3}' | tr -d ,)" } download_docker() { local tmp tmp="$(mktemp)" if have curl; then curl -fsSL https://get.docker.com -o "$tmp" elif have wget; then wget -qO "$tmp" https://get.docker.com else die "need curl or wget to install Docker"; fi $SUDO sh "$tmp" || { rm -f "$tmp"; die "Docker install failed"; } rm -f "$tmp" if [ -n "$SUDO" ] && have usermod; then $SUDO usermod -aG docker "$(id -un)" 2>/dev/null || true fi } # ============================================================================ # Compose wrapper — run as the current user against the prod stack # ============================================================================ compose() { NGINX_HOST_PORT="$HOST_PORT" docker compose "$@" } http_probe() { if have curl; then curl -fsS "$1" >/dev/null 2>&1 elif have wget; then wget -q -O /dev/null "$1" 2>/dev/null else return 1; fi } wait_healthy() { local url="http://localhost:${HOST_PORT}/api/challenges" i for i in $(seq 1 90); do if http_probe "$url"; then return 0; fi sleep 2 done return 1 } # ============================================================================ # Main # ============================================================================ main() { banner ensure_docker header "Fetching rveng" PROJECT="$(resolve_project)" [ -d "$PROJECT" ] || die "could not locate the rveng project directory" cd "$PROJECT" ok "project at $PROJECT" header "Building and starting (this compiles the frontend and engine image)" compose up -d --build header "Waiting for the app" if wait_healthy; then ok "rveng is answering on port ${HOST_PORT}" else warn "app did not answer within the timeout; check 'docker compose logs'" fi printf '\n%s\n\n' " ${GREEN}${BOLD}rveng is running.${RESET}" >&2 cat >&2 <