agentleFS
forefyforefy/.context/skills/hunter-utils/external-enumeration/SKILL.md

external-enumeration

Skill153 starsChanged 44 days agoMITView on GitHub

Passively map a company's domains, subdomains, DNS ownership, tech stack, and CDNs. Use to recon an external attack surface or enumerate subdomains.

  • Installs packages

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 10 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈10

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Ask the user for:

    line 15
  2. Distinctive:

    Before enumerating subdomains, confirm what domains are actually owned by the target.

    line 25
  3. Distinctive:

    Collect ALL headers in a single pass: Do not come back for a second pass.

    line 128
  4. Distinctive:

    If redirect leads to Microsoft login:

    line 194
  5. Distinctive:

    When Cloudflare blocks curl, use stealth Playwright.

    line 222
  6. Distinctive:

    Don't run on Cloudflare IPs, don't run on WAFs/CDNs protected targets.

    line 268
  7. Distinctive:

    Only look for applicative ports, don't overdo the web scan to more than a few strategic port decisions (passive-first approach).

    line 269
  8. Distinctive:

    Check registrant/NS of likely related domains (founder names, product names)

    line 291
  9. Distinctive:

    Always flag these automatically in the report:

    line 299
  10. Distinctive:

    Always produce the markdown report with this structure:

    line 316

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

External Enumeration Skill

You are performing passive external reconnaissance on a target company's domain infrastructure. Goal: produce a comprehensive, structured map of all domains, subdomains, technology stack, and notable security observations - using only passive/OSINT techniques (no active exploitation).


Phase 0 - Scope Clarification

Ask the user for:

  1. Primary domain(s) to enumerate (e.g. example.com)
  2. Known subsidiaries or related companies (acquisitions, brand names, sister domains)
  3. Output format - markdown report to a file path?
  4. Depth - quick pass (passive DNS only) or deep pass (stealth browser + port scan)?

Phase 1 - Domain Ownership & Attribution

Before enumerating subdomains, confirm what domains are actually owned by the target.

1.1 WHOIS Check

whois <domain> | grep -iE 'registrar|creation|name server|registrant'

Note: Most domains use privacy protection (e.g. GoDaddy DomainsbyProxy) - registrant names will be hidden. Do NOT rely on registrant names for attribution.

1.2 Nameserver Correlation (Primary Attribution Method)

for d in domain1.com domain2.io domain3.net; do
  echo -n "$d: "; dig NS $d +short | sort | tr '\n' ' '; echo
done

Identical NS pairs = same DNS account = same owner. This is the strongest passive attribution proof even when WHOIS is privacy-protected.

1.3 MX + TXT Record Cross-Reference

dig MX <domain> +short
dig TXT <domain> +short
  • Shared *.mail.protection.outlook.com MX = same Microsoft 365 tenant
  • TXT records reveal: Azure site verifications, Google Workspace, Atlassian, SendGrid
  • Azure TXT format: MS=ms... or azurewebsites.net subdomain references → same Azure tenant

1.4 Similar-Name Domain Trap

Note: Always verify similar-sounding domains (e.g. target.net, target.co) are actually owned by the target - different registrar or NS pair = likely unrelated squatter. Never assume.


Phase 2 - Multi-Source Subdomain Enumeration

Run all sources in parallel on first pass. crt.sh alone is never sufficient.

2.1 Certificate Transparency (crt.sh)

curl -s "https://crt.sh/?q=%.example.com&output=json" \
  | python3 -c "import sys,json; [print(e['name_value']) for e in json.load(sys.stdin)]" \
  | tr ',' '\n' | sort -u | grep -v '^\*'

2.2 HackerTarget

curl -s "https://api.hackertarget.com/hostsearch/?q=example.com" | cut -d',' -f1 | sort -u

2.3 Wayback Machine

curl -s "https://web.archive.org/cdx/search/cdx?url=*.example.com&output=text&fl=original&collapse=urlkey" \
  | grep -oP '[\w.-]+\.example\.com' | sort -u

2.4 urlscan.io

curl -s "https://urlscan.io/api/v1/search/?q=domain:example.com&size=100" \
  | python3 -c "import sys,json; d=json.load(sys.stdin); [print(r['page']['domain']) for r in d.get('results',[])]" \
  | sort -u

2.5 RapidDNS

curl -s "https://rapiddns.io/subdomain/example.com?full=1" \
  | grep -oP '[\w.-]+\.example\.com' | sort -u

2.6 AlienVault OTX

Note: Turn off ssl verification if using Python urllib. Rate limiting is aggressive; skip if blocked.

curl -s "https://otx.alienvault.com/api/v1/indicators/domain/example.com/passive_dns" \
  | python3 -c "import sys,json; [print(r.get('hostname','')) for r in json.load(sys.stdin).get('passive_dns',[])]" \
  | sort -u

2.7 subfinder (if installed)

subfinder -d example.com -silent 2>/dev/null | sort -u

Install: brew install subfinder

2.8 Deduplicate Everything

cat all_sources.txt | sort -u > subdomains_unique.txt
wc -l subdomains_unique.txt

Phase 3 - DNS Resolution & Live Check

# Resolve all subdomains, identify live ones
while read sub; do
  ip=$(dig +short A "$sub" 2>/dev/null | grep -m1 -oP '\d+\.\d+\.\d+\.\d+')
  if [ -n "$ip" ]; then
    echo "$sub -> $ip"
  fi
done < subdomains_unique.txt

Phase 4 - One-Pass Comprehensive Header Harvest

Collect ALL headers in a single pass. Do not come back for a second pass.

collect_headers() {
  local sub=$1
  local result=$(curl -sk -o /dev/null \
    --max-time 10 \
    -D - \
    -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
    "https://$sub" 2>/dev/null | head -80)

  local status=$(echo "$result" | head -1 | grep -oP '\d{3}')
  local ip=$(dig +short A "$sub" 2>/dev/null | grep -m1 -oP '\d+\.\d+\.\d+\.\d+')

  python3 -c "
import sys, json
headers_raw = '''$result'''
h = {}
for line in headers_raw.split('\n')[1:]:
    if ': ' in line:
        k, v = line.split(': ', 1)
        h[k.lower().strip()] = v.strip()
print(json.dumps({'subdomain': '$sub', 'status': '$status', 'ip': '$ip', **h}))
"
}

Phase 5 - Technology Identification

5.1 From Server Headers

server valueTechnology
cloudflareCloudflare WAF/proxy
gunicornPython WSGI - direct exposure, no WAF
nginxNginx (may be direct or behind CDN)
UploadServerGoogle Cloud Storage
AmazonS3AWS S3 bucket

5.2 From CNAME Chains

dig CNAME <subdomain> +short
CNAME targetPlatform
*.hubspot.netHubSpot (email/link tracking)
*.lmspowered.comLearnUpon LMS
*.partner-experience.comPartner portal SaaS
*.pendo.ioPendo product analytics
*.salesforce.comSalesforce CRM
*.zendesk.comZendesk support
*.freshdesk.comFreshdesk support
*.atlassian.netAtlassian (Jira/Confluence)
*.cloudfront.netAWS CloudFront (if direct CNAME, not leaked via)
*.vercel.appVercel hosting
*.netlify.appNetlify hosting
*.azurewebsites.netAzure App Service

5.3 From Redirect Targets (location header)

  • launcher.myapps.microsoft.com → Azure AD SSO (check URL for tenant ID)
  • *.okta.com → Okta SSO
  • accounts.google.com → Google Workspace SSO
  • *.auth0.com → Auth0

5.4 Azure AD Tenant ID Extraction

If redirect leads to Microsoft login:

location: https://launcher.myapps.microsoft.com/api/signin/APP_ID?tenantId=TENANT_UUID

Extract tenantId= value - this is the organization's Azure AD tenant ID.


Phase 6 - Dual CDN Detection

This is a high-value finding often missed on first pass.

The pattern: Cloudflare as outer WAF → AWS CloudFront as real CDN → origin

curl -sI https://app.example.com | grep -iE 'via|x-amz|x-cache|server'

Indicators:

  • server: cloudflare AND via: 1.1 xxxxxxxxx.cloudfront.net (CloudFront) → dual CDN confirmed
  • x-amz-cf-pop: TLV55-P1 → CloudFront PoP in Tel Aviv
  • x-cache: Miss from cloudfront → CloudFront active behind CF
  • x-amz-cf-id: → unique CloudFront request ID

Why it matters: Reveals true backend CDN provider, geographic PoP locations, and hints at origin server region.


Phase 7 - Cloudflare Bypass (Stealth Browser)

When Cloudflare blocks curl, use stealth Playwright.

Note: Critical package name: Use puppeteer-extra-plugin-stealth - NOT playwright-extra-plugin-stealth (that package does NOT exist and will throw an error).

cd /tmp && mkdir cf-stealth && cd cf-stealth
npm init -y
npm install playwright playwright-extra puppeteer-extra-plugin-stealth
npx playwright install chromium
const { chromium } = require('playwright-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
chromium.use(StealthPlugin());

const targets = [
  'https://app.example.com',
];

(async () => {
  const browser = await chromium.launch({ headless: true });
  for (const url of targets) {
    const page = await browser.newPage();
    const headers = {};
    page.on('response', async resp => {
      if (resp.url() === url || resp.url().startsWith(url)) {
        Object.assign(headers, resp.headers());
      }
    });
    try {
      await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
      console.log(JSON.stringify({ url, headers }));
    } catch(e) {
      console.log(JSON.stringify({ url, error: e.message }));
    }
    await page.close();
  }
  await browser.close();
})();

Phase 8 - Nonintrusive Port Probe (Second Pass)

Don't run on Cloudflare IPs, don't run on WAFs/CDNs protected targets. Only look for applicative ports, don't overdo the web scan to more than a few strategic port decisions (passive-first approach).

for host in direct-ip-1 direct-ip-2; do
  for port in 80 443 8080 8443 3000 4443; do
    result=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" \
      "$([ $port = 443 ] || [ $port = 8443 ] && echo https || echo http)://$host:$port/")
    [ "$result" != "000" ] && echo "$host:$port -> HTTP $result"
  done
done

Phase 9 - Subsidiary & Acquisition Research

Stealth startups acquired by the target may have no public domain - this is normal.

Search strategy:

  1. "[company name]" acquisition site:crunchbase.com
  2. "[company name]" acquired site:techcrunch.com OR site:businesswire.com
  3. LinkedIn: search target company name → filter by "acquired by" or check leadership history
  4. Check registrant/NS of likely related domains (founder names, product names)

Note: A stealth startup may have: no domain, no Wayback archive, no CT certificates, no passive DNS entries - this is expected, not a gap in enumeration.


Phase 10 - Notable Findings (Auto-Flag)

Always flag these automatically in the report:

PatternFlag
access-control-allow-origin: *Warning - Open CORS - unauthenticated cross-origin requests allowed
HTTP 525 statusWarning - SSL Handshake Failure - origin SSL misconfiguration behind Cloudflare
server: gunicorn or server: unicorn with no CDNWarning - Direct backend exposure - no WAF, origin IP exposed
HTTP 301 → self (same host)Info - Likely internal-only / auth-required (especially on cslab*, vpn*, admin*)
Azure AD tenantId= in redirect URLInfo - Azure AD Tenant ID leak - extract UUID
x-amz-cf-pop with city codeInfo - CDN geographic PoP - reveals infrastructure region
via: *.cloudfront.net on a CF-served domainInfo - Dual CDN architecture
Subdomain → *.mail.protection.outlook.com CNAMEInfo - Microsoft 365 tenant confirmation

Phase 11 - Report Structure

Always produce the markdown report with this structure:

# [Company] Recon Report
**Date:** YYYY-MM-DD  
**Scope:** domain1.com · domain2.io · domain3.com

## Table of Contents
1. Confirmed Owned Domains
2. Technology Stack Summary
3. Notable Findings
4. [Primary Domain] - Subdomain Headers
   - Live - Cloudflare + CloudFront (Dual CDN)
   - Live - Cloudflare Only
   - Live - Third-Party / No CDN
   - Cloudflare DNS-Only / No HTTP Response
5. [Other domains]
6. Ownership & Attribution
7. Enumeration Sources

## 1. Confirmed Owned Domains
| Domain | Registrar | Created | Nameservers | Verdict |

## 2. Technology Stack Summary
| Layer | Technology | Evidence |

## 3. Notable Findings
| Finding | Detail |

## 4. Per-Subdomain Tables
### [subdomain]
| Header | Value |
(include: status, ip, server, via, x-amz-cf-pop, x-cache, x-frame-options, CSP, CORS, location, platform)

## 5. Ownership & Attribution
(people, entities, investors, Azure tenant, WHOIS proof)

## 6. Enumeration Sources
| Source | Results | Method |
**Total unique subdomains: N**  
**Vercel detected: yes / no**

Common Pitfalls Reference

PitfallCorrect Approach
Assuming WHOIS registrant names identify ownerUse NS correlation + MX + TXT instead (privacy protection hides names)
Assuming target.net or target.co = same companyVerify NS + MX independently - often different owners
Running crt.sh only for subdomain discoveryAlways run 6+ sources in parallel
Doing header harvest after initial reconCollect ALL headers in the first live check pass
Port scanning CF-fronted IPsPointless - Cloudflare terminates connections. Only port-scan direct/non-CF IPs

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.