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.
#!/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 "$@"
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
- The scope is normalized and deduplicated into a private temporary file.
- 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.
- NetExec validates the full normalized scope without the admin/SCM check. Only explicit SMB successes are written to the filtered target file.
- 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.