-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall-hexstrike-opencode.sh
More file actions
808 lines (755 loc) · 27.8 KB
/
Copy pathinstall-hexstrike-opencode.sh
File metadata and controls
808 lines (755 loc) · 27.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
#!/usr/bin/env bash
#
# install-hexstrike-opencode.sh
# Interactive installer for HexStrike AI + OpenCode on Ubuntu or Kali Linux.
#
# Flow:
# 1. Detect/confirm distro.
# 2. Install base prerequisites (python venv, git, tmux, curl, build tools).
# 3. SCAN the system for existing pentest tools and report coverage.
# 4. Ask which pentest use-case(s) you actually need (network/web/exploit/...).
# 5. Compute only the MISSING tools for those use-cases and install just those
# (Kali users can alternatively opt into the full kali-linux-default).
# 6. Install Node (nvm) + OpenCode, clone HexStrike, set up venv.
# 7. Patch the known MCP health-check timeout, write a single OpenCode config.
# 8. Validate, optional live health check, then print a run guide.
#
# Safe to re-run. Run as the operating user (NOT via sudo); it calls sudo itself
# only for package installs.
set -uo pipefail
# ----------------------------------------------------------------------------
# Presentation helpers
# ----------------------------------------------------------------------------
if [[ -t 1 ]]; then
BOLD="$(tput bold)"; RESET="$(tput sgr0)"
RED="$(tput setaf 1)"; GREEN="$(tput setaf 2)"
YELLOW="$(tput setaf 3)"; BLUE="$(tput setaf 4)"; CYAN="$(tput setaf 6)"
else
BOLD=""; RESET=""; RED=""; GREEN=""; YELLOW=""; BLUE=""; CYAN=""
fi
info() { echo "${BLUE}[*]${RESET} $*"; }
success() { echo "${GREEN}[✓]${RESET} $*"; }
warn() { echo "${YELLOW}[!]${RESET} $*"; }
err() { echo "${RED}[✗]${RESET} $*" >&2; }
step() { echo; echo "${BOLD}${CYAN}==> $*${RESET}"; }
die() { err "$*"; exit 1; }
confirm() {
local prompt="${1:-Continue?}" answer
read -r -p "${prompt} [Y/n] " answer
answer="${answer:-Y}"; [[ "$answer" =~ ^[Yy]$ ]]
}
# ----------------------------------------------------------------------------
# Globals
# ----------------------------------------------------------------------------
DISTRO=""
RUN_USER="$(whoami)"
USER_HOME="$HOME"
HEXSTRIKE_DIR="${USER_HOME}/hexstrike-ai"
VENV_DIR="${HEXSTRIKE_DIR}/hexstrike-env"
VENV_PY="${VENV_DIR}/bin/python3"
OC_CONFIG_DIR="${USER_HOME}/.config/opencode"
OC_CONFIG="${OC_CONFIG_DIR}/opencode.json"
HEXSTRIKE_REPO="https://github.com/0x4m4/hexstrike-ai.git"
SERVER_PORT="8888"
VENV_PYTHON_FOR_PIP="python3"
INSTALL_LOG="/tmp/hexstrike-install.log"
# Official Go version to install when apt's Go is missing or incompatible with
# the go.mod format required by common security tools (needs 1.23.x line).
GO_VERSION="1.23.4"
ALL_CATS="network web exploit password osint wireless forensics cloud"
declare -a SELECTED_CATS=()
declare -a MISSING=()
# Detect shell and rc file for PATH persistence (bash, zsh, or other)
detect_shell() {
local shell_name
shell_name="$(basename "$SHELL")"
case "$shell_name" in
bash) echo "bash" ;;
zsh) echo "zsh" ;;
*) echo "bash" ;; # default to bash if unknown
esac
}
SHELL_TYPE="$(detect_shell)"
case "$SHELL_TYPE" in
bash) RC_FILE="${USER_HOME}/.bashrc" ;;
zsh) RC_FILE="${USER_HOME}/.zshrc" ;;
esac
# Make tools installed via Go / cargo / pipx visible to `have` checks this run.
# Prepend /usr/local/go/bin so official Go shadows system gccgo/old Go.
export PATH="/usr/local/go/bin:$PATH:${USER_HOME}/go/bin:${USER_HOME}/.cargo/bin:${USER_HOME}/.local/bin"
# ----------------------------------------------------------------------------
# Tool catalogue
# category_tools <cat> echoes lines: "binary method target"
# method in: apt | pip | go | gem | cargo
# ----------------------------------------------------------------------------
category_tools() {
case "$1" in
network)
cat <<'EOF'
nmap apt nmap
masscan apt masscan
arp-scan apt arp-scan
nbtscan apt nbtscan
enum4linux apt enum4linux
enum4linux-ng pip enum4linux-ng
smbmap apt smbmap
responder apt responder
autorecon pip autorecon
EOF
if [[ "$DISTRO" == "kali" ]]; then
echo "rustscan apt rustscan"
else
echo "rustscan cargo rustscan"
fi
;;
web)
cat <<'EOF'
gobuster apt gobuster
ffuf apt ffuf
feroxbuster apt feroxbuster
nikto apt nikto
sqlmap apt sqlmap
wpscan apt wpscan
whatweb apt whatweb
wafw00f apt wafw00f
dirsearch pip dirsearch
arjun pip arjun
uro pip uro
nuclei go github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
subfinder go github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
katana go github.com/projectdiscovery/katana/cmd/katana@latest
httpx go github.com/projectdiscovery/httpx/cmd/httpx@latest
dalfox go github.com/hahwul/dalfox/v2@latest
waybackurls go github.com/tomnomnom/waybackurls@latest
hakrawler go github.com/hakluke/hakrawler@latest
anew go github.com/tomnomnom/anew@latest
qsreplace go github.com/tomnomnom/qsreplace@latest
EOF
;;
exploit)
cat <<'EOF'
gdb apt gdb
gdb-multiarch apt gdb-multiarch
radare2 apt radare2
checksec apt checksec
searchsploit apt exploitdb
msfconsole apt metasploit-framework
pwntools pip pwntools
ropper pip ropper
ROPgadget pip ROPgadget
one_gadget gem one_gadget
EOF
[[ "$DISTRO" == "kali" ]] && echo "ghidra apt ghidra"
;;
password)
cat <<'EOF'
hydra apt hydra
john apt john
hashcat apt hashcat
medusa apt medusa
patator apt patator
hash-identifier apt hash-identifier
EOF
;;
osint)
cat <<'EOF'
theHarvester apt theharvester
recon-ng apt recon-ng
amass apt amass
spiderfoot apt spiderfoot
fierce apt fierce
dnsenum apt dnsenum
EOF
;;
wireless)
cat <<'EOF'
aircrack-ng apt aircrack-ng
kismet apt kismet
reaver apt reaver
EOF
;;
forensics)
cat <<'EOF'
binwalk apt binwalk
foremost apt foremost
exiftool apt libimage-exiftool-perl
steghide apt steghide
scalpel apt scalpel
testdisk apt testdisk
sleuthkit apt sleuthkit
zsteg gem zsteg
EOF
;;
cloud)
cat <<'EOF'
prowler pip prowler
scout pip scout-suite
checkov pip checkov
EOF
;;
esac
}
cat_label() {
case "$1" in
network) echo "Network pentesting" ;;
web) echo "Web application pentesting" ;;
exploit) echo "Exploit dev / binary exploitation" ;;
password) echo "Password cracking / brute force" ;;
osint) echo "OSINT / recon" ;;
wireless) echo "Wireless" ;;
forensics) echo "Forensics / stego" ;;
cloud) echo "Cloud security" ;;
esac
}
have() { command -v "$1" >/dev/null 2>&1; }
# ----------------------------------------------------------------------------
# 0. Preflight
# ----------------------------------------------------------------------------
banner() {
echo "${BOLD}${CYAN}"
echo " +-----------------------------------------------+"
echo " | HexStrike AI + OpenCode Installer |"
echo " +-----------------------------------------------+"
echo "${RESET}"
echo " User: ${RUN_USER}"
echo " Home: ${USER_HOME}"
echo " Install dir: ${HEXSTRIKE_DIR}"
echo
}
require_sudo() {
command -v sudo >/dev/null 2>&1 || die "sudo is required but not installed."
info "Requesting sudo up front (used for apt installs)..."
sudo -v || die "Could not obtain sudo."
}
ask_system() {
step "Select your system"
echo " 1) Ubuntu"
echo " 2) Kali Linux"
echo
local choice
while true; do
read -r -p "Which system is this? [1/2] " choice
case "$choice" in
1) DISTRO="ubuntu"; break ;;
2) DISTRO="kali"; break ;;
*) warn "Enter 1 or 2." ;;
esac
done
if [[ -r /etc/os-release ]]; then
local detected; detected="$(. /etc/os-release; echo "${ID:-unknown}")"
if [[ "$detected" != "$DISTRO" && "$detected" != "unknown" ]]; then
warn "You chose '${DISTRO}' but /etc/os-release reports '${detected}'."
confirm "Proceed anyway?" || die "Aborted by user."
fi
fi
success "Target system: ${DISTRO}"
}
# ----------------------------------------------------------------------------
# 1. Base prerequisites
# ----------------------------------------------------------------------------
install_base() {
step "Installing base prerequisites"
sudo apt update
sudo apt install -y \
python3 python3-venv python3-pip \
git tmux curl wget ca-certificates \
build-essential jq \
|| die "Base prerequisite install failed."
success "Base prerequisites installed."
}
# ----------------------------------------------------------------------------
# 2. Scan existing pentest toolset
# ----------------------------------------------------------------------------
scan_toolset() {
step "Scanning system for existing pentest tools"
local cat total present b m t
for cat in $ALL_CATS; do
total=0; present=0
while read -r b m t; do
[[ -z "${b:-}" ]] && continue
((total++)); have "$b" && ((present++))
done < <(category_tools "$cat")
printf " %-34s %s%d/%d%s\n" "$(cat_label "$cat")" \
"$( [[ $present -eq $total ]] && echo "$GREEN" || echo "$YELLOW" )" \
"$present" "$total" "$RESET"
done
echo
info "This reflects tools already on your PATH. Nothing installed yet."
}
# ----------------------------------------------------------------------------
# 3. Choose use-cases (multi-select)
# ----------------------------------------------------------------------------
select_usecases() {
step "Which pentest use-case(s) will you use this for?"
local -a keys=(network web exploit password osint wireless forensics cloud)
local i=1 k
for k in "${keys[@]}"; do printf " %d) %s\n" "$i" "$(cat_label "$k")"; ((i++)); done
echo
echo " Enter numbers comma-separated (e.g. 1,2 for network + web)."
echo " Or type 'all' for everything."
local sel; read -r -p "Selection: " sel
SELECTED_CATS=()
if [[ "$sel" =~ ^[Aa][Ll][Ll]$ ]]; then
SELECTED_CATS=("${keys[@]}")
else
IFS=',' read -ra nums <<< "$sel"
local n idx
for n in "${nums[@]}"; do
n="$(echo "$n" | tr -d '[:space:]')"
[[ "$n" =~ ^[0-9]+$ ]] || { warn "Ignoring invalid entry: '$n'"; continue; }
idx=$((n-1))
if [[ $idx -ge 0 && $idx -lt ${#keys[@]} ]]; then
SELECTED_CATS+=("${keys[$idx]}")
else
warn "Out of range: $n"
fi
done
fi
[[ ${#SELECTED_CATS[@]} -gt 0 ]] || die "No valid use-cases selected."
echo
info "Selected: $(for c in "${SELECTED_CATS[@]}"; do printf '%s; ' "$(cat_label "$c")"; done)"
}
# ----------------------------------------------------------------------------
# 4. Compute + install missing tools
# ----------------------------------------------------------------------------
compute_missing() {
MISSING=()
local cat b m t key seen=""
for cat in "${SELECTED_CATS[@]}"; do
while read -r b m t; do
[[ -z "${b:-}" ]] && continue
have "$b" && continue
key="${b}|${m}|${t}"
case "$seen" in *"|$key|"*) continue ;; esac
seen="${seen}|$key|"; MISSING+=("$key")
done < <(category_tools "$cat")
done
}
go_is_compatible() {
# Compatible = Go present AND major.minor is >= 1.21 and <= 1.24.
# (>=1.21 to build modern modules; <=1.24 to avoid the 1.25+ go.mod format
# that several tools' pinned module versions reject.)
have go || return 1
local v major minor
v="$(go version 2>/dev/null | awk '{print $3}' | sed 's/^go//')"
[[ -n "$v" ]] || return 1
major="${v%%.*}"; minor="$(echo "$v" | cut -d. -f2)"
[[ "$major" == "1" ]] || return 1
[[ "$minor" =~ ^[0-9]+$ ]] || return 1
(( minor >= 21 && minor <= 24 ))
}
install_official_go() {
local existing=""
have go && existing="$(go version 2>/dev/null | awk '{print $3}')"
if [[ -n "$existing" ]]; then
warn "Existing Go (${existing}) is incompatible with common tool modules."
else
info "No usable Go found."
fi
info "Installing official Go ${GO_VERSION} to /usr/local/go..."
local arch tarball url
case "$(uname -m)" in
x86_64) arch="amd64" ;;
aarch64) arch="arm64" ;;
*) warn "Unknown arch $(uname -m); defaulting to amd64."; arch="amd64" ;;
esac
tarball="go${GO_VERSION}.linux-${arch}.tar.gz"
url="https://go.dev/dl/${tarball}"
# Remove apt Go to prevent PATH shadowing of the official toolchain.
if dpkg -l 2>/dev/null | grep -q '^ii golang-go'; then
info "Removing apt golang-go to avoid PATH conflicts..."
sudo apt remove -y golang-go >>"$INSTALL_LOG" 2>&1 || true
fi
cd /tmp || return 1
if ! curl -fsSLo "$tarball" "$url" >>"$INSTALL_LOG" 2>&1; then
err "Failed to download ${url}. Check https://go.dev/dl/ for the current"
err "version and set GO_VERSION at the top of this script accordingly."
return 1
fi
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "$tarball" >>"$INSTALL_LOG" 2>&1 || { err "tar extract failed."; return 1; }
rm -f "$tarball"
export PATH="/usr/local/go/bin:$PATH:${USER_HOME}/go/bin"
if ! grep -qs '/usr/local/go/bin' "$RC_FILE"; then
echo 'export PATH=/usr/local/go/bin:$PATH:$HOME/go/bin' >> "$RC_FILE"
info "Added Go paths to $RC_FILE (prepended for priority)"
fi
if go_is_compatible; then
success "Official Go installed: $(go version | awk '{print $3}')"
else
warn "Go install completed but version check still fails; see ${INSTALL_LOG}"
fi
}
ensure_toolchain() {
case "$1" in
pip)
# Ensure pipx exists for isolated CLI-tool installs on PEP 668 systems.
have pipx && return 0
info "Installing pipx (for isolated Python CLI tools)..."
sudo apt install -y pipx >/dev/null 2>&1 || \
pip3 install --break-system-packages pipx >/dev/null 2>&1 || \
warn "pipx install failed; will fall back to pip/apt per tool."
pipx ensurepath >/dev/null 2>&1 || {
if ! grep -qs '.local/bin' "$RC_FILE"; then
echo 'export PATH=$PATH:$HOME/.local/bin' >> "$RC_FILE"
fi
export PATH="$PATH:${USER_HOME}/.local/bin"
}
;;
go)
# Security tools' go.mod files require the 1.23.x version-string format.
# apt's Go can be too old OR too new (e.g. 1.25.x writes a version string
# these modules reject). If a compatible Go isn't present, install the
# official toolchain to /usr/local/go.
if go_is_compatible; then
info "Compatible Go already present ($(go version 2>/dev/null | awk '{print $3}'))."
return 0
fi
install_official_go
;;
gem)
have gem && return 0
info "Installing Ruby (needed for gem-based tools)..."
sudo apt install -y ruby ruby-dev || warn "Ruby install failed."
;;
cargo)
export PATH="$PATH:${USER_HOME}/.cargo/bin"
have cargo && return 0
info "Installing Rust/cargo (needed for cargo-based tools)..."
sudo apt install -y cargo rustc 2>/dev/null || {
warn "apt cargo unavailable; installing rustup..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# shellcheck disable=SC1091
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
}
if ! grep -qs '.cargo/bin' "$RC_FILE"; then
echo 'export PATH=$PATH:$HOME/.cargo/bin' >> "$RC_FILE"
info "Added cargo bin to $RC_FILE"
fi
;;
esac
}
install_one() {
local binary="$1" method="$2" target="$3"
# All command output is appended to $INSTALL_LOG so failures are inspectable.
case "$method" in
apt)
if sudo apt install -y "$target" >>"$INSTALL_LOG" 2>&1; then
return 0
fi
# Special-case tools frequently absent from apt: try a language installer.
case "$binary" in
rustscan)
ensure_toolchain cargo
cargo install rustscan >>"$INSTALL_LOG" 2>&1
;;
*) return 1 ;;
esac
;;
pip)
# Modern Kali/Debian Python is externally-managed (PEP 668). Prefer pipx
# for CLI tools; fall back to pip --break-system-packages, then to apt
# (many of these tools also ship as apt packages of the same name).
if have pipx; then
pipx install "$target" >>"$INSTALL_LOG" 2>&1 && return 0
fi
pip3 install --break-system-packages "$target" >>"$INSTALL_LOG" 2>&1 && return 0
sudo apt install -y "$target" >>"$INSTALL_LOG" 2>&1
;;
go)
GOBIN="${USER_HOME}/go/bin" GOPATH="${USER_HOME}/go" \
go install -v "$target" >>"$INSTALL_LOG" 2>&1
;;
gem)
sudo gem install "$target" >>"$INSTALL_LOG" 2>&1
;;
cargo)
cargo install "$target" >>"$INSTALL_LOG" 2>&1
;;
*)
echo "unknown method: $method" >>"$INSTALL_LOG"; return 1
;;
esac
}
install_targeted() {
compute_missing
if [[ ${#MISSING[@]} -eq 0 ]]; then
success "All tools for your selected use-cases are already installed."
return 0
fi
step "Missing tools for your selection (${#MISSING[@]})"
local entry b m t
for entry in "${MISSING[@]}"; do
IFS='|' read -r b m t <<< "$entry"; printf " %-18s (%s)\n" "$b" "$m"
done
echo
confirm "Install these now?" || { warn "Skipped tool installation."; return 0; }
# Per-run install log so silent failures become inspectable.
INSTALL_LOG="$(mktemp /tmp/hexstrike-install.XXXXXX.log)"
info "Detailed install output -> ${INSTALL_LOG}"
# Pre-install needed toolchains once (pip=pipx, plus go/gem/cargo).
local methods_needed="" mm
for entry in "${MISSING[@]}"; do
IFS='|' read -r b m t <<< "$entry"
case "$methods_needed" in *"|$m|"*) ;; *) methods_needed="${methods_needed}|$m|";; esac
done
for mm in pip go gem cargo; do
case "$methods_needed" in *"|$mm|"*) ensure_toolchain "$mm" ;; esac
done
mkdir -p "${USER_HOME}/go/bin"
export PATH="$PATH:${USER_HOME}/go/bin:${USER_HOME}/.local/bin"
local okc=0 failc=0
for entry in "${MISSING[@]}"; do
IFS='|' read -r b m t <<< "$entry"
info "Installing ${b} via ${m}..."
if install_one "$b" "$m" "$t" && have "$b"; then
success "installed: ${b}"; ((okc++))
else
warn "could not install: ${b} (${m}:${t}) -- last log lines:"
tail -n 3 "$INSTALL_LOG" 2>/dev/null | sed 's/^/ /'
((failc++))
fi
done
if [[ "$methods_needed" == *"|go|"* ]] && ! grep -qs 'go/bin' "$RC_FILE"; then
echo 'export PATH=$PATH:$HOME/go/bin' >> "$RC_FILE"
info "Added ~/go/bin to PATH in $RC_FILE"
fi
echo
info "Tool install summary: ${GREEN}${okc} ok${RESET}, ${RED}${failc} failed/skipped${RESET}."
}
choose_and_install_tools() {
step "Tool installation strategy"
if [[ "$DISTRO" == "kali" ]]; then
echo " On Kali you can either:"
echo " 1) Install ONLY missing tools for your selected use-cases (targeted, lean)"
echo " 2) Install the full kali-linux-default metapackage (large, covers most)"
echo " 3) Skip tool installation entirely"
local c; read -r -p "Choose [1/2/3] (default 1): " c; c="${c:-1}"
case "$c" in
1) install_targeted ;;
2) warn "kali-linux-default is a large download (several GB)."
if confirm "Proceed with kali-linux-default?"; then
sudo apt install -y kali-linux-default || warn "kali-linux-default reported errors."
else install_targeted; fi ;;
3) warn "Skipped tool installation." ;;
*) warn "Unrecognized choice; doing targeted install."; install_targeted ;;
esac
else
warn "Ubuntu has no kali-linux-default metapackage; installing selected"
warn "tools individually from Ubuntu repos + Go/pip where needed."
sudo add-apt-repository -y universe 2>/dev/null || true
sudo apt update
install_targeted
fi
}
# ----------------------------------------------------------------------------
# 5. Node.js (nvm) + OpenCode
# ----------------------------------------------------------------------------
install_node_opencode() {
step "Installing Node.js (via nvm) and OpenCode"
if [[ ! -d "${USER_HOME}/.nvm" ]]; then
info "Installing nvm..."
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash \
|| die "nvm install failed."
else
info "nvm already present."
fi
export NVM_DIR="${USER_HOME}/.nvm"
# nvm's internals reference unset vars; relax `set -u` while loading/using it.
set +u
# shellcheck disable=SC1091
[[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh"
command -v nvm >/dev/null 2>&1 || { set -u; die "nvm not loaded; open a new shell and re-run."; }
info "Installing Node LTS..."
nvm install --lts || { set -u; die "Node install failed."; }
nvm use --lts
set -u
info "Installing OpenCode..."
npm install -g opencode-ai || die "OpenCode install failed."
success "Node $(node -v) / OpenCode $(opencode --version 2>/dev/null || echo '?') installed."
}
# ----------------------------------------------------------------------------
# 6. HexStrike clone + venv
# ----------------------------------------------------------------------------
install_hexstrike() {
step "Cloning HexStrike and setting up virtualenv"
if [[ -d "$HEXSTRIKE_DIR/.git" ]]; then
info "Repo already cloned; pulling latest."
git -C "$HEXSTRIKE_DIR" pull --ff-only || warn "git pull failed; using existing checkout."
else
git clone "$HEXSTRIKE_REPO" "$HEXSTRIKE_DIR" || die "git clone failed."
fi
cd "$HEXSTRIKE_DIR" || die "Cannot cd into $HEXSTRIKE_DIR"
[[ -d "$VENV_DIR" ]] || python3 -m venv "$VENV_DIR" || die "venv creation failed."
# shellcheck disable=SC1091
source "${VENV_DIR}/bin/activate"
info "Installing Python requirements..."
pip3 install --upgrade pip >/dev/null 2>&1 || true
if [[ -f requirements.txt ]]; then
pip3 install -r requirements.txt || die "pip install -r requirements.txt failed."
else
warn "requirements.txt not found; check the repo layout."
fi
deactivate 2>/dev/null || true
success "HexStrike installed at ${HEXSTRIKE_DIR}"
}
# ----------------------------------------------------------------------------
# 7a. Patch MCP health-check timeout
# ----------------------------------------------------------------------------
patch_timeout() {
step "Patching MCP startup health-check timeout (5s -> 15s)"
local mcp_file="${HEXSTRIKE_DIR}/hexstrike_mcp.py"
[[ -f "$mcp_file" ]] || { warn "hexstrike_mcp.py not found; skipping patch."; return 0; }
if grep -q 'timeout=15)' "$mcp_file"; then info "Already patched."; return 0; fi
if grep -q '/health", timeout=5)' "$mcp_file"; then
sed -i 's#/health", timeout=5)#/health", timeout=15)#' "$mcp_file"
success "Patched health-check timeout to 15s."
else
warn "Expected timeout=5 health line not found (repo may have changed)."
warn "If OpenCode later reports 'Connection closed', bump that timeout manually."
fi
}
# ----------------------------------------------------------------------------
# 7b. OpenCode MCP config (single source of truth)
# ----------------------------------------------------------------------------
write_opencode_config() {
step "Writing OpenCode MCP config"
mkdir -p "$OC_CONFIG_DIR"
if [[ -f "${OC_CONFIG_DIR}/opencode.jsonc" ]]; then
warn "Found opencode.jsonc which can override opencode.json."
mv "${OC_CONFIG_DIR}/opencode.jsonc" "${OC_CONFIG_DIR}/opencode.jsonc.bak.$(date +%s)"
success "Backed up conflicting .jsonc file."
fi
cat > "$OC_CONFIG" <<EOF
{
"\$schema": "https://opencode.ai/config.json",
"mcp": {
"hexstrike": {
"type": "local",
"command": [
"${VENV_PY}",
"${HEXSTRIKE_DIR}/hexstrike_mcp.py",
"--server",
"http://localhost:${SERVER_PORT}"
],
"enabled": true
}
}
}
EOF
if command -v jq >/dev/null 2>&1 && jq empty "$OC_CONFIG" >/dev/null 2>&1; then
success "Config written and JSON validated: ${OC_CONFIG}"
else
success "Config written: ${OC_CONFIG}"
fi
}
# ----------------------------------------------------------------------------
# 8. Validation + live health check
# ----------------------------------------------------------------------------
validate() {
step "Validating installation"
local ok=0 fail=0
check() { local label="$1"; shift; if "$@" >/dev/null 2>&1; then success "$label"; ((ok++)); else err "$label"; ((fail++)); fi; }
check "python3 present" command -v python3
check "git present" command -v git
check "tmux present" command -v tmux
check "venv python exists" test -x "$VENV_PY"
check "hexstrike_server.py exists" test -f "${HEXSTRIKE_DIR}/hexstrike_server.py"
check "hexstrike_mcp.py exists" test -f "${HEXSTRIKE_DIR}/hexstrike_mcp.py"
check "opencode config exists" test -f "$OC_CONFIG"
export NVM_DIR="${USER_HOME}/.nvm"
set +u
# shellcheck disable=SC1091
[[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh"
set -u
check "node present" command -v node
check "opencode present" command -v opencode
if "$VENV_PY" -c "import mcp" >/dev/null 2>&1; then
success "venv has 'mcp' module"; ((ok++))
else
err "venv missing 'mcp' module (re-run pip install -r requirements.txt)"; ((fail++))
fi
echo
info "Validation: ${GREEN}${ok} passed${RESET}, ${RED}${fail} failed${RESET}."
echo
confirm "Start the HexStrike server now and run a live health check?" && live_health_check
}
live_health_check() {
step "Live health check"
info "Starting server in tmux session 'hexstrike'..."
tmux kill-session -t hexstrike 2>/dev/null || true
tmux new-session -d -s hexstrike \
"cd '${HEXSTRIKE_DIR}' && source '${VENV_DIR}/bin/activate' && python3 hexstrike_server.py --debug"
info "Waiting for the server to come up..."
local i healthy=0
for i in $(seq 1 20); do
curl -s "http://localhost:${SERVER_PORT}/health" >/dev/null 2>&1 && { healthy=1; break; }
sleep 1
done
if [[ "$healthy" -eq 1 ]]; then
success "Server responding on port ${SERVER_PORT}."
echo; info "Tool availability by HexStrike category:"
curl -s "http://localhost:${SERVER_PORT}/health" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c,s in sorted(d.get('category_stats',{}).items()):
print(f' {c:15} {s[\"available\"]}/{s[\"total\"]}')
print()
print(f' TOTAL {d.get(\"total_tools_available\",\"?\")}/{d.get(\"total_tools_count\",\"?\")}')
" 2>/dev/null || warn "Could not parse health output."
echo; info "Server left running in tmux 'hexstrike' (attach: tmux attach -t hexstrike)."
else
err "Server did not respond within 20s. Check: tmux attach -t hexstrike"
fi
}
# ----------------------------------------------------------------------------
# 9. Run guide
# ----------------------------------------------------------------------------
print_run_guide() {
cat <<EOF
${BOLD}${GREEN}Installation complete.${RESET}
${BOLD}How to run HexStrike + OpenCode from here on:${RESET}
${CYAN}1. Start the HexStrike API server (tmux keeps it alive after disconnect):${RESET}
tmux new -s hexstrike
cd ${HEXSTRIKE_DIR} && source ${VENV_DIR}/bin/activate
python3 hexstrike_server.py --debug
# detach: Ctrl+b then d
${CYAN}2. Confirm it's healthy:${RESET}
curl -s http://localhost:${SERVER_PORT}/health | jq .status
${CYAN}3. Launch OpenCode (it spawns the MCP bridge itself -- never run${RESET}
${CYAN}hexstrike_mcp.py by hand):${RESET}
opencode
${CYAN}4. Verify the MCP link:${RESET}
opencode mcp list # expect: hexstrike connected
${CYAN}5. Test inside OpenCode:${RESET}
use hexstrike to scan scanme.nmap.org
${BOLD}Add more tools later:${RESET} re-run this script and pick additional
use-cases; it only installs what is still missing.
${BOLD}Re-check installed tools any time:${RESET}
curl -s http://localhost:${SERVER_PORT}/health \\
| python3 -c "import json,sys;d=json.load(sys.stdin);print(d['total_tools_available'],'/',d['total_tools_count'])"
${BOLD}Config file (edit to add model providers):${RESET}
${OC_CONFIG}
${YELLOW}Security reminder:${RESET} keep port ${SERVER_PORT} bound to localhost. On a
public VPS, firewall it off: sudo ufw deny ${SERVER_PORT}
EOF
}
# ----------------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------------
main() {
banner
require_sudo
ask_system
install_base
scan_toolset # check for existing pentest toolset FIRST
select_usecases # ask what it will be used for
choose_and_install_tools # install ONLY what is missing for those use-cases
install_node_opencode
install_hexstrike
patch_timeout
write_opencode_config
validate
print_run_guide
}
main "$@"