Files
CyberStrikeAI/install-tools.sh
T
2026-06-08 15:55:03 +08:00

1065 lines
35 KiB
Bash

#!/usr/bin/env bash
# ============================================================================
# CyberStrikeAI 工具一键安装脚本
# ----------------------------------------------------------------------------
# 解决问题: 工具执行失败: exec: "xxx": executable file not found in $PATH
# 跑 skill 时不同工具缺失导致任务无法继续 (issue #139)
#
# 工作方式:
# 1. 自动扫描 tools/*.yaml 中声明的所有工具 (name + command)
# 2. 在 Kali / Debian / Ubuntu / Parrot 上优先使用 apt 安装
# 3. apt 找不到的, 依次降级到 pip / gem / go install / GitHub release
# 4. 已安装的自动跳过, 不会重复执行
#
# 用法:
# ./install-tools.sh # 安装所有工具
# ./install-tools.sh --check # 只检查, 不安装
# ./install-tools.sh --list # 列出所有工具及状态
# ./install-tools.sh --only nmap,gau # 只安装指定工具
# ./install-tools.sh --skip msfvenom # 跳过指定工具
# ./install-tools.sh --dry-run # 模拟运行
# ./install-tools.sh --method apt # 强制使用某种安装方式
# ./install-tools.sh --no-sudo # 不使用 sudo (用于已是 root 的环境)
# ./install-tools.sh --verbose # 显示安装命令输出 (排错用)
# ./install-tools.sh --install-bash # macOS: 用 Homebrew 安装 bash 4+ 并继续
#
# 环境变量 (可选):
# PIP_INDEX_URL 自定义 pip 镜像源 (未设置时: 中文环境用清华源, 其他用 pypi.org)
# GOPROXY 自定义 Go 模块代理 (未设置时: 中文环境用 goproxy.cn)
# INSTALL_PREFIX 自定义二进制安装目录 (默认 /usr/local/bin)
# VERBOSE 设为 1 等同 --verbose
#
# 平台说明:
# 主力支持 Kali / Debian / Ubuntu (apt). macOS 仅 pip/go/GitHub 部分可用.
# ============================================================================
# bash 4+ 必需 (关联数组). macOS 自带 3.2 — 自动寻找 Homebrew bash 或 --install-bash
__csai_find_bash4() {
local c prefix
for c in \
"${BASH4:-}" \
/opt/homebrew/bin/bash \
/usr/local/opt/bash/bin/bash \
/usr/local/bin/bash; do
[[ -z "$c" || ! -x "$c" ]] && continue
if "$c" -c '((BASH_VERSINFO[0] >= 4))' 2>/dev/null; then
echo "$c"
return 0
fi
done
if command -v brew >/dev/null 2>&1; then
prefix="$(brew --prefix bash 2>/dev/null)" || true
c="${prefix}/bin/bash"
if [[ -n "$prefix" && -x "$c" ]] && "$c" -c '((BASH_VERSINFO[0] >= 4))' 2>/dev/null; then
echo "$c"
return 0
fi
fi
return 1
}
__csai_reexec_bash4() {
local b4
b4="$(__csai_find_bash4)"
[[ -n "$b4" ]] || return 1
exec "$b4" "$0" "$@"
}
if ((BASH_VERSINFO[0] < 4)); then
want_install_bash=0
for arg in "$@"; do
[[ "$arg" == "--install-bash" ]] && want_install_bash=1
done
if [[ $want_install_bash -eq 1 ]]; then
if ! command -v brew >/dev/null 2>&1; then
echo "[-] --install-bash 需要 Homebrew: https://brew.sh" >&2
exit 1
fi
echo "[*] 通过 Homebrew 安装 bash 4+ ..."
brew install bash
__csai_reexec_bash4 "$@" || true
fi
if ! __csai_reexec_bash4 "$@"; then
echo "[!] 此脚本需要 bash 4.0+, 当前是: $BASH_VERSION" >&2
if command -v brew >/dev/null 2>&1; then
echo " macOS 一键修复: ./install-tools.sh --install-bash --list" >&2
echo " 或手动: brew install bash" >&2
else
echo " macOS: 安装 Homebrew 后运行 ./install-tools.sh --install-bash" >&2
fi
echo " Kali/Debian/Ubuntu: 默认 bash 5.x, 直接运行即可" >&2
exit 1
fi
fi
set -uo pipefail
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
TOOLS_DIR="$ROOT_DIR/tools"
# ----------------------------------------------------------------------------
# 颜色与日志
# ----------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
GRAY='\033[0;90m'
NC='\033[0m'
info() { echo -e "${BLUE}[i]${NC} $*"; }
success() { echo -e "${GREEN}[+]${NC} $*"; }
warning() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[-]${NC} $*"; }
note() { echo -e "${CYAN}[*]${NC} $*"; }
dim() { echo -e "${GRAY} $*${NC}"; }
VERBOSE="${VERBOSE:-0}"
log_run() {
if [[ "$VERBOSE" -eq 1 ]]; then
"$@"
else
"$@" >/dev/null 2>&1
fi
}
# ----------------------------------------------------------------------------
# 默认源 (按语言环境选择镜像, 可被环境变量覆盖)
# ----------------------------------------------------------------------------
is_zh_locale() {
[[ "${LANG:-}${LC_ALL:-}" == *zh_* ]]
}
if [[ -z "${PIP_INDEX_URL:-}" ]]; then
if is_zh_locale; then
PIP_INDEX_URL="https://pypi.tuna.tsinghua.edu.cn/simple"
else
PIP_INDEX_URL="https://pypi.org/simple"
fi
fi
if [[ -z "${GOPROXY:-}" ]]; then
if is_zh_locale; then
GOPROXY="https://goproxy.cn,direct"
else
GOPROXY="https://proxy.golang.org,direct"
fi
fi
INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local/bin}"
# 状态统计
declare -A STATUS=() # name -> ok|skip|fail|manual
declare -A METHOD=() # name -> method used
declare -A SKIP_REASON=()
TOTAL=0
OK_COUNT=0
SKIP_COUNT=0
FAIL_COUNT=0
MANUAL_COUNT=0
# ----------------------------------------------------------------------------
# 参数解析
# ----------------------------------------------------------------------------
MODE="install" # install | check | list | dry-run
ONLY_TOOLS=""
SKIP_TOOLS=""
FORCE_METHOD=""
USE_SUDO="auto"
usage() {
sed -n '2,36p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
--check) MODE="check"; shift ;;
--list) MODE="list"; shift ;;
--dry-run) MODE="dry-run"; shift ;;
--only) ONLY_TOOLS="${2:-}"; shift 2 ;;
--skip) SKIP_TOOLS="${2:-}"; shift 2 ;;
--method) FORCE_METHOD="${2:-}"; shift 2 ;;
--no-sudo) USE_SUDO="no"; shift ;;
--sudo) USE_SUDO="yes"; shift ;;
--verbose|-v) VERBOSE=1; shift ;;
--install-bash) shift ;; # 已在启动阶段处理
-h|--help) usage ;;
*) error "未知参数: $1"; usage ;;
esac
done
# ----------------------------------------------------------------------------
# 系统检测
# ----------------------------------------------------------------------------
DISTRO_ID=""
DISTRO_LIKE=""
DISTRO_FAMILY="" # debian | macos | other
PKG_MGR="" # apt | brew | unknown
SUDO_CMD=""
detect_distro() {
if [[ "$OSTYPE" == "darwin"* ]]; then
DISTRO_FAMILY="macos"
PKG_MGR="brew"
DISTRO_ID="macos"
return
fi
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
DISTRO_ID="${ID:-unknown}"
DISTRO_LIKE="${ID_LIKE:-}"
elif [[ -f /etc/lsb-release ]]; then
# shellcheck disable=SC1091
. /etc/lsb-release
DISTRO_ID="${DISTRIB_ID:-unknown}"
else
DISTRO_ID="unknown"
fi
case "$DISTRO_ID" in
kali) DISTRO_FAMILY="debian" ;;
parrot*|debian|ubuntu|linuxmint|elementary|pop|deepin)
DISTRO_FAMILY="debian" ;;
centos|rhel|fedora|rocky|alma|amazon)
DISTRO_FAMILY="rhel" ;;
arch|manjaro|endeavouros)
DISTRO_FAMILY="arch" ;;
*)
if [[ "$DISTRO_LIKE" == *debian* ]]; then
DISTRO_FAMILY="debian"
elif [[ "$DISTRO_LIKE" == *rhel* ]]; then
DISTRO_FAMILY="rhel"
else
DISTRO_FAMILY="other"
fi
;;
esac
case "$DISTRO_FAMILY" in
debian) PKG_MGR="apt" ;;
rhel) PKG_MGR="dnf" ;;
arch) PKG_MGR="pacman" ;;
esac
}
setup_sudo() {
if [[ $EUID -eq 0 ]]; then
SUDO_CMD=""
return
fi
case "$USE_SUDO" in
yes) SUDO_CMD="sudo" ;;
no)
if [[ $EUID -ne 0 ]]; then
error "需要 root 权限才能继续, 但 --no-sudo 已指定"
error "请以 root 身份运行, 或去掉 --no-sudo"
exit 1
fi
SUDO_CMD=""
;;
auto)
if command -v sudo >/dev/null 2>&1; then
SUDO_CMD="sudo"
else
if [[ $EUID -ne 0 ]]; then
error "需要 root 权限, 但系统未安装 sudo"
exit 1
fi
SUDO_CMD=""
fi
;;
esac
}
# ----------------------------------------------------------------------------
# 工具映射表
# ----------------------------------------------------------------------------
# 字段: name|cmd|apt|pip|gem|go|github_repo|binary_url|note
# - name: tools/*.yaml 中的 name
# - cmd: 实际可执行文件名 (用于 command -v 检测)
# - apt: Debian/Kali 包名 (空表示跳过)
# - pip: PyPI 包名 (空表示跳过)
# - gem: Ruby gem 包名
# - go: go install 路径 (github.com/xxx/cmd@latest)
# - github_repo: GitHub 仓库 owner/repo 用于下载 release (格式 owner/repo)
# - binary_url: 自定义二进制直链
# - note: 备注
#
# 优先级: apt > brew(自动) > pip(映射+PyPI自动探测) > gem > go > github release
# 非 apt 平台: 按 apt/name/cmd 依次尝试 Homebrew; 再按 pip列/name/cmd/apt 探测 PyPI
# ----------------------------------------------------------------------------
get_tool_map() {
cat <<'EOF'
amass|amass|amass|||||Kali 自带
angr|python3||angr|||||二进制较重, 首次编译耗时长
api-schema-analyzer|spectral|||||||需要 Node.js (npm install)
arjun|arjun|arjun|arjun||||Kali 自带
arp-scan|arp-scan|arp-scan|||||Kali 自带
binwalk|binwalk|binwalk|||||Kali 自带
bloodhound|bloodhound-python||bloodhound|||或 apt:bloodhound
checkov|checkov||checkov||||需 pip
checksec|checksec|checksec|||||Kali 自带
clair|clair|||||quay/clair|需 Docker 或单独下载二进制
cloudmapper|cloudmapper||cloudmapper||||
dalfox|dalfox|dalfox|||github.com/hahwul/dalfox/v2||
dirsearch|dirsearch|dirsearch|dirsearch||||
dnsenum|dnsenum|dnsenum|||||Kali 自带
dnslog|python3||||||||内部 Python 包装, 无需单独安装
dotdotpwn|dotdotpwn|dotdotpwn|||||Kali 自带
enum4linux-ng|enum4linux-ng|enum4linux-ng|||||Kali 自带
exec|sh||||||||内置 sh, 跳过
execute-python-script|/bin/bash||||||||内置 bash, 跳过
exiftool|exiftool|libimage-exiftool-perl||||apt 包名: libimage-exiftool-perl
falco|falco|falco|||||Kali 自带
feroxbuster|feroxbuster|feroxbuster|||github.com/epi052/feroxbuster|可能需 GitHub release
ffuf|ffuf|ffuf|||||Kali 自带
fierce|fierce|fierce|fierce||||
fofa_search|python3||requests||||fofa-py (需 API key)
foremost|foremost|foremost|||||Kali 自带
fscan|fscan|||||shelldigger/fscan|国内开源, GitHub release
gau|gau|gau|||github.com/lc/gau||
gdb|gdb|gdb|||||Kali 自带
ghidra|analyzeHeadless|ghidra|||||包较大, 可能需手动确认
gobuster|gobuster|gobuster|||||Kali 自带
graphql-scanner|graphqlmap||graphqlmap|||||或 git clone
hashcat|hashcat|hashcat|||||Kali 自带
hashpump|hashpump|hashpump|||||Kali 自带
http-framework-test|python3||requests||||内置 Python 包装
hydra|hydra|hydra|||||Kali 自带
impacket|python3|python3-impacket|impacket||||优先 apt
install-python-package|/bin/bash||||||||内置, 跳过
jaeles|jaeles|jaeles|||github.com/jaeles-project/jaeles||
john|john|john|||||Kali 自带
jwt-analyzer|jwt_tool|jwt|jwt-tool|||apt 包 jwt 提供 jwt_tool
katana|katana|katana|||github.com/projectdiscovery/katana/v2||
kube-bench|kube-bench|kube-bench|||||Kali 自带
kube-hunter|kube-hunter||kube-hunter|||pip 安装
libc-database|python3|libc-database||||niklasb/libc-database||git clone 较慢
lightx|lightx|||||zyylhn/lightx|需 GitHub release
linpeas|linpeas.sh||||||从 GitHub 下载脚本到 /usr/local/bin
masscan|masscan|masscan|||||Kali 自带
metasploit|python3|metasploit-framework|||||依赖 msfconsole
msfvenom|msfvenom|metasploit-framework|||||由 metasploit-framework 提供
nbtscan|nbtscan|nbtscan|||||Kali 自带
netexec|netexec|netexec|netexec||||
nikto|nikto|nikto|||||Kali 自带
nmap|nmap|nmap|||||Kali 自带
nuclei|nuclei|nuclei|||github.com/projectdiscovery/nuclei/v3/cmd/nuclei||
objdump|objdump|binutils|||||由 binutils 提供
one-gadget|one_gadget|one-gadget|one_gadget|||apt one-gadget
pacu|pacu||pacu||||
paramspider|paramspider||paramspider||||
prowler|prowler||prowler||||
pwninit|pwninit|pwninit|||github.com/io12/pwninit||
pwntools|python3||pwntools|||较重
quake_search|python3||requests||||quake 需 API key
query_execution_result|internal:query_execution_result||||||||内置, 跳过
radare2|r2|radare2|||||apt 包 radare2 提供 r2
responder|python3|responder||||Kali 自带 responder
ropgadget|ROPgadget||ROPgadget|||pip 安装
ropper|ropper|ropper|ropper|||Kali 自带
rpcclient|python3|samba-common-bin||||samba-common-bin 提供 rpcclient
rustscan|rustscan|rustscan|||github.com/RustScan/RustScan||
scout-suite|scout||scoutsuite||||
shodan_search|python3||shodan|||需 API key
smbmap|smbmap|smbmap|||||Kali 自带
sqlmap|sqlmap|sqlmap|||||Kali 自带
steghide|steghide|steghide|||||Kali 自带
strings|strings|binutils|||||由 binutils 提供
subfinder|subfinder|subfinder|||github.com/projectdiscovery/subfinder/v2/cmd/subfinder||
terrascan|terrascan|terrascan|||||Kali 自带
trivy|trivy|trivy|||||Kali 自带
volatility3|volatility3|volatility3|volatility3|||Kali 自带
wafw00f|wafw00f|wafw00f|||||Kali 自带
waybackurls|waybackurls|waybackurls|||github.com/tomnomnom/waybackurls||
wpscan|wpscan|wpscan|||||Kali 自带
x8|x8|x8|||github.com/Sh1Yo/x8||
xsser|xsser|xsser|||||Kali 自带
xxd|xxd|vim-gtk3|xxd||||Kali 装 vim 即可
zap|zap-cli||zapcli|||pip 安装
zoomeye_search|python3||zoomeye-sdk|||zoomeye 需 API key
zsteg|zsteg|zsteg|zsteg|||apt:ruby-zsteg
EOF
}
# 把映射加载到数组 (容忍列数不齐, 自动纠正常见错位)
declare -A M=()
load_map() {
local line name rest cmd apt pip gem go gh bin_url note
local nfields
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" || "$line" == \#* ]] && continue
name="${line%%|*}"
rest="${line#*|}"
# 补齐 cmd..note 共 8 段 (bash 3.2 不用 local -a)
nfields=$(awk -F'|' '{print NF}' <<< "$rest")
while (( nfields < 8 )); do
rest="${rest}|"
nfields=$((nfields + 1))
done
IFS='|' read -r cmd apt pip gem go gh bin_url note <<< "$rest"
# gem 列误填 GitHub owner/repo → 提升到 gh
if [[ "$gem" == */* && -z "$go" && -z "$gh" ]]; then
gh="$gem"
gem=""
fi
# gh 列误填备注 (无 /) → 移到 note
if [[ -n "$gh" && "$gh" != */* && -z "$bin_url" && -z "$note" ]]; then
note="$gh"
gh=""
fi
# bin_url 列误填备注 → 移到 note
if [[ -n "$bin_url" && -z "$note" && "$bin_url" != http* ]]; then
note="$bin_url"
bin_url=""
fi
M["$name|cmd"]="$cmd"
M["$name|apt"]="$apt"
M["$name|pip"]="$pip"
M["$name|gem"]="$gem"
M["$name|go"]="$go"
M["$name|gh"]="$gh"
M["$name|bin"]="$bin_url"
M["$name|note"]="$note"
done < <(get_tool_map)
}
# 从 tools/*.yaml 抽取 name 与 command
declare -A YAML_CMD=()
declare -A YAML_ENABLED=()
discover_tools() {
local f name cmd enabled
for f in "$TOOLS_DIR"/*.yaml "$TOOLS_DIR"/*.yml; do
[[ -f "$f" ]] || continue
name=$(grep -E "^name:" "$f" | head -1 | sed -E 's/^name:[[:space:]]*"?([^"]+)"?.*/\1/')
cmd=$(grep -E "^command:" "$f" | head -1 | sed -E 's/^command:[[:space:]]*"?([^"]+)"?.*/\1/')
enabled=$(grep -E "^enabled:" "$f" | head -1 | sed -E 's/^enabled:[[:space:]]*"?([^"]+)"?.*/\1/')
if [[ -n "$name" ]]; then
YAML_CMD["$name"]="$cmd"
YAML_ENABLED["$name"]="${enabled:-true}"
fi
done
}
validate_tool_coverage() {
local name
for name in "${!YAML_CMD[@]}"; do
if [[ -z "${M[$name|cmd]:-}" ]]; then
warning "tools/${name}.yaml 未纳入 install-tools 映射表, 将不会自动安装"
fi
done
}
# ----------------------------------------------------------------------------
# 检测工具是否已安装 (按工具名判断, 避免 python3 包装器误报)
# ----------------------------------------------------------------------------
resolve_cmd() {
local name="$1"
echo "${M[$name|cmd]:-${YAML_CMD[$name]:-$name}}"
}
cmd_on_path() {
local cmd="$1"
[[ -n "$cmd" ]] && command -v "$cmd" >/dev/null 2>&1
}
is_builtin_tool() {
case "$1" in
exec|execute-python-script|install-python-package|query_execution_result|dnslog|http-framework-test)
return 0 ;;
esac
return 1
}
is_installed() {
local name="$1"
local cmd="${2:-$(resolve_cmd "$name")}"
case "$name" in
angr) python3 -c "import angr" >/dev/null 2>&1 && return 0 ;;
pwntools) python3 -c "import pwn" >/dev/null 2>&1 && return 0 ;;
impacket) python3 -c "import impacket" >/dev/null 2>&1 && return 0 ;;
metasploit) cmd_on_path msfconsole && return 0 ;;
msfvenom) cmd_on_path msfvenom && return 0 ;;
responder) cmd_on_path responder && return 0 ;;
rpcclient) cmd_on_path rpcclient && return 0 ;;
bloodhound) cmd_on_path bloodhound-python && return 0 ;;
shodan_search) python3 -c "import shodan" >/dev/null 2>&1 && return 0 ;;
zoomeye_search) python3 -c "import zoomeye" >/dev/null 2>&1 && return 0 ;;
fofa_search|quake_search)
python3 -c "import requests" >/dev/null 2>&1 && return 0 ;;
libc-database)
[[ -d /usr/share/libc-database || -d "$HOME/libc-database" ]] && return 0 ;;
api-schema-analyzer) cmd_on_path spectral && return 0 ;;
linpeas)
[[ -x "$INSTALL_PREFIX/linpeas.sh" ]] && return 0
cmd_on_path linpeas.sh && return 0 ;;
paramspider) cmd_on_path paramspider && return 0 ;;
pacu) cmd_on_path pacu && return 0 ;;
prowler) cmd_on_path prowler && return 0 ;;
checkov) cmd_on_path checkov && return 0 ;;
scout-suite) cmd_on_path scout && return 0 ;;
graphql-scanner) cmd_on_path graphqlmap && return 0 ;;
cloudmapper) cmd_on_path cloudmapper && return 0 ;;
dnslog|http-framework-test)
cmd_on_path python3 && return 0 ;;
esac
# go install 产物可能在 ~/go/bin
if [[ -x "$HOME/go/bin/$cmd" ]]; then
return 0
fi
# 通用二进制检测 (排除包装器命令)
case "$cmd" in
python3|/bin/bash|sh|internal:*|'') return 1 ;;
esac
cmd_on_path "$cmd"
}
# ----------------------------------------------------------------------------
# 各种安装方法
# ----------------------------------------------------------------------------
have_sudo() {
[[ -n "$SUDO_CMD" || $EUID -eq 0 ]]
}
install_via_apt() {
local pkg="$1"
[[ -z "$pkg" ]] && return 1
if ! have_sudo; then
warning "无 root 权限, 跳过 apt 安装: $pkg"
return 1
fi
# 检查包是否已存在
if log_run $SUDO_CMD apt-cache show "$pkg"; then
note "apt 安装: $pkg"
log_run $SUDO_CMD DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg"
return $?
fi
return 1
}
# 是否为可尝试安装的包名 (排除包装器/路径/内部命令)
is_installable_name() {
local n="$1"
[[ -n "$n" ]] || return 1
case "$n" in
python3|/bin/bash|sh|internal:*|*/*|*:*|analyzeHeadless|linpeas.sh)
return 1 ;;
esac
return 0
}
pypi_exists() {
local pkg="$1"
[[ -n "$pkg" ]] || return 1
log_run curl -fsSL "https://pypi.org/pypi/${pkg}/json" -o /dev/null
}
# 依次尝试多个候选名, 去重
try_install_brew() {
local seen="" c
for c in "$@"; do
is_installable_name "$c" || continue
[[ " $seen " == *" $c "* ]] && continue
seen="$seen $c"
install_via_brew "$c" && return 0
done
return 1
}
try_install_pip() {
local explicit="$1" seen="" c
shift
# 映射表 pip 列: 直接安装, 不探测 PyPI
if [[ -n "$explicit" ]] && is_installable_name "$explicit"; then
install_via_pip "$explicit" && return 0
fi
# 自动探测: name / cmd / apt 列, 须在 PyPI 存在
for c in "$@"; do
is_installable_name "$c" || continue
[[ " $seen " == *" $c "* ]] && continue
seen="$seen $c"
pypi_exists "$c" || continue
install_via_pip "$c" && return 0
done
return 1
}
install_via_brew() {
local pkg="$1"
[[ -z "$pkg" ]] && return 1
if ! command -v brew >/dev/null 2>&1; then
return 1
fi
if brew list "$pkg" &>/dev/null; then
note "brew 已安装: $pkg"
return 0
fi
if ! brew info "$pkg" &>/dev/null; then
return 1
fi
note "brew 安装: $pkg"
log_run brew install "$pkg"
return $?
}
install_via_pip() {
local pkg="$1"
[[ -z "$pkg" ]] && return 1
note "pip 安装: $pkg"
PIP_DISABLE_PIP_VERSION_CHECK=1 log_run pip3 install --index-url "$PIP_INDEX_URL" \
--break-system-packages --quiet "$pkg"
local rc=$?
if [[ $rc -ne 0 ]]; then
log_run pip3 install --index-url "$PIP_INDEX_URL" --quiet "$pkg"
rc=$?
fi
return $rc
}
install_via_gem() {
local pkg="$1"
[[ -z "$pkg" ]] && return 1
if ! command -v gem >/dev/null 2>&1; then
warning "gem 不可用, 跳过: $pkg"
return 1
fi
if ! have_sudo; then
warning "无 root 权限, 跳过 gem 安装: $pkg"
return 1
fi
note "gem 安装: $pkg"
log_run $SUDO_CMD gem install "$pkg" --no-document
return $?
}
install_via_go() {
local pkg="$1"
[[ -z "$pkg" ]] && return 1
if ! command -v go >/dev/null 2>&1; then
warning "go 未安装, 跳过: $pkg"
return 1
fi
note "go install: $pkg"
GOPROXY="$GOPROXY" log_run go install "$pkg@latest"
local rc=$?
# 确保 ~/go/bin 在 PATH 提示里
if [[ $rc -eq 0 ]] && [[ -d "$HOME/go/bin" ]]; then
# 不强行修改用户 shell, 仅提示
:
fi
return $rc
}
install_via_github_release() {
local repo="$1"
local cmd="$2"
[[ -z "$repo" ]] && return 1
if ! command -v curl >/dev/null 2>&1; then
warning "curl 未安装, 无法下载 GitHub release"
return 1
fi
if ! have_sudo; then
warning "无 root 权限, 跳过 GitHub release 安装: $repo"
return 1
fi
note "GitHub release 下载: $repo"
local platform="${DISTRO_FAMILY:-linux}"
local api="https://api.github.com/repos/${repo}/releases/latest"
local tmp
tmp=$(mktemp -d)
if ! log_run curl -fsSL "$api" -o "$tmp/release.json"; then
rm -rf "$tmp"
return 1
fi
local url
url=$(python3 - "$tmp/release.json" "$platform" <<'PY' 2>/dev/null
import json, re, sys
data = json.load(open(sys.argv[1]))
platform = sys.argv[2]
assets = data.get("assets", [])
if platform == "macos":
patterns = [
r'darwin.*arm64', r'macos.*arm64', r'apple.*arm64',
r'darwin.*amd64', r'macos.*amd64', r'darwin.*x86_64',
r'darwin', r'macos',
]
else:
patterns = [
r'linux.*amd64', r'linux.*x86_64', r'linux.*64bit',
r'.*linux.*x64', r'_linux$', r'linux',
]
for p in patterns:
for a in assets:
n = a.get("name", "").lower()
if re.search(p, n) and n.endswith(('.tar.gz', '.tgz', '.zip')):
print(a["browser_download_url"])
sys.exit(0)
for a in assets:
n = a.get("name", "").lower()
if n.endswith(('.tar.gz', '.tgz', '.zip')):
print(a["browser_download_url"])
sys.exit(0)
PY
)
if [[ -z "$url" ]]; then
rm -rf "$tmp"
warning "未找到合适的 release 资产: $repo (platform=$platform)"
return 1
fi
local fname
fname=$(basename "$url")
note "下载: $url"
if ! log_run curl -fsSL "$url" -o "$tmp/$fname"; then
rm -rf "$tmp"
return 1
fi
# 解压
case "$fname" in
*.tar.gz|*.tgz) tar -xzf "$tmp/$fname" -C "$tmp" 2>/dev/null || { rm -rf "$tmp"; return 1; } ;;
*.zip) unzip -q "$tmp/$fname" -d "$tmp" 2>/dev/null || { rm -rf "$tmp"; return 1; } ;;
*) rm -rf "$tmp"; return 1 ;;
esac
# 找可执行文件
local exec_target=""
if [[ -n "$cmd" ]]; then
exec_target=$(find "$tmp" -type f -name "$cmd" -executable 2>/dev/null | head -1)
fi
if [[ -z "$exec_target" ]]; then
# 退一步: 找任意可执行文件
exec_target=$(find "$tmp" -type f -executable 2>/dev/null \
| grep -Ev '\.(md|txt|json|yaml|yml|sum|sha|pem|1)$' \
| head -1)
fi
if [[ -z "$exec_target" ]]; then
rm -rf "$tmp"
warning "解压后未找到可执行文件: $repo"
return 1
fi
log_run $SUDO_CMD install -m 0755 "$exec_target" "$INSTALL_PREFIX/$cmd"
local rc=$?
rm -rf "$tmp"
return $rc
}
install_linpeas_script() {
# linpeas 是单个 .sh 脚本, 直接 curl 下来放到 /usr/local/bin
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
if ! have_sudo; then
warning "无 root 权限, 跳过 linpeas 下载"
return 1
fi
local tmpdir="${TMPDIR:-/tmp}"
note "下载 linpeas.sh 到 $INSTALL_PREFIX/"
if log_run curl -fsSL "https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh" \
-o "$tmpdir/linpeas.sh"; then
log_run $SUDO_CMD install -m 0755 "$tmpdir/linpeas.sh" "$INSTALL_PREFIX/linpeas.sh" \
&& { rm -f "$tmpdir/linpeas.sh"; return 0; }
fi
log_run curl -fsSL "https://raw.githubusercontent.com/peass-ng/PEASS-ng/master/linPEAS/linpeas.sh" \
-o "$tmpdir/linpeas.sh" \
&& log_run $SUDO_CMD install -m 0755 "$tmpdir/linpeas.sh" "$INSTALL_PREFIX/linpeas.sh"
local rc=$?
rm -f "$tmpdir/linpeas.sh"
return $rc
}
install_spectral() {
# spectral 来自 npm
if ! command -v npm >/dev/null 2>&1; then
warning "npm 未安装, 无法装 spectral (请先装 Node.js)"
return 1
fi
note "npm 安装: @stoplight/spectral-cli"
log_run npm install -g @stoplight/spectral-cli
return $?
}
# ----------------------------------------------------------------------------
# 安装单个工具
# ----------------------------------------------------------------------------
install_tool() {
local name="$1"
local cmd
cmd="$(resolve_cmd "$name")"
local apt_pkg="${M[$name|apt]:-}"
local pip_pkg="${M[$name|pip]:-}"
local gem_pkg="${M[$name|gem]:-}"
local go_pkg="${M[$name|go]:-}"
local gh_repo="${M[$name|gh]:-}"
local note="${M[$name|note]:-}"
# 跳过内置/内部工具
if is_builtin_tool "$name"; then
if [[ -z "$apt_pkg" && -z "$pip_pkg" && -z "$gh_repo" && -z "$go_pkg" ]]; then
STATUS["$name"]="skip"
SKIP_REASON["$name"]="内置工具, 运行时由 Python 包装"
SKIP_COUNT=$((SKIP_COUNT+1))
return 0
fi
fi
# 已安装
if is_installed "$name" "$cmd"; then
if [[ "$MODE" == "install" ]]; then
STATUS["$name"]="skip"
SKIP_REASON["$name"]="已存在 ($cmd)"
SKIP_COUNT=$((SKIP_COUNT+1))
dim "$name: 已安装 ($cmd)"
else
STATUS["$name"]="ok"
OK_COUNT=$((OK_COUNT+1))
fi
return 0
fi
if [[ "$MODE" == "check" || "$MODE" == "list" ]]; then
STATUS["$name"]="fail"
FAIL_COUNT=$((FAIL_COUNT+1))
return 0
fi
if [[ "$MODE" == "dry-run" ]]; then
echo " [DRY] $name (cmd=$cmd) -> apt/brew=$apt_pkg pip=$pip_pkg gem=$gem_pkg go=$go_pkg gh=$gh_repo"
STATUS["$name"]="dry"
return 0
fi
# 选择安装方法
local tried=()
local rc=1
if [[ -z "$FORCE_METHOD" || "$FORCE_METHOD" == "apt" ]]; then
if [[ "$PKG_MGR" == "apt" && -n "$apt_pkg" ]]; then
tried+=("apt")
install_via_apt "$apt_pkg" && { rc=0; METHOD["$name"]="apt"; }
fi
fi
if [[ $rc -ne 0 && ( -z "$FORCE_METHOD" || "$FORCE_METHOD" == "brew" ) ]]; then
if [[ "$PKG_MGR" == "brew" ]]; then
tried+=("brew")
try_install_brew "$apt_pkg" "$name" "$cmd" && { rc=0; METHOD["$name"]="brew"; }
fi
fi
if [[ $rc -ne 0 && ( -z "$FORCE_METHOD" || "$FORCE_METHOD" == "pip" ) ]]; then
tried+=("pip")
try_install_pip "$pip_pkg" "$name" "$cmd" "$apt_pkg" \
&& { rc=0; METHOD["$name"]="pip"; }
fi
if [[ $rc -ne 0 && ( -z "$FORCE_METHOD" || "$FORCE_METHOD" == "gem" ) ]]; then
if [[ -n "$gem_pkg" ]]; then
tried+=("gem")
install_via_gem "$gem_pkg" && { rc=0; METHOD["$name"]="gem"; }
fi
fi
if [[ $rc -ne 0 && ( -z "$FORCE_METHOD" || "$FORCE_METHOD" == "go" ) ]]; then
if [[ -n "$go_pkg" ]]; then
tried+=("go")
install_via_go "$go_pkg" && { rc=0; METHOD["$name"]="go"; }
fi
fi
if [[ $rc -ne 0 && ( -z "$FORCE_METHOD" || "$FORCE_METHOD" == "github" ) ]]; then
if [[ -n "$gh_repo" ]]; then
tried+=("github")
install_via_github_release "$gh_repo" "$cmd" && { rc=0; METHOD["$name"]="github"; }
fi
fi
# 特殊情况
if [[ $rc -ne 0 ]]; then
case "$name" in
linpeas)
install_linpeas_script && { rc=0; METHOD["$name"]="github-script"; }
;;
api-schema-analyzer)
install_spectral && { rc=0; METHOD["$name"]="npm"; }
;;
esac
fi
if [[ $rc -eq 0 ]] && is_installed "$name" "$cmd"; then
STATUS["$name"]="ok"
OK_COUNT=$((OK_COUNT+1))
success "$name 安装成功 (${METHOD[$name]:-unknown})"
return 0
fi
STATUS["$name"]="fail"
FAIL_COUNT=$((FAIL_COUNT+1))
if [[ -n "$note" ]]; then
warning "$name 安装失败 (尝试: ${tried[*]:-}). 备注: $note"
else
warning "$name 安装失败 (尝试: ${tried[*]:-})"
fi
return 1
}
# ----------------------------------------------------------------------------
# 过滤: --only / --skip
# ----------------------------------------------------------------------------
should_handle() {
local name="$1"
if [[ -n "$ONLY_TOOLS" ]]; then
[[ ",$ONLY_TOOLS," == *",$name,"* ]] && return 0 || return 1
fi
if [[ -n "$SKIP_TOOLS" ]]; then
[[ ",$SKIP_TOOLS," == *",$name,"* ]] && return 1 || return 0
fi
return 0
}
# ----------------------------------------------------------------------------
# 主流程
# ----------------------------------------------------------------------------
main() {
detect_distro
setup_sudo
load_map
discover_tools
validate_tool_coverage
echo ""
echo "============================================================"
echo " CyberStrikeAI 工具安装器"
echo "============================================================"
note "项目根: $ROOT_DIR"
note "系统: $DISTRO_ID (family=$DISTRO_FAMILY, pkg=$PKG_MGR)"
note "模式: $MODE"
note "pip 源: $PIP_INDEX_URL"
note "go 代理: $GOPROXY"
if [[ -n "$FORCE_METHOD" ]]; then note "强制方式: $FORCE_METHOD"; fi
[[ -n "$ONLY_TOOLS" ]] && note "白名单: $ONLY_TOOLS"
[[ -n "$SKIP_TOOLS" ]] && note "黑名单: $SKIP_TOOLS"
if [[ "$DISTRO_FAMILY" == "macos" && "$MODE" == "install" ]]; then
warning "macOS 无 apt, 将自动尝试: brew → pip(PyPI 探测) → go → GitHub"
fi
echo ""
# 累计工具列表 (按映射表的顺序)
local names=()
while IFS='|' read -r name _; do
[[ -z "$name" || "$name" == \#* ]] && continue
# 优先用 yaml 里的 (排除 yaml 关闭的)
if [[ "${YAML_ENABLED[$name]:-true}" == "false" ]]; then
STATUS["$name"]="skip"
SKIP_REASON["$name"]="yaml 中 enabled=false"
continue
fi
names+=("$name")
done < <(get_tool_map)
TOTAL=${#names[@]}
if [[ "$MODE" == "list" ]]; then
info "检测 $TOTAL 个工具状态..."
echo ""
else
info "准备处理 $TOTAL 个工具..."
echo ""
fi
local i=0
for name in "${names[@]}"; do
i=$((i+1))
if ! should_handle "$name"; then
STATUS["$name"]="skip"
SKIP_REASON["$name"]="被 --skip / 未在 --only"
SKIP_COUNT=$((SKIP_COUNT+1))
continue
fi
if [[ "$MODE" != "list" ]]; then
printf "${GRAY}[%d/%d]${NC} " "$i" "$TOTAL"
fi
install_tool "$name"
done
if [[ "$MODE" == "list" ]]; then
echo ""
echo " 工具名 | 命令 | 状态 | 说明"
echo " --------------------------------+--------------+-----------+--------"
for name in "${names[@]}"; do
local cmd note_text st st_color
cmd="$(resolve_cmd "$name")"
note_text="${M[$name|note]:-}"
st="${STATUS[$name]:-?}"
case "$st" in
ok) st_color="${GREEN} OK ${NC}" ;;
skip) st_color="${GRAY}SKIP${NC}" ;;
fail) st_color="${RED}FAIL${NC}" ;;
*) st_color="${YELLOW} ? ${NC}" ;;
esac
printf " %-32s | %-13s| %b | %s\n" "$name" "$cmd" "$st_color" "$note_text"
done
fi
echo ""
print_summary
}
print_summary() {
echo "============================================================"
echo " 安装结果汇总"
echo "============================================================"
if [[ "$MODE" == "check" || "$MODE" == "list" ]]; then
info "总计: $TOTAL | ✅ 已就绪: $OK_COUNT | ⏭ 跳过: $SKIP_COUNT | ❌ 缺失: $FAIL_COUNT"
else
info "总计: $TOTAL | ✅ 成功: $OK_COUNT | ⏭ 跳过: $SKIP_COUNT | ❌ 失败: $FAIL_COUNT"
fi
echo ""
if [[ $FAIL_COUNT -gt 0 ]]; then
note "失败工具:"
for name in "${!STATUS[@]}"; do
[[ "${STATUS[$name]}" == "fail" ]] && echo " - $name"
done | sort
echo ""
note "常见补救:"
if [[ "$DISTRO_FAMILY" == "macos" ]]; then
dim " • macOS: brew install <工具名> 或 pip3 install <工具名>"
dim " • 部分工具仅 Kali/apt 提供, macOS 请用 pip/go 或手动安装"
else
dim " • 在 Kali 上先运行: sudo apt update"
fi
dim " • 确认外网可达, 或设置代理: HTTPS_PROXY=http://your-proxy:port"
dim " • 大型工具如 ghidra/clair 可手动安装"
dim " • API 类工具 (fofa/shodan/zoomeye/quake) 需自行申请并配置 API key"
fi
# PATH 提示
if [[ -d "$HOME/go/bin" ]] && [[ ":$PATH:" != *":$HOME/go/bin:"* ]]; then
echo ""
warning "go install 安装的二进制在: $HOME/go/bin"
warning "请把它加入 PATH, 例如: echo 'export PATH=\$HOME/go/bin:\$PATH' >> ~/.bashrc"
fi
}
main
# check 模式: 有缺失工具时返回非零退出码 (便于 CI)
if [[ "$MODE" == "check" && $FAIL_COUNT -gt 0 ]]; then
exit 1
fi