Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions lib/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,10 @@ __dotsec_load_global() {
if [[ -f "${DOTSEC_CONFIG}/config" ]]; then
source "${DOTSEC_CONFIG}/config"
fi
# Resolve the Exegol container: detect when unset OR when the configured one
# no longer exists (e.g. removed/recreated) — so a stale config value never
# sticks. Prefer a running container, fall back to any.
if [[ -z "${EXEGOL_CONTAINER:-}" ]] || ! docker container inspect "${EXEGOL_CONTAINER}" >/dev/null 2>&1; then
local detected
detected=$(docker ps --filter "name=exegol" --format '{{.Names}}' 2>/dev/null | head -1)
[[ -z "$detected" ]] && detected=$(docker ps -a --filter "name=exegol" --format '{{.Names}}' 2>/dev/null | head -1)
[[ -n "$detected" ]] && EXEGOL_CONTAINER="$detected"
fi
# Last statement must not leak a non-zero status under `set -e` (this
# function runs top-level; a falsy [[ -n ]] above would otherwise abort dotsec).
# Exegol containers are per-engagement (exegol-<target>, see __exegol_name).
# EXEGOL_CONTAINER stays unset unless the user forces one in their config —
# no auto-detection, which would otherwise pin every engagement to whichever
# exegol-* container happens to be running.
return 0
}

Expand Down
52 changes: 32 additions & 20 deletions lib/engagement.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ cmd_new() {
*)
if [[ -z "$target" ]]; then
target="$1"
elif [[ "$1" == "." || "$1" == ".." || "$1" == /* || "$1" == ./* || "$1" == ../* || "$1" == "~"/* ]]; then
# A path-like 2nd positional sets WHERE to create the workspace:
# dotsec new swiss_post . → ./swiss_post
# dotsec new swiss_post ~/bb/ywh → ~/bb/ywh/swiss_post
ws_root="$1"
elif [[ -z "$domain" ]]; then
domain="$1"
fi
Expand All @@ -31,11 +36,14 @@ cmd_new() {
__require_docker

if [[ -z "$target" ]]; then
printf '%b\n' "${RED}[!] Usage: dotsec new [-w <workspace_root>] <target> [domain]${RESET}" >&2
printf '%b\n' "${RED}[!] Usage: dotsec new <target> [domain] [path|-w <root>]${RESET}" >&2
printf '%b\n' " ${DIM}path: '.' or a dir → create the workspace there (e.g. dotsec new swiss_post .)${RESET}" >&2
exit 1
fi

[[ -z "$domain" ]] && domain="${target}"
# Docker mounts (proxy, Exegol) need an absolute workspace root.
ws_root="$(realpath -m "$ws_root" 2>/dev/null || echo "$ws_root")"
local ws="${ws_root}/${target}"

printf '%b\n' "${BOLD}${GREEN}▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀${RESET}"
Expand All @@ -45,6 +53,11 @@ cmd_new() {
# 1. Workspace structure
printf '%b\n' " ${DIM}[1/6]${RESET} ${DIM}Creating workspace...${RESET}"
mkdir -p "${ws}"/{recon/{passive,active},scans/{ports,web,vuln},exploits/{pocs,payloads},loot/{credentials,data},logs,report/assets,replays/{recon,scan,exploit,post,report,monitor},keys}
# Default ACLs so files the root Exegol/proxy containers create in the
# workspace stay editable from the host (your user keeps rwx by inheritance).
if command -v setfacl >/dev/null 2>&1; then
setfacl -R -m "u:$(id -u):rwX" -m "d:u:$(id -u):rwX" "$ws" 2>/dev/null || true
fi

# 2. Copy and fill .env
printf '%b\n' " ${DIM}[2/6]${RESET} ${DIM}Setting up dotenv...${RESET}"
Expand Down Expand Up @@ -86,14 +99,14 @@ DOC
printf '%b\n' " ${DIM}[4/6]${RESET} ${DIM}Starting mitmproxy...${RESET}"
DOTSEC_WORKSPACE_ROOT="${ws_root}" TARGET="${target}" proxy_up

# 5. Ensure Exegol is running with workspace mounted
printf '%b\n' " ${DIM}[5/6]${RESET} ${DIM}Ensuring Exegol container...${RESET}"
__exegol_ensure_running "$target" "$ws_root"
# 5. Create the per-engagement Exegol container (workspace mounted, my-resources)
printf '%b\n' " ${DIM}[5/6]${RESET} ${DIM}Creating Exegol container...${RESET}"
__exegol_ensure_running "$target" "$ws" || true

# 6. Spawn tmux inside Exegol
# 6. Spawn tmux inside Exegol (engagement workspace is mounted at /workspace)
printf '%b\n' " ${DIM}[6/6]${RESET} ${DIM}Creating tmux session in Exegol...${RESET}"
local container="${EXEGOL_CONTAINER:-exegol}"
local load_cmd="source /workspace/${target}/.env; [ -f /workspace/${target}/.env.secrets ] && source /workspace/${target}/.env.secrets; export TARGET=${target} DOMAIN=${domain}; clear"
local container; container="$(__exegol_name "$target")"
local load_cmd="source /workspace/.env; [ -f /workspace/.env.secrets ] && source /workspace/.env.secrets; export TARGET=${target} DOMAIN=${domain}; clear"
__exegol_tmux_spawn "$container" "$target" "$load_cmd"

echo ""
Expand Down Expand Up @@ -206,7 +219,7 @@ cmd_archive() {
exit 1
fi

local container="${EXEGOL_CONTAINER:-exegol}"
local container; container="$(__exegol_name "$target")"
local timestamp
timestamp=$(date '+%Y%m%d-%H%M%S')
local archive_name="${target}-${timestamp}.tar.gz"
Expand Down Expand Up @@ -288,9 +301,8 @@ cmd_rm() {

[[ $do_archive -eq 1 ]] && cmd_archive "$target"

local container="${EXEGOL_CONTAINER:-exegol}"
docker rm -f "mitmproxy-${target}" "oob-${target}" >/dev/null 2>&1 || true
docker exec "$container" tmux kill-session -t "$target" >/dev/null 2>&1 || true
# Remove the per-engagement containers (never an externally forced one)
docker rm -f "mitmproxy-${target}" "oob-${target}" "exegol-${target}" >/dev/null 2>&1 || true

rm -rf "$ws" 2>/dev/null || true
if [[ -d "$ws" ]]; then
Expand All @@ -313,7 +325,7 @@ cmd_stop() {
printf '%b\n' "${RED}[!] Usage: dotsec stop <target>${RESET}" >&2
exit 1
fi
local container="${EXEGOL_CONTAINER:-exegol}"
local container="exegol-${target}"

printf '%b\n' "${BOLD}${YELLOW}Stopping${RESET} ${CYAN}${target}${RESET}..."

Expand All @@ -325,12 +337,12 @@ cmd_stop() {
printf '%b\n' " ${DIM}Proxy: already stopped${RESET}"
fi

# Kill tmux session in Exegol
if docker exec "$container" tmux has-session -t "$target" 2>/dev/null; then
printf '%b\n' " ${DIM}Tmux session...${RESET}"
docker exec "$container" tmux kill-session -t "$target" 2>/dev/null || true
# Stop the Exegol container
if docker ps --filter "name=^${container}$" --format '{{.Names}}' 2>/dev/null | grep -q .; then
printf '%b\n' " ${DIM}Exegol container...${RESET}"
docker stop "$container" >/dev/null 2>&1 || true
else
printf '%b\n' " ${DIM}Tmux: no session${RESET}"
printf '%b\n' " ${DIM}Exegol: not running${RESET}"
fi

# Stop dashboard
Expand All @@ -352,7 +364,7 @@ cmd_restart() {
exit 1
fi
local ws="${WORKSPACE_ROOT:-/workspace}/${target}"
local container="${EXEGOL_CONTAINER:-exegol}"
local container; container="$(__exegol_name "$target")"

if [[ ! -d "$ws" ]] || [[ ! -f "${ws}/.env" ]]; then
printf '%b\n' "${RED}[!] Engagement not found: ${target}${RESET}" >&2
Expand All @@ -375,11 +387,11 @@ cmd_restart() {

# Ensure Exegol
printf '%b\n' " ${DIM}Exegol...${RESET}"
__exegol_ensure_running "$target"
__exegol_ensure_running "$target" "$ws" || true

# Recreate tmux session
printf '%b\n' " ${DIM}Tmux session...${RESET}"
local load_cmd="source /workspace/${target}/.env; [ -f /workspace/${target}/.env.secrets ] && source /workspace/${target}/.env.secrets; export TARGET=${target} DOMAIN=${domain}; clear"
local load_cmd="source /workspace/.env; [ -f /workspace/.env.secrets ] && source /workspace/.env.secrets; export TARGET=${target} DOMAIN=${domain}; clear"
__exegol_tmux_spawn "$container" "$target" "$load_cmd"

printf '%b\n' " ${GREEN}Environment restarted${RESET}"
Expand Down
120 changes: 67 additions & 53 deletions lib/exegol.sh
Original file line number Diff line number Diff line change
@@ -1,72 +1,80 @@
#!/usr/bin/env bash
# ─── lib/exegol.sh ─── Exegol container and tmux helpers ──

# ── Exegol helpers ───────────────────────────────────────
# Per-engagement Exegol container name. EXEGOL_CONTAINER overrides it (use an
# already-running container instead of creating exegol-<target>).
__exegol_name() {
echo "${EXEGOL_CONTAINER:-exegol-${1}}"
}

# __exegol_ensure_running <target> [engagement_workspace]
# Create the per-engagement Exegol container (exegol-<target>) with the
# engagement workspace mounted as /workspace and my-resources deployed, or just
# start it if it already exists. Best-effort: returns non-zero only if it can't
# bring the container up.
__exegol_ensure_running() {
local target="${1}"
local ws_root="${2:-${WORKSPACE_ROOT}}"
local container="${EXEGOL_CONTAINER:-exegol}"
local ws="${2:-${WORKSPACE_ROOT}/${target}}"
local container; container="$(__exegol_name "$target")"

# Already running? Check mount and return
# Already running?
if docker ps --filter "name=^${container}$" --format '{{.Names}}' 2>/dev/null | grep -q .; then
local mount_src
mount_src=$(docker inspect "$container" --format '{{range .Mounts}}{{if eq .Destination "/workspace"}}{{.Source}}{{end}}{{end}}' 2>/dev/null || echo "")
if [[ -n "$mount_src" ]] && [[ "$mount_src" = "${ws_root}" ]]; then
return 0
fi
return 0
fi

# Container exists (running or stopped)? Start it — fast
# Exists but stopped → start it.
if docker ps -a --filter "name=^${container}$" --format '{{.Names}}' 2>/dev/null | grep -q .; then
# Stop if running with wrong mount
if docker ps --filter "name=^${container}$" --format '{{.Names}}' 2>/dev/null | grep -q .; then
docker stop "$container" >/dev/null 2>&1
fi
# Recreate with correct mount if needed
local mount_src
mount_src=$(docker inspect "$container" --format '{{range .Mounts}}{{if eq .Destination "/workspace"}}{{.Source}}{{end}}{{end}}' 2>/dev/null || echo "")
if [[ -z "$mount_src" ]] || [[ "$mount_src" != "${ws_root}" ]]; then
docker rm "$container" >/dev/null 2>&1
if command -v exegol &>/dev/null; then
exegol start -w "${ws_root}" "$container" &>/dev/null &
else
docker run -d --name "$container" -v "${ws_root}:/workspace" --network host "nwodtuhs/exegol:free" sleep infinity
fi
else
docker start "$container" >/dev/null 2>&1
fi
docker start "$container" >/dev/null 2>&1 || true
return 0
fi

# Container doesn't exist — start in background (may take time)
printf '%b\n' " ${DIM}Exegol first launch (pulling image)...${RESET}"
if command -v exegol &>/dev/null; then
exegol start -w "${ws_root}" "$container" &>/dev/null &
# A forced EXEGOL_CONTAINER that doesn't exist: don't try to create it.
if [[ -n "${EXEGOL_CONTAINER:-}" ]]; then
printf '%b\n' " ${YELLOW}[!]${RESET} ${DIM}EXEGOL_CONTAINER=${EXEGOL_CONTAINER} not found — start it yourself${RESET}" >&2
return 1
fi
# Create it. Prefer the exegol CLI: it deploys my-resources and mounts the
# workspace. stdin=/dev/null makes the post-create auto-attach fail cleanly
# and leaves the container running detached (we connect via tmux instead).
if command -v exegol >/dev/null 2>&1; then
printf '%b\n' " ${DIM}Creating Exegol container ${container} (first run can take a moment)...${RESET}"
# Host-side ACLs (set by dotsec at workspace creation) keep root-created
# files editable from the host, so --update-fs isn't needed here.
exegol start "${target}" "${EXEGOL_IMAGE:-free}" -w "${ws}" --accept-eula </dev/null >/dev/null 2>&1 || true
else
printf '%b\n' " ${DIM}exegol CLI not found — using docker run fallback...${RESET}"
docker run -d --name "$container" \
-v "${ws_root}:/workspace" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "${ws}:/workspace" \
-v "${HOME}/.exegol/my-resources:/opt/my-resources" \
--network host \
"nwodtuhs/exegol:free" sleep infinity
nwodtuhs/exegol:free sleep infinity >/dev/null 2>&1 || true
fi
if docker ps --filter "name=^${container}$" --format '{{.Names}}' 2>/dev/null | grep -q .; then
return 0
fi
printf '%b\n' " ${YELLOW}[!]${RESET} ${DIM}Exegol container ${container} did not come up${RESET}" >&2
return 1
}

__exegol_tmux_spawn() {
local container="${1}"
local session_name="${2}"
local load_cmd="${3:-}"

# Wait for container to be ready (up to 120s for first pull)
# Container not present at all → skip without the long wait.
if ! docker ps -a --filter "name=^${container}$" --format '{{.Names}}' 2>/dev/null | grep -q .; then
printf '%b\n' " ${YELLOW}[!]${RESET} ${DIM}No Exegol container ${container} — run${RESET} ${YELLOW}dotsec tmux create ${session_name}${RESET} ${DIM}once it's up${RESET}" >&2
return 0
fi

# Wait for it to be exec-able (short: it was just created/started).
printf '%b\n' " ${DIM}Waiting for Exegol container...${RESET}"
local waited=0
while ! docker exec "$container" true 2>/dev/null && [[ $waited -lt 120 ]]; do
sleep 3; ((waited+=3))
while ! docker exec "$container" true 2>/dev/null && [[ $waited -lt 60 ]]; do
sleep 2; ((waited+=2)) || true
printf '.' >&2
done
echo ""
echo "" >&2
if ! docker exec "$container" true 2>/dev/null; then
printf '%b\n' " ${YELLOW}[!]${RESET} ${DIM}Exegol not ready after 2min — tmux will be created later${RESET}" >&2
printf '%b\n' " ${DIM}Run${RESET} ${YELLOW}dotsec tmux attach${RESET} ${DIM}once container is up${RESET}" >&2
printf '%b\n' " ${YELLOW}[!]${RESET} ${DIM}Exegol not ready — ${RESET}${YELLOW}dotsec tmux create ${session_name}${RESET}" >&2
return 0
fi

Expand Down Expand Up @@ -97,10 +105,16 @@ __exegol_tmux_spawn() {
# ── Exegol ───────────────────────────────────────────────
cmd_exegol() {
local action="${1:-shell}"; shift || true
local container="${EXEGOL_CONTAINER:-exegol}"
local target="${TARGET:-}"
local container; container="$(__exegol_name "$target")"

__require_docker

if [[ -z "${EXEGOL_CONTAINER:-}" && -z "$target" ]]; then
printf '%b\n' "${RED}[!] No engagement loaded — ${RESET}${YELLOW}dotsec load <target>${RESET} ${DIM}first${RESET}" >&2
exit 1
fi

local docker_tty=""
[[ -t 0 ]] && docker_tty="-it"

Expand All @@ -115,7 +129,6 @@ cmd_exegol() {
printf '%b\n' "${DIM}Running Exegol tool setup (uv + pnpm)...${RESET}"
if ! docker ps --filter "name=^${container}$" --format '{{.Names}}' | grep -q .; then
printf '%b\n' "${YELLOW}[!]${RESET} ${DIM}Container${RESET} ${YELLOW}${container}${RESET} ${DIM}not running.${RESET}" >&2
printf '%b\n' " ${DIM}Start it with:${RESET} ${YELLOW}exegol start ${container}${RESET}" >&2
exit 1
fi
docker exec ${docker_tty} "$container" bash /opt/my-resources/setup/load_user_setup.sh
Expand All @@ -131,20 +144,20 @@ cmd_exegol() {
# Instant pentest-ready tmux session, with or without engagement loaded
cmd_spawn() {
local session_name="${1:-${TARGET:-pentest}}"
local ws="${WORKSPACE:-}"
local target="${TARGET:-${session_name}}"
local ws="${WORKSPACE:-${WORKSPACE_ROOT}/${target}}"
local envfile="${ws}/.env"
local container="${EXEGOL_CONTAINER:-exegol}"
local container; container="$(__exegol_name "$target")"

__require_docker

# Check if target is an engagement with .env → use Exegol path
# Engagement with .env → source it inside (workspace is mounted at /workspace)
local exegol_env=""
[[ -n "$ws" ]] && [[ -f "$envfile" ]] && exegol_env="/workspace/${TARGET:-${session_name}}/.env"
[[ -f "$envfile" ]] && exegol_env="/workspace/.env"

printf '%b\n' "${DIM}Spawning tmux session in Exegol:${RESET} ${CYAN}${session_name}${RESET}"

# Ensure Exegol is running
__exegol_ensure_running "${TARGET:-${session_name}}"
__exegol_ensure_running "$target" "$ws" || true

local load_cmd=""
[[ -n "$exegol_env" ]] && load_cmd="source ${exegol_env}; clear"
Expand All @@ -161,7 +174,7 @@ cmd_tmux() {
local action="${1:-}"; shift || true
local target="${TARGET:-}"
[[ -z "$target" ]] && target="${1:-}"
local container="${EXEGOL_CONTAINER:-exegol}"
local container; container="$(__exegol_name "$target")"

__require_docker

Expand All @@ -172,8 +185,9 @@ cmd_tmux() {
;;
create)
[[ -z "$target" ]] && { printf '%b\n' "${RED}[!] Usage: dotsec tmux create <target>${RESET}" >&2; exit 1; }
__exegol_ensure_running "$target"
__exegol_tmux_spawn "$container" "$target"
__exegol_ensure_running "$target" "${WORKSPACE_ROOT}/${target}" || true
local load_cmd="source /workspace/.env 2>/dev/null; [ -f /workspace/.env.secrets ] && source /workspace/.env.secrets; export TARGET=${target}; clear"
__exegol_tmux_spawn "$container" "$target" "$load_cmd"
printf '%b\n' "${GREEN}Session${RESET} ${CYAN}${target}${RESET} ${DIM}created in Exegol${RESET}"
;;
kill|k)
Expand Down
9 changes: 4 additions & 5 deletions lib/status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
# ─── lib/status.sh ─── engagement status overview ──

__status_global() {
local exegol="${EXEGOL_CONTAINER:-exegol}"
local ex_state="stopped" db_state="down"
docker ps --filter "name=^${exegol}$" --format '{{.Names}}' 2>/dev/null | grep -q . && ex_state="running"
local db_state="down" ex_count
ex_count=$(docker ps --filter "name=exegol-" --format '{{.Names}}' 2>/dev/null | grep -c . || true)
docker ps --filter "name=dotsec-homer" --format '{{.Names}}' 2>/dev/null | grep -q . \
&& db_state="up (127.0.0.1:${HOMER_PORT:-9997})"
printf '%b\n' "${BOLD}${CYAN}Global${RESET} ${DIM}Exegol:${RESET} ${ex_state} ${DIM}Dashboard:${RESET} ${db_state}"
printf '%b\n' "${BOLD}${CYAN}Global${RESET} ${DIM}Exegol:${RESET} ${ex_count:-0} running ${DIM}Dashboard:${RESET} ${db_state}"
return 0
}

Expand All @@ -22,7 +21,7 @@ __status_engagement() {
proxy="up 127.0.0.1:${pport} / ${wport}"
fi
tmuxs="—"
exegol="${EXEGOL_CONTAINER:-exegol}"
exegol="$(__exegol_name "$target")"
if docker ps --filter "name=^${exegol}$" --format '{{.Names}}' 2>/dev/null | grep -q .; then
docker exec "$exegol" tmux has-session -t "$target" 2>/dev/null && tmuxs="session present"
fi
Expand Down
Loading
Loading