#!/bin/bash # ============================================================== # HTB Writeup Scaffolder (htb) # -------------------------------------------------------------- # Author : Kris Yotam (aka. khr1st) # Contact : krisyotam@protonmail.com # License : GNU GPLv3 # Date : 2025-09-29 # -------------------------------------------------------------- # Description: # - Interactive scaffold for HackTheBox writeups. # - Creates directory under: # $HOME/Notes/Writeups/HackTheBox (or HTB_WRITEUPS_DIR) # with subdirs: Machines, Challenges, Starting Point. # - For Machines/Challenges asks Active|Retired and difficulty # (Machines: Easy, Medium, Hard, Insane). # - For Starting Point asks Tier (0, I, II). # - Produces a writeup file named HTB_ # inside the box directory and prints an ASCII banner. # - Minimal, functionized, suckless-minded. # ============================================================== set -eu # ============================================================ # CONFIGURATION - Modify these paths and settings as needed # ============================================================ # Base directory for all HTB writeups BASE="${HTB_WRITEUPS_DIR:-$HOME/Notes/Writeups/HackTheBox}" # Script binary name BIN_NAME="htb" # Directory structure CHALLENGES_DIR="Challenges" MACHINES_DIR="Machines" STARTING_POINT_DIR="Starting Point" # Status subdirectories ACTIVE_SUBDIR="Active" RETIRED_SUBDIR="Retired" # Starting Point tiers TIER_0_DIR="Tier 0" TIER_I_DIR="Tier I" TIER_II_DIR="Tier II" # File naming WRITEUP_PREFIX="HTB_" # Difficulty levels MACHINE_DIFFICULTIES="Very Easy Easy Medium Hard Insane" CHALLENGE_DIFFICULTIES="Very Easy Easy Medium Hard" STARTING_POINT_DIFFICULTIES="Very Easy Easy Medium Hard" # Type options TYPE_OPTIONS="Machine Challenge Starting Point" # Status options STATUS_OPTIONS="Active Retired" # Tier options TIER_OPTIONS="0 I II" # File handling options OVERWRITE_OPTIONS="Overwrite Append Abort" # Contact and metadata AUTHOR_NAME="Kris Yotam (aka. khr1st)" AUTHOR_EMAIL="krisyotam@protonmail.com" LICENSE="GNU GPLv3" SCRIPT_DATE="2025-09-29" # ============================================================ # ASCII banner printed to terminal (visible TUI header) ascii_banner() { cat <<'BANNER' ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⢀⣠⣤⣤⠴⠶⠾⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⣰⡿⠥⠤⠤⠶⠶⠶⠶⠖⣒⣒⣿⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⣩⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⢈⣉⡛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡥⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠒⠒⠒⠉⠉⠉⠁⠀⠀⠀⠀⢈⣉⡒⠯⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⢀⣀⡠⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⣺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⢋⣠⣤⣶⣿⠝⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡤⠒⠀⠀⠀⠀⠀⠀⠀⢀⡤⠒⠉⢩⠄⠀⠙⢿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣾⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⣴⠎⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠑⢻⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠁⠀⠀⠀⡠⠀⠀⣠⣀⣤⠒⠋⠁⡴⠁⠀⠀⠀⠀⠀⢀⢄⡜⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢯⠻⣿⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⠀⢀⠄⡠⠊⢀⡤⠖⠉⠈⢻⡳⡀⢠⠃⠀⠀⠀⠀⢀⣴⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⡆⠹⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⢀⡔⣡⣞⣡⣶⣡⣤⣄⠀⠀⠀⠹⣧⡎⠀⠄⡔⠀⡴⡻⠋⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡀⢹ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢁⣔⣿⣾⣿⣿⣿⣿⠟⣛⣿⣧⠐⡄⠀⢿⠀⡘⠞⢠⢎⠞⠁⠀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⢰⢸⡏⡇⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣱⣾⠟⣽⣫⣍⣾⡿⢁⡼⢋⣌⢽⡇⠸⠀⡼⠀⠉⡰⣳⠋⠀⠀⠀⢠⠀⠀⠀⠀⠠⠀⠀⠀⠀⠀⠀⠀⠀⠈⡜⣼⡇⡇⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠴⠋⠀⣼⢋⣽⡟⠀⠁⢸⠀⠘⠤⢿⡇⠀⠀⡇⠀⢐⡿⠁⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⢓⣿⡇⣱⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⢀⣞⣵⣿⢿⠁⠀⠀⠈⠢⠤⠤⠚⠁⠀⢨⠁⢠⠞⠀⠀⠀⠀⠀⠀⠘⠀⠀⠀⠀⠀⠀⢀⠀⢀⡎⠀⠀⢠⢡⣿⡟⠇⡇⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⢀⡼⠋⢸⡞⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡰⠃⠀⠀⠀⠐⠲⢦⣄⡀⡆⠀⠀⠀⢠⠀⣮⢇⡞⠀⡴⣠⢣⢳⡿⡸⢰⠃⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠁⠀⠀⣿⠃⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠁⠀⠀⠀⠀⠀⠀⠀⠈⠻⡅⠀⠀⠀⡆⡜⢈⣾⡁⡐⡱⢣⢣⣿⡵⢁⡿⠤⢔ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⣿⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣀⣀⠂⠄⠀⠀⠀⡇⠀⠀⠀⢠⢁⢎⣜⡞⡴⣱⣷⣿⡟⢡⣾⠶⣶⣿ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⡿⣖⣇⠀⠀⢠⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠻⣶⣄⠀⠀⡇⠀⠀⠀⣎⡞⢎⢎⡜⣼⣿⡿⢋⣴⠋⣁⣼⣿⣿ ⠟⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠿⡏⠀⠀⠀⠀⠀⠀⠀⣿⣾⣾⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣧⡀⡇⠀⠀⠀⠜⠃⢈⠞⣾⣿⢋⣴⢟⣻⣩⠿⢿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀⢀⡼⠛⣿⣇⠀⠀⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣇⡇⠀⠀⠀⠀⠠⣋⣾⢟⣥⣿⣋⣴⠛⠃⠀⠀⢹⣿ ⠀⠀⠀⠉⠉⠁⠈⠉⠉⠐⠲⠤⣀⠇⠀⡀⣠⠄⠐⠐⠒⠉⠀⠈⠉⠙⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠔⠋⣇⡀⠀⠀⢠⣾⢟⡵⠟⠻⣆⣽⣿⣶⣶⣶⣦⣼⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⢿⢲⠁⠀⠀⠀⠀⠀⠀⠀⠀⢀⢼⢿⣍⣒⢂⡠⠀⠀⠀⢄⣀⠀⠀⠀⣀⣤⠞⢁⡴⠊⠀⠉⠓⠶⣿⡿⢟⣶⣲⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣘⡇⡇⠀⠀⠀⠀⠀⠀⠀⢀⡴⠋⣼⣿⠏⠛⠉⠀⠀⠀⠀⠀⠈⢙⢻⣫⠟⢁⠞⠁⠀⠀⠀⠀⠀⠀⠀⠉⠛⠻⣍⡀⢀⣿⣿⣿⣿⣿⣏⣹⡟ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣬⣿⣳⠀⠀⠀⠀⠀⠀⢀⡴⣟⠁⡜⠀⠀⠀⠀⠀⠀⠀⠀⠠⣤⡀⠈⣆⢯⣀⣿⡴⠀⣀⡠⠔⠚⠋⠀⠀⠀⠀⠀⠀⠻⣾⣿⣿⣿⣿⣿⣿⡟⠀ ⠀⠀⠀⠀⠀⠀⠀⣠⠏⠀⣐⣿⠋⢸⡇⠀⠀⠀⣀⣴⠞⢻⠃⢀⡟⠀⠀⠀⢀⣠⡖⠉⣻⡿⠶⣬⠿⢦⣈⣯⣿⣯⣤⠾⠳⠒⠒⢚⡍⢙⠋⠲⢄⣀⠀⠀⠹⣿⣿⣿⣿⣿⣿⠁⠀ ⣀⠀⠀⠀⠀⠀⣰⠇⣠⣽⠞⠁⠀⢯⡇⠤⠒⠛⣃⣀⣀⡜⢠⠞⠀⠀⠀⢰⠋⢸⣦⡉⠁⣀⣼⣧⡀⠀⠸⣽⠀⠀⠀⠀⠀⠀⠰⠟⠁⢘⠀⠀⢰⡌⠀⠀⠀⠙⢿⣛⣛⣛⠃⠀⠀ ⢾⣳⡀⠀⠀⠚⢡⢒⠞⠁⠀⠀⠀⣀⣉⣀⣀⣠⠏⠀⣾⡿⠃⠀⠀⠀⠀⠋⠀⠀⢿⣷⣠⣾⣿⢻⣅⠀⠀⠃⠀⠀⠀⠀⠀⠀⠀⠀⢀⡞⠀⠀⠀⠹⣆⠀⠀⠀⠀⠛⣿⣿⣷⡄⠀ ⠀⢹⣷⣀⣠⣶⣏⠤⠒⣲⠾⢫⢽⢳⣖⣶⣿⣿⠀⠐⡟⠁⠀⠀⠀⠀⠀⠀⠀⢀⡈⣿⡟⣽⣿⠃⠀⠉⠲⠤⣄⣀⠀⠀⠀⠀⠀⢠⠏⠀⠀⠀⠀⠀⠘⡆⠀⠀⠀⠀⠘⢿⣿⣿⠀ ⠀⠀⣤⣻⠿⠥⡞⡗⢺⣿⣽⣿⣽⡿⣿⣿⣿⣿⣇⠀⢃⠀⠀⠀⠀⣸⠁⠀⠀⠈⠁⠈⠑⠻⠆⠀⠀⠀⠀⠀⠀⠈⠉⠒⠤⠤⠒⠉⠀⠀⠀⠀⠀⠀⠀⠸⡄⠀⠀⠀⠀⠈⢻⣿⠀ ⠀⠀⢦⣟⣻⣾⣭⣭⡽⠶⠶⠶⠶⣤⣦⠖⠿⣯⡿⡄⠘⡆⠀⠀⡼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠢⡄⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⣀⣀⣤⡝⢆⠀⠀⠀⠀⠈⡿⠀ ⠂⠀⠈⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⡾⡭⣗⢣⢹⡀⠘⣄⠞⣡⣤⣤⣤⣴⣶⣶⣿⣽⠿⠉⠉⠁⠒⠒⠤⣧⠤⠴⢲⢶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⢦⡀⠀⢀⠇⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠃⣿⢨⠇⢳⡀⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠓⠀⠀⠀⠀⠀⠈⠁⠀⠀⡞⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡍⠁⡸⠀⢠ ⣤⣤⣤⢤⣤⣤⣄⡀⣠⣀⣀⣾⣤⠀⠄⠈⡀⣸⡏⠀⢘⣷⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⠿⡗⠦⣀⣀⡀⠀⠀⠀⠀⠀⡇⢾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣤⠇⠀⣼ HTB Writeup Scaffolder — ${AUTHOR_NAME} Contact : ${AUTHOR_EMAIL} License : ${LICENSE} Date : ${SCRIPT_DATE} BANNER } # Utilities ----------------------------------------------------------------- prompt() { # prompt varname=$1; shift msg=$* printf "%s " "$msg" # POSIX read into a temp var then export to caller via eval IFS= read -r _answer || _answer='' eval "$varname=\"\$_answer\"" } choose_one() { # choose_one ... msg=$1; shift opts="$@" while :; do printf "%s\n" "$msg" i=1 for opt in $opts; do printf " %d) %s\n" "$i" "$opt" i=$((i + 1)) done printf "Choice [1-%d]: " $((i - 1)) IFS= read -r choice || choice='' case "$choice" in ''|*[!0-9]*) printf "Invalid. Enter a number.\n" ;; *) if [ "$choice" -ge 1 ] 2>/dev/null && [ "$choice" -lt "$i" ] 2>/dev/null; then # return the selected option set -- $opts shift $((choice - 1)) printf "%s\n" "$1" return 0 else printf "Out of range.\n" fi ;; esac done } lower_sanitize() { # convert to lowercase and replace non-alnum with underscore printf "%s" "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g' } camelcase_join() { # join words into CamelCase, remove non-alnum # uses awk for portability printf "%s" "$*" | awk '{ n = split($0, a, /[^[:alnum:]]+/) for (i=1;i<=n;i++) { if (a[i] == "") continue $i = toupper(substr(a[i],1,1)) tolower(substr(a[i],2)) printf "%s", $i } }' } ensure_dir() { if [ ! -d "$1" ]; then mkdir -p "$1" || { printf "Failed to create %s\n" "$1" >&2; exit 1; } printf "created: %s\n" "$1" fi } # Prepare base layout ------------------------------------------------------- prepare_layout() { ensure_dir "$BASE" ensure_dir "$BASE/$CHALLENGES_DIR" ensure_dir "$BASE/$MACHINES_DIR" ensure_dir "$BASE/$STARTING_POINT_DIR" ensure_dir "$BASE/$CHALLENGES_DIR/$ACTIVE_SUBDIR" ensure_dir "$BASE/$CHALLENGES_DIR/$RETIRED_SUBDIR" ensure_dir "$BASE/$MACHINES_DIR/$ACTIVE_SUBDIR" ensure_dir "$BASE/$MACHINES_DIR/$RETIRED_SUBDIR" ensure_dir "$BASE/$STARTING_POINT_DIR/$TIER_0_DIR" ensure_dir "$BASE/$STARTING_POINT_DIR/$TIER_I_DIR" ensure_dir "$BASE/$STARTING_POINT_DIR/$TIER_II_DIR" } # Compose writeup header exactly as requested -------------------------------- compose_writeup() { name_print="$1" type_print="$2" status_print="$3" difficulty_print="$4" permalink_name="$5" file_path="$6" # ensure no blank lines inside the YAML-like front matter cat > "$file_path" <&2 exit 1 fi DIR_NAME="$(lower_sanitize "$BOXNAME")" FILE_BASENAME="$(camelcase_join "$BOXNAME")" WRITEUP_FILENAME="${WRITEUP_PREFIX}${FILE_BASENAME}" prepare_layout # Type selection TYPE="$(choose_one "Is this a Machine, Challenge, or Starting Point?" $TYPE_OPTIONS)" TIER="N/A" STATUS="N/A" DIFFICULTY="N/A" DEST_DIR="" if [ "$TYPE" = "Starting Point" ]; then TIER_CHOICE="$(choose_one "Which tier?" $TIER_OPTIONS)" DIFFICULTY="$(choose_one "Starting Point difficulty" $STARTING_POINT_DIFFICULTIES)" case "$TIER_CHOICE" in 0) TIER="$TIER_0_DIR"; DEST_DIR="$BASE/$STARTING_POINT_DIR/$TIER_0_DIR" ;; I) TIER="$TIER_I_DIR"; DEST_DIR="$BASE/$STARTING_POINT_DIR/$TIER_I_DIR" ;; II) TIER="$TIER_II_DIR"; DEST_DIR="$BASE/$STARTING_POINT_DIR/$TIER_II_DIR" ;; esac else STATUS="$(choose_one "Is it Active or Retired?" $STATUS_OPTIONS)" if [ "$TYPE" = "Machine" ]; then DIFFICULTY="$(choose_one "Machine difficulty" $MACHINE_DIFFICULTIES)" DEST_DIR="$BASE/$MACHINES_DIR/$STATUS" else DIFFICULTY="$(choose_one "Challenge difficulty" $CHALLENGE_DIFFICULTIES)" DEST_DIR="$BASE/$CHALLENGES_DIR/$STATUS" fi fi TARGET_DIR="${DEST_DIR}/${DIR_NAME}" ensure_dir "$TARGET_DIR" WRITEUP_PATH="${TARGET_DIR}/${WRITEUP_FILENAME}" if [ -e "$WRITEUP_PATH" ]; then CHOICE="$(choose_one "Writeup exists. Overwrite, Append, Abort?" $OVERWRITE_OPTIONS)" case "$CHOICE" in Overwrite) : > "$WRITEUP_PATH" ;; Append) printf "\n\n" >> "$WRITEUP_PATH" ;; Abort) printf "Aborted.\n"; exit 0 ;; esac else : > "$WRITEUP_PATH" fi # Compose file header # Convert TYPE to single-word friendly form for permalink: machine/challenge/starting_point type_perm="$(printf "%s" "$TYPE" | tr '[:upper:]' '[:lower:]' | sed 's/ /_/g')" # For Starting Point, use TIER as status, otherwise use STATUS if [ "$TYPE" = "Starting Point" ]; then compose_writeup "$BOXNAME" "$TYPE" "$TIER" "$DIFFICULTY" "$DIR_NAME" "$WRITEUP_PATH" else compose_writeup "$BOXNAME" "$TYPE" "$STATUS" "$DIFFICULTY" "$DIR_NAME" "$WRITEUP_PATH" fi printf "\nCreated writeup at: %s\n" "$WRITEUP_PATH" printf "Open and edit to add notes.\n" } main "$@"