#!/usr/bin/env bash # ©AngelaMos | 2026 # install.sh # # One-shot installer for tlsfp. Goes from a fresh machine to `tlsfp` on your # PATH with the intelligence database seeded, whether run from a clone or piped # straight from the web: # # curl -fsSL https://angelamos.com/tlsfp/install.sh | bash # # Pass --live to also grant the raw-socket capabilities live capture needs. set -euo pipefail # ============================================================================ # Config # ============================================================================ REPO_OWNER="CarterPerez-dev" REPO_NAME="Cybersecurity-Projects" SUBDIR="PROJECTS/intermediate/ja3-ja4-tls-fingerprinting" BINARY="tlsfp" CRATE="crates/tlsfp" REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}.git" DEFAULT_BRANCH="main" JA4DB_URL="${JA4DB_URL:-https://ja4db.com/api/read/}" JA4DB_TIMEOUT="${JA4DB_TIMEOUT:-180}" PREFIX="${TLSFP_PREFIX:-}" # cargo install --root; empty = cargo's default (~/.cargo/bin) DO_LIVE=0 # ============================================================================ # Colors (gated so `| bash`, logs, and CI stay clean) # ============================================================================ if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then BOLD=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GREEN=$'\033[32m' YELLOW=$'\033[33m'; CYAN=$'\033[36m'; RESET=$'\033[0m' else BOLD=""; DIM=""; RED=""; GREEN=""; YELLOW=""; CYAN=""; RESET="" fi info() { printf '%s\n' " ${CYAN}+${RESET} $*" >&2; } ok() { printf '%s\n' " ${GREEN}+${RESET} $*" >&2; } warn() { printf '%s\n' " ${YELLOW}!${RESET} $*" >&2; } die() { printf '%s\n' " ${RED}x $*${RESET}" >&2; exit 1; } header(){ printf '\n%s\n\n' "${BOLD}${CYAN}--- $* ---${RESET}" >&2; } have() { command -v "$1" >/dev/null 2>&1; } trap 'printf "%s\n" "${RED}x install failed${RESET}" >&2' ERR TMP_DIR="" cleanup() { [ -n "$TMP_DIR" ] && rm -rf "$TMP_DIR"; return 0; } trap cleanup EXIT banner() { printf '%s' "${CYAN}${BOLD}" >&2 cat >&2 <<'ART' _ _ __ | |_| |___ / _|_ __ | __| / __| |_| '_ \ | |_| \__ \ _| |_) | \__|_|___/_| | .__/ |_| ART printf '%s\n' "${RESET}" >&2 printf '%s\n' " ${DIM}JA3/JA4 TLS fingerprinting, intel matching, anomaly detection${RESET}" >&2 } # ============================================================================ # Privilege + package-manager fan # ============================================================================ SUDO="" if [ "$(id -u)" -ne 0 ]; then if have sudo; then SUDO="sudo"; fi fi pkg_install() { # best-effort multi-distro install of the given package names if have apt-get; then $SUDO apt-get update -y >/dev/null 2>&1 || warn "apt update had errors (often unrelated repos); continuing" $SUDO apt-get install -y --no-install-recommends "$@" elif have dnf; then $SUDO dnf install -y "$@" elif have pacman; then $SUDO pacman -S --needed --noconfirm "$@" elif have zypper; then $SUDO zypper install -y "$@" elif have apk; then $SUDO apk add "$@" elif have brew; then brew install "$@" else return 1; fi } download() { # download URL DEST if have curl; then curl -fsSL --max-time "$JA4DB_TIMEOUT" "$1" -o "$2" elif have wget; then wget -q --timeout="$JA4DB_TIMEOUT" -O "$2" "$1" else return 1; fi } # ============================================================================ # Args # ============================================================================ usage() { cat >&2 </dev/null || warn "pull failed; using existing clone" else info "cloning ${REPO_NAME}" git clone --depth 1 --branch "$DEFAULT_BRANCH" --filter=blob:none --quiet "$REPO_URL" "$cache" \ || die "clone failed from ${REPO_URL}" fi printf '%s\n' "$cache/$SUBDIR" } # ============================================================================ # Rust toolchain # ============================================================================ ensure_rust() { if ! have cargo; then info "Rust not found — installing via rustup" if have curl; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path >&2 elif have wget; then wget -qO- https://sh.rustup.rs | sh -s -- -y --no-modify-path >&2 else die "need curl or wget to install Rust (or install it from https://rustup.rs)" fi # shellcheck disable=SC1091 . "${CARGO_HOME:-$HOME/.cargo}/env" fi have cargo || die "cargo still not on PATH after install; open a new shell and re-run" ok "cargo $(cargo --version | awk '{print $2}')" } # ============================================================================ # System build dependencies (libpcap + a C toolchain for bundled SQLite) # ============================================================================ ensure_build_deps() { # Only act if something is actually missing, so we never sudo for no reason. local cc_ok=0 pcap_ok=0 if have cc || have gcc || have clang; then cc_ok=1; fi if [ -e /usr/include/pcap/pcap.h ] || [ -e /usr/include/pcap.h ] || [ -e /opt/homebrew/include/pcap/pcap.h ]; then pcap_ok=1; fi if [ "$OS" = "darwin" ]; then if [ "$pcap_ok" -eq 0 ]; then pkg_install libpcap pkg-config || warn "could not install libpcap via brew; build may fail" fi return fi if [ "$pcap_ok" -eq 0 ] || [ "$cc_ok" -eq 0 ]; then info "installing build dependencies (libpcap headers + C toolchain)" if have apt-get; then pkg_install libpcap-dev pkg-config build-essential elif have dnf; then pkg_install libpcap-devel pkgconf-pkg-config gcc make elif have pacman; then pkg_install libpcap pkgconf base-devel elif have zypper; then pkg_install libpcap-devel pkg-config gcc make elif have apk; then pkg_install libpcap-dev pkgconfig build-base else warn "unknown package manager — ensure libpcap headers and a C compiler are installed"; fi fi } # ============================================================================ # Build + install onto PATH # ============================================================================ CARGO_BIN_DIR="${CARGO_HOME:-$HOME/.cargo}/bin" install_binary() { header "Building and installing tlsfp" if [ -n "$PREFIX" ]; then cargo install --path "$CRATE" --root "$PREFIX" --force >&2 CARGO_BIN_DIR="$PREFIX/bin" else cargo install --path "$CRATE" --force >&2 fi ok "installed ${BINARY} -> ${CARGO_BIN_DIR}/${BINARY}" } wire_path() { case ":$PATH:" in *":$CARGO_BIN_DIR:"*) ok "$CARGO_BIN_DIR already on PATH"; return ;; esac local shell rc="" shell="$(basename "${SHELL:-bash}")" case "$shell" in zsh) rc="$HOME/.zshrc" ;; fish) mkdir -p "$HOME/.config/fish/conf.d" echo "fish_add_path $CARGO_BIN_DIR" > "$HOME/.config/fish/conf.d/tlsfp.fish" ok "added $CARGO_BIN_DIR to PATH (fish)" ;; bash) rc="$HOME/.bashrc"; [ -f "$rc" ] || rc="$HOME/.bash_profile" ;; *) rc="$HOME/.profile" ;; esac if [ -n "$rc" ] && ! grep -q "$CARGO_BIN_DIR" "$rc" 2>/dev/null; then printf '\nexport PATH="%s:$PATH"\n' "$CARGO_BIN_DIR" >> "$rc" ok "added $CARGO_BIN_DIR to PATH in $rc" fi export PATH="$CARGO_BIN_DIR:$PATH" } # ============================================================================ # Seed the intelligence database # ============================================================================ seed_intel() { header "Seeding the intelligence database" info "loading the bundled feeds (abuse.ch SSLBL, salesforce/ja3, curated C2)" "$BINARY" intel seed >&2 ok "bundled feeds loaded" info "fetching the ja4db.com enrichment feed" if ! have curl && ! have wget; then warn "no curl/wget; skipping ja4db. The bundled feeds still work." return fi TMP_DIR="$(mktemp -d)" if download "$JA4DB_URL" "$TMP_DIR/ja4db.json"; then "$BINARY" intel import "$TMP_DIR/ja4db.json" >&2 && ok "ja4db imported" else warn "could not reach ${JA4DB_URL} (large and often slow); bundled feeds still work." warn "retry later: ${BINARY} intel import <(curl -fsSL ${JA4DB_URL})" fi } # ============================================================================ # Live-capture capabilities # ============================================================================ grant_live() { header "Enabling live capture" if [ "$OS" = "darwin" ]; then warn "setcap is Linux-only; on macOS run live capture under sudo: sudo ${BINARY} live " return fi if ! have setcap; then info "installing setcap (libcap)" if have apt-get; then pkg_install libcap2-bin elif have dnf || have zypper; then pkg_install libcap elif have pacman; then pkg_install libcap elif have apk; then pkg_install libcap fi fi local bin; bin="$(command -v "$BINARY")" if have setcap && $SUDO setcap cap_net_raw,cap_net_admin=eip "$bin"; then ok "live capture enabled for $bin (no sudo needed to run ${BINARY} live)" else warn "setcap unavailable; run live capture under sudo, or grant later:" warn " sudo setcap cap_net_raw,cap_net_admin=eip \"\$(command -v ${BINARY})\"" fi } # ============================================================================ # Main # ============================================================================ banner have "$BINARY" && info "existing install at $(command -v "$BINARY") — updating" PROJECT="$(resolve_project)" cd "$PROJECT" ensure_rust ensure_build_deps install_binary wire_path seed_intel [ "$DO_LIVE" -eq 1 ] && grant_live header "Verify" if have "$BINARY"; then ok "$($BINARY --version 2>/dev/null || echo "$BINARY installed")" else warn "installed to $CARGO_BIN_DIR but not yet on this shell's PATH" warn "open a new terminal, or: export PATH=\"$CARGO_BIN_DIR:\$PATH\"" fi printf '\n%s\n\n' " ${GREEN}${BOLD}tlsfp is ready.${RESET}" >&2 cat >&2 <