Install
$ agentstack add skill-thapr0digy-skills-recon-passive ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
@pentest-core/skills/shared/engagement-resolver.md @pentest-core/skills/shared/scope-validator.md
/recon-passive — Open Source Intelligence Gathering
You are conducting authorized open source intelligence (OSINT) collection on organizational assets. Your goal is to gather publicly available intelligence using legitimate research methods WITHOUT directly interacting with target infrastructure. All data collection uses public databases, search engines, and archived records within authorized scope.
Step 1: Resolve Active Engagement
Run the engagement resolver block verbatim before any other logic.
# --- Engagement Resolver ---
ENGAGEMENT_JSON=$(cat ~/.pentest/active-engagement 2>/dev/null)
if [ -z "$ENGAGEMENT_JSON" ]; then
echo "No active engagement found. Run /pentest-init or /pentest-switch." >&2
exit 1
fi
if [ ! -f "$ENGAGEMENT_JSON" ]; then
echo "Active engagement path '$ENGAGEMENT_JSON' does not exist. Run /pentest-switch." >&2
exit 1
fi
ENGAGEMENT_ID=$(jq -r '.engagement_id' "$ENGAGEMENT_JSON")
OUTPUT_DIR=$(jq -r '.output_dir' "$ENGAGEMENT_JSON")
ASSESSOR=$(whoami)
ASSESSOR_MATCH=$(jq -r --arg h "$ASSESSOR" '.security assessors[] | select(.handle == $h) | .handle' "$ENGAGEMENT_JSON")
if [ -z "$ASSESSOR_MATCH" ]; then
echo "Operator '$ASSESSOR' is not registered on this engagement." >&2
exit 1
fi
# --- End Engagement Resolver ---
After this block, $ENGAGEMENT_ID, $OUTPUT_DIR, and $ASSESSOR are guaranteed set and valid. If the resolver fails for any reason, stop and tell the user to run /pentest-init.
Step 2: Parse Arguments and Determine Targets
If an argument was provided (e.g., /recon-passive acme.com)
Use the argument as the primary target domain:
TARGET_DOMAIN="acme.com" # from user argument
If no argument was provided
Extract domains from the engagement scope:
SCOPE_DOMAINS=$(jq -r '.scope.in_scope[] | select(.type == "domain") | .value' "$ENGAGEMENT_JSON" \
| sed 's/^\*\.//')
If SCOPE_DOMAINS is empty after extraction, ask the user which domain(s) to run passive recon against before continuing.
Build a list of all targets to enumerate — there may be more than one. For each target in the list, run all subsequent steps.
Scope validation
For each target domain, run scope validation per the scope validator instructions. For passive recon, skip the testing window check — passive recon produces no traffic to the target and is permitted outside testing windows. Still perform the out-of-scope hard block and the in-scope/unlisted confirmation checks.
# Check out-of-scope (hard block)
OUT_OF_SCOPE=$(jq -r '.scope.out_of_scope[]' "$ENGAGEMENT_JSON" 2>/dev/null)
# Check in-scope (confirm or warn if unlisted)
IN_SCOPE=$(jq -r '.scope.in_scope[]' "$ENGAGEMENT_JSON" 2>/dev/null)
Log all scope check outcomes to ${OUTPUT_DIR}/activity.log using action: "scope_check" per the scope validator log entry reference.
Step 3: Check Tool Availability
Check for each tool with which 2>/dev/null. Record availability — do not abort if a tool is missing; skip that step and note the gap in the output.
Required tools and the steps they support:
| Tool | Step | |---|---| | subfinder | 5 — Subdomain Enumeration | | amass | 5, 14 — Subdomain Enumeration, ASN | | dnsx | 7 — DNS Resolution | | theHarvester | 8 — Email and Name Harvesting | | gau | 12 — URL Discovery | | waybackurls | 12 — URL Discovery | | cloud_enum | 13 — Cloud Asset Discovery | | cloudlist | 13 — Cloud Asset Discovery | | trufflehog | 10 — GitHub/GitLab Recon |
for TOOL in subfinder amass dnsx theHarvester gau waybackurls cloud_enum cloudlist trufflehog; do
if which "$TOOL" > /dev/null 2>&1; then
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" \
--arg action "tool_check" \
--arg command "which $TOOL" \
--arg status "completed" \
--arg result "available" \
'{ts:$ts, assessor:$assessor, action:$action, command:$command, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
else
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" \
--arg action "tool_check" \
--arg command "which $TOOL" \
--arg status "skipped" \
--arg result "not found" \
'{ts:$ts, assessor:$assessor, action:$action, command:$command, status:$status, result:$result}' \
>> "${OUTPUT_DIR}/activity.log"
fi
done
Create the output directory for this recon run:
mkdir -p "${OUTPUT_DIR}/recon"
RECON_DIR="${OUTPUT_DIR}/recon"
TIMESTAMP=$(date -u +"%Y%m%dT%H%M%SZ")
Step 4: WHOIS and DNS Records
Run synchronously. These tools are always available on standard systems.
# WHOIS
whois "$TARGET_DOMAIN" > "${RECON_DIR}/whois-${TARGET_DOMAIN}.txt" 2>/dev/null
# DNS records
for QTYPE in ANY MX TXT NS SOA; do
dig "$QTYPE" "$TARGET_DOMAIN" +noall +answer >> "${RECON_DIR}/dns-${TARGET_DOMAIN}.txt" 2>/dev/null
done
Parse and extract from the results:
- Registrant — organization name and contact email from WHOIS output
- Nameservers — authoritative NS records
- MX records — mail exchangers and their priorities
- SPF —
v=spf1entry from TXT records - DKIM — any
._domainkey.TXT entries discovered - DMARC —
_dmarc.TXT record
Store parsed values in a WHOIS_DATA and DNS_DATA structure for inclusion in the final JSON output (Step 16).
Log the action:
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$TARGET_DOMAIN" \
--arg command "whois $TARGET_DOMAIN; dig ANY/MX/TXT/NS/SOA $TARGET_DOMAIN" \
--arg status "completed" \
--arg output_file "${RECON_DIR}/dns-${TARGET_DOMAIN}.txt" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target,
command:$command, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
Step 5: Subdomain Enumeration
Run only for root domains. If the target is already a specific subdomain (e.g., app.acme.com — has more than one dot-separated label before the TLD), skip this step entirely and note: "Target is a subdomain — subdomain enumeration skipped." The target is already resolved.
Detect root vs subdomain:
# Count labels before TLD. "acme.com" = root, "app.acme.com" = subdomain.
LABEL_COUNT=$(echo "$TARGET_DOMAIN" | awk -F. '{print NF}')
IS_ROOT_DOMAIN=false
if [ "$LABEL_COUNT" -le 2 ]; then
IS_ROOT_DOMAIN=true
elif echo "$TARGET_DOMAIN" | grep -qE '\.(co|com|org|net|gov)\.[a-z]{2}$'; then
# Handle ccTLDs like .co.uk, .com.au — 3 labels is still a root domain
[ "$LABEL_COUNT" -le 3 ] && IS_ROOT_DOMAIN=true
fi
If $IS_ROOT_DOMAIN is false, skip to Step 6.
For root domains, run subfinder and amass in passive mode. For large domains (more than 500 subdomains expected, or when the user requests it), offer to run these asynchronously.
SUBDOMAINS_FILE="${RECON_DIR}/subdomains-${TARGET_DOMAIN}.txt"
# subfinder
if [ "$IS_ROOT_DOMAIN" = "true" ] && which subfinder > /dev/null 2>&1; then
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "subfinder -d \"$TARGET_DOMAIN\" -silent -o \"${RECON_DIR}/subfinder-${TARGET_DOMAIN}.txt\"" --arg status "dispatched" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target, command:$command, status:$status}' \
>> "${OUTPUT_DIR}/activity.log"
subfinder -d "$TARGET_DOMAIN" -silent -o "${RECON_DIR}/subfinder-${TARGET_DOMAIN}.txt"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "subfinder -d \"$TARGET_DOMAIN\" -silent -o \"${RECON_DIR}/subfinder-${TARGET_DOMAIN}.txt\"" \
--arg status "$([ $? -eq 0 ] && echo completed || echo failed)" \
--arg output_file "${RECON_DIR}/subfinder-${TARGET_DOMAIN}.txt" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target,
command:$command, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
fi
# amass (passive only)
if [ "$IS_ROOT_DOMAIN" = "true" ] && which amass > /dev/null 2>&1; then
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "amass enum -passive -d \"$TARGET_DOMAIN\" -o \"${RECON_DIR}/amass-${TARGET_DOMAIN}.txt\"" --arg status "dispatched" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target, command:$command, status:$status}' \
>> "${OUTPUT_DIR}/activity.log"
amass enum -passive -d "$TARGET_DOMAIN" -o "${RECON_DIR}/amass-${TARGET_DOMAIN}.txt"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "amass enum -passive -d \"$TARGET_DOMAIN\" -o \"${RECON_DIR}/amass-${TARGET_DOMAIN}.txt\"" \
--arg status "$([ $? -eq 0 ] && echo completed || echo failed)" \
--arg output_file "${RECON_DIR}/amass-${TARGET_DOMAIN}.txt" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target,
command:$command, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
fi
# Merge and deduplicate
cat "${RECON_DIR}/subfinder-${TARGET_DOMAIN}.txt" \
"${RECON_DIR}/amass-${TARGET_DOMAIN}.txt" \
2>/dev/null \
| sort -u > "$SUBDOMAINS_FILE"
Step 6: Certificate Transparency
Query crt.sh database directly for certificate transparency logs — faster than web API.
CT_FILE="${RECON_DIR}/crt-${TARGET_DOMAIN}.txt"
# Direct PostgreSQL query to crt.sh database for better performance
if which psql > /dev/null 2>&1; then
# Use timeout to avoid hanging and optimize query for performance
timeout 30s psql -h crt.sh -p 5432 -U guest -d certwatch -t -c "
SELECT DISTINCT regexp_replace(name_value, '^\*\.', '', 'g')
FROM certificate_identity
WHERE lower(name_value) LIKE lower('%.${TARGET_DOMAIN}')
OR lower(name_value) = lower('${TARGET_DOMAIN}')
LIMIT 5000;
" 2>/dev/null | grep -v '^\s*$' | sort -u > "$CT_FILE"
# Check if query succeeded and produced results
if [ $? -ne 0 ] || [ ! -s "$CT_FILE" ]; then
echo "Warning: PostgreSQL query failed or timed out, falling back to web API"
curl -s "https://crt.sh/?q=%25.${TARGET_DOMAIN}&output=json" \
| jq -r '.[].name_value' \
| sed 's/\*\.//g' \
| sort -u > "$CT_FILE"
fi
else
# Fallback to web API if psql not available
echo "Info: psql not found, using web API"
curl -s "https://crt.sh/?q=%25.${TARGET_DOMAIN}&output=json" \
| jq -r '.[].name_value' \
| sed 's/\*\.//g' \
| sort -u > "$CT_FILE"
fi
Merge the crt.sh results into the deduplicated subdomain list:
cat "$SUBDOMAINS_FILE" "$CT_FILE" | sort -u > "${RECON_DIR}/subdomains-merged-${TARGET_DOMAIN}.txt"
mv "${RECON_DIR}/subdomains-merged-${TARGET_DOMAIN}.txt" "$SUBDOMAINS_FILE"
Log the action:
# Determine which method was used based on file size and psql availability
if which psql > /dev/null 2>&1 && [ -s "$CT_FILE" ]; then
COMMAND_DESC="PostgreSQL direct query to crt.sh certificate transparency database"
METHOD="database"
else
COMMAND_DESC="Web API fallback query to crt.sh certificate transparency logs"
METHOD="web_api"
fi
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" \
--arg action "enum" \
--arg target "$TARGET_DOMAIN" \
--arg command "$COMMAND_DESC" \
--arg method "$METHOD" \
--arg status "completed" \
--arg output_file "$CT_FILE" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target,
command:$command, method:$method, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
Step 7: DNS Resolution
Resolve each discovered subdomain to its A record(s). Skip if dnsx is not available and note the gap.
RESOLVED_FILE="${RECON_DIR}/resolved-${TARGET_DOMAIN}.json"
if which dnsx > /dev/null 2>&1; then
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "dnsx -l \"$SUBDOMAINS_FILE\" -silent -a -resp -json -o \"$RESOLVED_FILE\"" --arg status "dispatched" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target, command:$command, status:$status}' \
>> "${OUTPUT_DIR}/activity.log"
dnsx -l "$SUBDOMAINS_FILE" -silent -a -resp -json -o "$RESOLVED_FILE"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "dnsx -l \"$SUBDOMAINS_FILE\" -silent -a -resp -json -o \"$RESOLVED_FILE\"" \
--arg status "$([ $? -eq 0 ] && echo completed || echo failed)" \
--arg output_file "$RESOLVED_FILE" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target,
command:$command, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
else
echo "dnsx not available — DNS resolution step skipped" >&2
fi
Step 8: Email and Name Harvesting
Use theHarvester to collect email addresses, employee names, and associated metadata from public sources.
HARVESTER_DIR="${RECON_DIR}/harvester-${TARGET_DOMAIN}"
mkdir -p "$HARVESTER_DIR"
if which theHarvester > /dev/null 2>&1; then
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "theHarvester -d \"$TARGET_DOMAIN\" -b all -f \"${HARVESTER_DIR}/output\"" --arg status "dispatched" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target, command:$command, status:$status}' \
>> "${OUTPUT_DIR}/activity.log"
theHarvester -d "$TARGET_DOMAIN" -b all -f "${HARVESTER_DIR}/output"
jq -nc \
--arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg assessor "$ASSESSOR" --arg action "enum" \
--arg target "$TARGET_DOMAIN" --arg command "theHarvester -d \"$TARGET_DOMAIN\" -b all -f \"${HARVESTER_DIR}/output\"" \
--arg status "$([ $? -eq 0 ] && echo completed || echo failed)" \
--arg output_file "${HARVESTER_DIR}/output.json" \
'{ts:$ts, assessor:$assessor, action:$action, target:$target,
command:$command, status:$status, output_file:$output_file}' \
>> "${OUTPUT_DIR}/activity.log"
else
echo "theHarvester not available — email harvesting step skipped" >&2
fi
Step 9: Google Dorking
Generate dork queries for the target. Do NOT execute these queries — present them to the user for manual execution in a browser. Automated Google scraping violates ToS and often results in CAPTCHA blocks.
Present the following queries to the user as a formatted list:
site:$TARGET_DOMAIN # all indexed pages
site:$TARGET_DOMAIN filetype:pdf # public PDF documents
site:$TARGET_DOMAIN filetype:xls OR filetype:xlsx # spreadsheets
site:$TARGET_DOMAIN filetype:doc OR filetype:docx # Word documents
site:$TARGET_DOMAIN inurl:admin # admin panels
site:$TARGET_DOMAIN inurl:login # login pages
site:$TARGET_DOMAIN inurl:api # API endpoints
site:$TARGET_DOMAIN intitle:"index of" # directory listings
site:$TARGET_DOMAIN ext:env OR ext:config # config files
site:$TARGET_DOMAIN ext:sql OR ext:bak OR ext:backup # database/backup files
"@$TARGET_DOMAIN" -site:$TARGET_DOMAIN # email addresses off-site
"$TARGET_DOMAIN" password OR credential OR secret # credential leaks
Tell the user: "These dork queries are pr
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: thapr0digy
- Source: thapr0digy/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.