refactor: keep Bash bootstrap diff focused on host neutrality
This commit is contained in:
@@ -133,7 +133,8 @@ make_temp_file() {
|
||||
local suffix="${1:-download}"
|
||||
local tmp_dir
|
||||
tmp_dir="$(mktemp -d /tmp/reverse-bootstrap-XXXXXX)"
|
||||
printf '%s/%s\n' "$tmp_dir" "$suffix"
|
||||
printf '%s/%s
|
||||
' "$tmp_dir" "$suffix"
|
||||
}
|
||||
|
||||
sudo_cmd() {
|
||||
@@ -302,6 +303,8 @@ ensure_pnpm() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Args: repo regex [release_tag]
|
||||
# Prints: url\tdigest_or_empty
|
||||
latest_github_asset_meta() {
|
||||
local repo="$1"
|
||||
local regex="$2"
|
||||
@@ -332,6 +335,8 @@ latest_github_asset_url() {
|
||||
latest_github_asset_meta "$repo" "$regex" "$tag" | cut -f1
|
||||
}
|
||||
|
||||
# verify_sha256 file expected_or_empty [github_digest]
|
||||
# expected may be "hex" or "sha256:hex"; github_digest same. Prefer expected.
|
||||
verify_sha256() {
|
||||
local file="$1"
|
||||
local expected="$2"
|
||||
@@ -369,9 +374,18 @@ extract_archive() {
|
||||
safe_remove_install_dir "$dest" "$dest.tmp"
|
||||
mkdir -p "$dest"
|
||||
case "$archive" in
|
||||
*.zip) unzip -q "$archive" -d "$dest.tmp" ;;
|
||||
*.tar.gz|*.tgz) mkdir -p "$dest.tmp"; tar -xzf "$archive" -C "$dest.tmp" ;;
|
||||
*) mkdir -p "$dest"; cp "$archive" "$dest/"; return 0 ;;
|
||||
*.zip)
|
||||
unzip -q "$archive" -d "$dest.tmp"
|
||||
;;
|
||||
*.tar.gz|*.tgz)
|
||||
mkdir -p "$dest.tmp"
|
||||
tar -xzf "$archive" -C "$dest.tmp"
|
||||
;;
|
||||
*)
|
||||
mkdir -p "$dest"
|
||||
cp "$archive" "$dest/"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
local top_count
|
||||
top_count=$(find "$dest.tmp" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')
|
||||
@@ -383,6 +397,7 @@ extract_archive() {
|
||||
case "$dest.tmp" in /tmp/reverse-bootstrap-*|"$TOOLS_ROOT"/*.tmp) rm -rf "$dest.tmp" ;; esac
|
||||
}
|
||||
|
||||
# install_github_release repo regex dest [release_tag] [expected_sha256]
|
||||
install_github_release() {
|
||||
local repo="$1"
|
||||
local regex="$2"
|
||||
@@ -548,7 +563,7 @@ out.append(f'[mcp_servers.{name}]')
|
||||
for key in ('type', 'url', 'command', 'args', 'bearer_token_env_var'):
|
||||
if key in payload:
|
||||
out.append(f'{key} = {literal(payload[key])}')
|
||||
for key in sorted(k for k in payload if k not in {'type','url','command','args','bearer_token_env_var','env','headers'}):
|
||||
for key in sorted(k for k in payload if k not in {'type', 'url', 'command', 'args', 'bearer_token_env_var', 'env', 'headers'}):
|
||||
out.append(f'{key} = {literal(payload[key])}')
|
||||
env = payload.get('env')
|
||||
if isinstance(env, dict) and env:
|
||||
@@ -569,8 +584,12 @@ write_mcp_server() {
|
||||
LAST_CAPABILITY_REGISTRATION_REQUIRED=true
|
||||
log_warn "MCP registration skipped for '$name' (client-neutral default). Re-run with --mcp-host=claude, codex, or both."
|
||||
;;
|
||||
claude) write_claude_mcp_server "$name" "$json_payload" ;;
|
||||
codex) write_codex_mcp_server "$name" "$json_payload" ;;
|
||||
claude)
|
||||
write_claude_mcp_server "$name" "$json_payload"
|
||||
;;
|
||||
codex)
|
||||
write_codex_mcp_server "$name" "$json_payload"
|
||||
;;
|
||||
both)
|
||||
write_claude_mcp_server "$name" "$json_payload"
|
||||
write_codex_mcp_server "$name" "$json_payload"
|
||||
@@ -583,7 +602,9 @@ test_tcp_port() {
|
||||
python3 - "$port" <<'PY' >/dev/null 2>&1
|
||||
import socket, sys
|
||||
port=int(sys.argv[1])
|
||||
s=socket.socket(); s.settimeout(1); s.connect(('127.0.0.1', port))
|
||||
s=socket.socket()
|
||||
s.settimeout(1)
|
||||
s.connect(('127.0.0.1', port))
|
||||
PY
|
||||
}
|
||||
|
||||
@@ -592,9 +613,15 @@ test_mcp_http() {
|
||||
local timeout_seconds="${2:-3}"
|
||||
python3 - "$port" "$timeout_seconds" <<'PY' >/dev/null 2>&1
|
||||
import sys, json, urllib.request
|
||||
port = int(sys.argv[1]); timeout = int(sys.argv[2])
|
||||
port = int(sys.argv[1])
|
||||
timeout = int(sys.argv[2])
|
||||
body = json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}).encode()
|
||||
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", data=body, headers={"Content-Type": "application/json"}, method="POST")
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/mcp",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
exit(0 if resp.status == 200 else 1)
|
||||
@@ -621,7 +648,10 @@ manual_required() {
|
||||
log_warn "MANUAL_INSTALL_REQUIRED: $name — $hint"
|
||||
}
|
||||
|
||||
is_ready_cmd() { local cmd="$1"; has_cmd "$cmd"; }
|
||||
is_ready_cmd() {
|
||||
local cmd="$1"
|
||||
has_cmd "$cmd"
|
||||
}
|
||||
|
||||
ensure_jeb_pro() {
|
||||
if has_cmd jeb_wincon || has_cmd jeb; then
|
||||
@@ -656,7 +686,8 @@ ensure_apktool() {
|
||||
linux)
|
||||
if install_apt apktool; then return 0; fi
|
||||
ensure_dir "$TOOLS_ROOT/apktool"
|
||||
local meta url digest jar wrapper repo tag sha re
|
||||
local meta url digest jar wrapper
|
||||
local repo tag sha re
|
||||
repo=$(manifest_field apktool repo) || return 1
|
||||
tag=$(manifest_field apktool releaseTag) || return 1
|
||||
sha=$(manifest_field apktool assetSha256) || return 1
|
||||
@@ -722,7 +753,9 @@ ensure_anything_analyzer() {
|
||||
local repo commit
|
||||
repo=$(manifest_field anything-analyzer repoUrl) || return 1
|
||||
commit=$(manifest_field anything-analyzer pinnedCommit) || return 1
|
||||
if ! has_cmd git; then case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac; fi
|
||||
if ! has_cmd git; then
|
||||
case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac
|
||||
fi
|
||||
install_git_commit "$repo" "$commit" "$dir" || return 1
|
||||
ensure_node_runtime || return 1
|
||||
ensure_pnpm || return 1
|
||||
@@ -730,9 +763,20 @@ ensure_anything_analyzer() {
|
||||
if $START_SERVICES; then
|
||||
(cd "$dir" && pnpm install --frozen-lockfile) || return 1
|
||||
install_git_commit "$repo" "$commit" "$dir" || return 1
|
||||
(cd "$dir" || exit 1; if has_cmd nohup; then nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 & else pnpm dev >/tmp/anything-analyzer.log 2>&1 & fi)
|
||||
(
|
||||
cd "$dir" || exit 1
|
||||
if has_cmd nohup; then
|
||||
nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &
|
||||
else
|
||||
pnpm dev >/tmp/anything-analyzer.log 2>&1 &
|
||||
fi
|
||||
)
|
||||
if wait_for_port 23816 120; then
|
||||
if test_mcp_http 23816; then log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)"; else log_warn "anything-analyzer port 23816 open but MCP HTTP handshake failed; check /tmp/anything-analyzer.log"; fi
|
||||
if test_mcp_http 23816; then
|
||||
log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)"
|
||||
else
|
||||
log_warn "anything-analyzer port 23816 open but MCP HTTP handshake failed; check /tmp/anything-analyzer.log"
|
||||
fi
|
||||
else
|
||||
log_warn "anything-analyzer did not open port 23816; see /tmp/anything-analyzer.log"
|
||||
fi
|
||||
@@ -744,11 +788,19 @@ ensure_idapro() {
|
||||
write_mcp_server "idapro" '{"url":"http://127.0.0.1:13337/mcp"}'
|
||||
if $START_SERVICES; then
|
||||
case "$PLATFORM" in
|
||||
linux) log_warn "Linux package does not include an IDA GUI launcher. Start IDA manually and ensure MCP listens on 127.0.0.1:13337." ;;
|
||||
macos) log_warn "Start IDA Pro manually on macOS and confirm MCP port in the IDA Output window." ;;
|
||||
linux)
|
||||
log_warn "Linux package does not include an IDA GUI launcher. Start IDA manually and ensure MCP listens on 127.0.0.1:13337."
|
||||
;;
|
||||
macos)
|
||||
log_warn "Start IDA Pro manually on macOS and confirm MCP port in the IDA Output window."
|
||||
;;
|
||||
esac
|
||||
wait_for_port 13337 45 || log_warn "idapro MCP service is not online on port 13337 yet."
|
||||
if test_mcp_http 13337 3; then log_ok "idapro MCP server ready (HTTP verified)"; else log_warn "idapro port 13337 open but MCP HTTP handshake failed; check IDA Output window"; fi
|
||||
if test_mcp_http 13337 3; then
|
||||
log_ok "idapro MCP server ready (HTTP verified)"
|
||||
else
|
||||
log_warn "idapro port 13337 open but MCP HTTP handshake failed; check IDA Output window"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -756,7 +808,10 @@ ensure_r2() {
|
||||
if has_cmd r2; then log_ok "r2 ready: $(cmd_path r2)"; return 0; fi
|
||||
case "$PLATFORM" in
|
||||
macos) install_brew radare2 ;;
|
||||
linux) if install_apt radare2; then return 0; fi; manual_required r2 "Install radare2 from GitHub/source: https://github.com/radareorg/radare2" ;;
|
||||
linux)
|
||||
if install_apt radare2; then return 0; fi
|
||||
manual_required r2 "Install radare2 from GitHub/source: https://github.com/radareorg/radare2"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -785,8 +840,17 @@ ensure_ghidra_mcp() {
|
||||
repo=$(manifest_field ghidra-mcp repo) || return 1
|
||||
regex=$(manifest_field ghidra-mcp assetRegex) || return 1
|
||||
case "$PLATFORM" in
|
||||
macos) if ! has_cmd ghidraRun && [[ ! -d /Applications/Ghidra.app ]]; then install_brew ghidra || brew install --cask ghidra || true; fi ;;
|
||||
linux) if ! has_cmd ghidraRun; then install_github_release "$repo" "$regex" "$TOOLS_ROOT/ghidra" || manual_required ghidra-mcp "Install Ghidra from GitHub release or Flatpak, then configure ghidra-mcp if used."; fi ;;
|
||||
macos)
|
||||
if ! has_cmd ghidraRun && [[ ! -d /Applications/Ghidra.app ]]; then
|
||||
install_brew ghidra || brew install --cask ghidra || true
|
||||
fi
|
||||
;;
|
||||
linux)
|
||||
if ! has_cmd ghidraRun; then
|
||||
install_github_release "$repo" "$regex" "$TOOLS_ROOT/ghidra" || \
|
||||
manual_required ghidra-mcp "Install Ghidra from GitHub release or Flatpak, then configure ghidra-mcp if used."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
log_warn "ghidra-mcp requires local Ghidra MCP plugin/server setup. See docs/platforms/$( [[ "$PLATFORM" == macos ]] && echo macos || echo linux ).md"
|
||||
}
|
||||
@@ -807,7 +871,12 @@ ensure_proxycat() {
|
||||
local repo commit
|
||||
repo=$(manifest_field proxycat repo) || return 1
|
||||
commit=$(manifest_field proxycat pinnedCommit) || return 1
|
||||
pipx install "git+${repo}@${commit}" || { manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'."; LAST_CAPABILITY_MANUAL=true; MANUAL_REQUIRED=true; return 0; }
|
||||
pipx install "git+${repo}@${commit}" || {
|
||||
manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'."
|
||||
LAST_CAPABILITY_MANUAL=true
|
||||
MANUAL_REQUIRED=true
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
ensure_burpsuite_mcp() {
|
||||
@@ -840,16 +909,28 @@ PY
|
||||
ensure_pentestswarm() {
|
||||
local pentestswarm_path
|
||||
pentestswarm_path="$(cmd_path pentestswarm)"
|
||||
if [[ -n "$pentestswarm_path" ]]; then register_pentestswarm_mcp "$pentestswarm_path"; log_ok "pentestswarm ready"; return 0; fi
|
||||
if ! has_cmd go; then case "$PLATFORM" in macos) install_brew go ;; linux) install_apt golang-go ;; esac; fi
|
||||
if [[ -n "$pentestswarm_path" ]]; then
|
||||
register_pentestswarm_mcp "$pentestswarm_path"
|
||||
log_ok "pentestswarm ready"
|
||||
return 0
|
||||
fi
|
||||
if ! has_cmd go; then
|
||||
case "$PLATFORM" in macos) install_brew go ;; linux) install_apt golang-go ;; esac
|
||||
fi
|
||||
local go_package docker_image
|
||||
go_package=$(manifest_field pentestswarm goPackage) || return 1
|
||||
docker_image=$(manifest_field pentestswarm dockerImage) || return 1
|
||||
if go install "$go_package"; then
|
||||
local go_bin
|
||||
go_bin="$(go env GOBIN 2>/dev/null || true)"
|
||||
if [[ -z "$go_bin" ]]; then go_bin="$(go env GOPATH 2>/dev/null || true)/bin"; fi
|
||||
if [[ -x "$go_bin/pentestswarm" ]]; then register_pentestswarm_mcp "$go_bin/pentestswarm"; log_ok "pentestswarm installed and registered"; return 0; fi
|
||||
if [[ -z "$go_bin" ]]; then
|
||||
go_bin="$(go env GOPATH 2>/dev/null || true)/bin"
|
||||
fi
|
||||
if [[ -x "$go_bin/pentestswarm" ]]; then
|
||||
register_pentestswarm_mcp "$go_bin/pentestswarm"
|
||||
log_ok "pentestswarm installed and registered"
|
||||
return 0
|
||||
fi
|
||||
log_warn "pentestswarm installed but no executable was found in GOBIN/GOPATH; trying Docker fallback"
|
||||
fi
|
||||
if has_cmd docker; then
|
||||
@@ -866,12 +947,18 @@ PY
|
||||
|
||||
ensure_binwalk() {
|
||||
if has_cmd binwalk; then log_ok "binwalk ready: $(cmd_path binwalk)"; return 0; fi
|
||||
case "$PLATFORM" in macos) install_brew binwalk ;; linux) install_apt binwalk || manual_required binwalk "git clone https://github.com/ReFirmLabs/binwalk.git and install manually" ;; esac
|
||||
case "$PLATFORM" in
|
||||
macos) install_brew binwalk ;;
|
||||
linux) install_apt binwalk || manual_required binwalk "git clone https://github.com/ReFirmLabs/binwalk.git and install manually" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_yara() {
|
||||
if has_cmd yara; then log_ok "yara ready: $(cmd_path yara)"; return 0; fi
|
||||
case "$PLATFORM" in macos) install_brew yara ;; linux) install_apt yara || manual_required yara "Install from source: https://github.com/VirusTotal/yara" ;; esac
|
||||
case "$PLATFORM" in
|
||||
macos) install_brew yara ;;
|
||||
linux) install_apt yara || manual_required yara "Install from source: https://github.com/VirusTotal/yara" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_pwntools() {
|
||||
@@ -886,7 +973,11 @@ status_json_line() {
|
||||
local name="$1"
|
||||
local status="$2"
|
||||
local extra="${3:-}"
|
||||
if [[ -n "$extra" ]]; then printf '{"name":"%s","status":"%s","note":"%s"}\n' "$name" "$status" "$extra"; else printf '{"name":"%s","status":"%s"}\n' "$name" "$status"; fi
|
||||
if [[ -n "$extra" ]]; then
|
||||
printf '{"name":"%s","status":"%s","note":"%s"}\n' "$name" "$status" "$extra"
|
||||
else
|
||||
printf '{"name":"%s","status":"%s"}\n' "$name" "$status"
|
||||
fi
|
||||
}
|
||||
|
||||
cap_depends() {
|
||||
@@ -904,7 +995,10 @@ expand_capabilities() {
|
||||
local cap dep
|
||||
for cap in "$@"; do
|
||||
for dep in $(cap_depends "$cap"); do
|
||||
if [[ "$seen" != *" $dep "* ]]; then out+=("$dep"); seen+="$dep "; fi
|
||||
if [[ "$seen" != *" $dep "* ]]; then
|
||||
out+=("$dep")
|
||||
seen+="$dep "
|
||||
fi
|
||||
done
|
||||
done
|
||||
printf '%s\n' "${out[@]}"
|
||||
@@ -941,6 +1035,8 @@ ensure_capability() {
|
||||
RESULTS_FILE="$(mktemp)"
|
||||
trap 'rm -f "$RESULTS_FILE"' EXIT
|
||||
|
||||
# macOS ships Bash 3.2, which has no mapfile/readarray. Keep this path portable
|
||||
# instead of requiring users to install a newer Bash just to run the bootstrapper.
|
||||
EXPANDED=()
|
||||
while IFS= read -r capability; do
|
||||
[[ -n "$capability" ]] || continue
|
||||
@@ -997,4 +1093,4 @@ else
|
||||
cat "$RESULTS_FILE"
|
||||
fi
|
||||
|
||||
exit "$FINAL_EXIT_CODE"
|
||||
exit "$FINAL_EXIT_CODE"
|
||||
Reference in New Issue
Block a user