Org Attack Surface — Corporate-Family Footprint Mapping
Companion skills:
osint-methodology(the "how to think" 5-stage recon pipeline this plugs into) andoffensive-osint(the per-host arsenal you run once this skill hands you owned domains/netblocks — see its §14 Public Records and §28 Infrastructure OSINT for the tool directory this skill deepens rather than duplicates). This skill answers a different, upstream question: not "what doesacme.comexpose", but "what does Acme Corporation, the legal entity, own across every domain, netblock, and subsidiary it has — including the parts with no DNS trail back to the seed at all."
0. When to Use / When NOT
Use this skill when:
- The engagement starts from a company name or legal entity, not a domain — you need to derive the domain(s) first, not just enumerate one.
- You need to find an org's subsidiaries, sister brands, or M&A-acquired footprint that a DNS/CT-only sweep of one seed domain would never surface.
- You suspect "dark" IP space — netblocks or ASNs registered to the org's legal entity with no DNS record pointing at them (forgotten datacenter allocations, un-linked M&A infrastructure, IPs that only ever ran raw services).
- You need attribution discipline: every candidate domain/netblock/ASN this skill surfaces carries an explicit, auditable ownership score — never a bare "looks related."
- You are scoping a large or conglomerate engagement and need to prune scope by real corporate ownership before spending recon budget on strangers.
Do NOT use this skill when:
- You already have a confirmed, bounded target list and just need per-host recon — go straight to
offensive-osint. - The target's authorization isn't established — see §1.
- You need active exploitation or post-exploitation — out of scope here and everywhere in this skill family.
1. Authorization & Legal Posture
Same posture as the companion skills: intended for assets the operator owns or has written authorization to assess. This skill is unusually likely to surface entities the operator did NOT ask about (subsidiaries, sister brands, M&A targets) — that is the point, but it sharpens the scope question rather than loosening it.
Soft scope check — ask once when unclear:
"This will surface the target's subsidiaries and related infrastructure, some of which may be outside your engagement scope. Should I include the full corporate family, or stay bounded to the named entity and its direct domains?"
Always-on guardrail specific to this skill: every asset this skill discovers beyond the seed domain — related domains, dark netblocks, discover-only ASNs, index-search IPs — is a lead, not a target. See §6.6 and every "discover-only" callout below. Nothing this skill produces is automatically in scope for active testing; a human confirms ownership first.
2. Confidence Levels & Ownership Tiers (two axes — do not conflate them)
This skill runs two separate scoring axes, and mixing them up is the single most common way to misreport a finding.
Axis 1 — Finding confidence (same as the companion skills): TENTATIVE / FIRM / CONFIRMED. Answers "how sure am I this asset/record exists and I read it correctly."
Axis 2 — Ownership tier: answers "how sure am I this asset belongs to the target
organization." Computed by combining every OwnerSignal that fired for a candidate via an
independent-evidence formula (full mechanics in §8.4):
| Score band | Tier | Meaning |
|---|---|---|
| 0 | NONE |
No signal fired. |
| 0–39 | WEAK |
One low-weight signal (e.g. shared nameserver) — a lead, not attribution. |
| 40–69 | MODERATE |
Multiple weak signals, or one medium signal — worth an operator's eyeball. |
| 70–89 | STRONG |
Multiple independent signals, or one high-confidence signal — an active-scan-eligible score in a typical ASM promotion gate. |
| 90–100 | CONFIRMED |
Independent corroboration close to certainty, or a confirming signal (e.g. the seed domain itself) that forces 100 directly. |
A NOT_OWNED (stranger-lock) signal caps the score at 20 regardless of how many weak positives
also fired — so a foreign asset that happens to share a generic nameserver with the seed can never
climb into an owned tier.
Critical nuance: ownership tier is advisory, not a gate. Every domain/netblock/ASN/IP this skill
mints beyond the seed carries an explicit discover_only=True structural flag that keeps it out
of active scanning independent of its score — a related: domain that happens to score
STRONG (85) still does not get auto-scanned. The score tells the operator which discover-only
lead to promote first; only an explicit operator action (§10, promote-to-scan) moves an asset into
the active pipeline.
Rule of three still applies on top of the numeric score: a single signal type, however
individually weighted, is a lead. Treat anything below STRONG as requiring a second, independent
signal class before you say it out loud to a client as "this belongs to them."
3. Output Format
Every candidate carries the standard finding schema (see companion skill §3) plus the org-attribution evidence block:
Finding:
id: <stable hash>
module: org-attack-surface
asset_key: <typed key — e.g. org:acme-corp, related:acmesub.com, net:203.0.113.0/24, asn:64500>
category: ORG_FOOTPRINT | RELATED_DOMAIN | DARK_NETBLOCK | PROMOTE_QUEUE
severity: info # attribution work is discovery, not a vuln — see §6.6
confidence: <tentative|firm|confirmed> # Axis 1 — did I read the record right
title: <one-line summary>
description: <what was found + why it's plausibly the org's>
attribution:
owner_score: <0-100> # Axis 2 — combine() output
owner_tier: <none|weak|moderate|strong|confirmed>
discover_only: true # structural gate; independent of owner_score
signals:
- name: <signal name, e.g. cert_org_match>
weight: <0.0-1.0>
confirming: <bool>
detail: <human-readable evidence, e.g. "crt.sh O= match: Acme Corp">
source: <module/connector>
evidence:
url: <where found>
timestamp: <UTC ISO8601>
raw: <truncated to 2 KiB>
references: [<registry URL, RFC, etc.>]
remediation: <"confirm ownership before promoting to active scan" — always the remediation here>
UTC timestamps everywhere. Never collapse attribution.signals to a bare count — the reviewing
operator (or an auditor asking "why did you attribute this to them") needs to see exactly which
evidence fired, not just how much.
4. Source Hygiene & Citations
Same discipline as the companion skills: URL + UTC timestamp + tool/API version + run_id on every artifact. For registry lookups specifically:
- Record the exact query string sent to GLEIF/EDGAR/ARIN/RIPE — registry search results are not reproducible from a vague "I searched for the company" note months later.
- Cache raw registry JSON responses (GLEIF, ARIN, RIPE, EDGAR) — these APIs change/deprecate fields and a re-run six months later may not reproduce the same shape.
- GLEIF LEI records and EDGAR filings are durable references (an LEI or CIK doesn't expire); prefer citing those over an ephemeral index-search hit.
5. Do NOT
- Do NOT auto-scan a
related:domain, adiscover_onlynetblock/ASN, or adiscover_onlyIP. Ever. That is the entire structural contract of this skill (§2, §6.6). - Do NOT re-resolve a subsidiary's name against GLEIF/EDGAR/OpenCorporates once you already hold its exact LEI or CIK — name search is not unique across jurisdictions and will graft a namesake's subtree onto the wrong parent (§7.1, §11.1).
- Do NOT treat a shared nameserver, shared MX, or a brand string in an ASN org field as ownership.
Each is
WEAK-tier at best and needs corroboration (§11.1). - Do NOT attribute an entire hyperscaler-announced ASN range (AWS/GCP/Azure/Cloudflare/Akamai) to a tenant because one of the tenant's IPs lives in it (§9.5 — the hyperscaler-scope guard).
- Do NOT pivot on a privacy-masked WHOIS registrant org or a registrar's own name/email — the pivotability guards in §8.1/§8.3 exist because these produce thousands of unrelated false positives.
- Do NOT treat a fuzzy label match (Wikidata
rdfs:label, an ASN holder substring match) as equivalent evidence to a filed registry record (a GLEIF LEI relationship, an EDGAR Exhibit-21 entry, an exact RIR org-handle match). Weight them accordingly (§8.4's signal table). - Do NOT paste real registrant PII, private beneficial-ownership data, or engagement-client identity details into cloud LLMs beyond what's needed for the recon task itself.
6. The Org-First Attribution Pyramid
The mental model for everything in this skill. Most recon starts bottom-up from a seed domain and walks outward (subdomains, then IPs, then maybe an ASN). That misses everything that has no DNS trail back to the seed. This skill also walks top-down, starting from the legal entity itself:
┌─────────────────────────┐
│ LEGAL ENTITY (name) │ §7 — the anchor: WHOIS registrant_org,
│ → LEI / registration │ the seed's own TLS cert O=, or a
└────────────┬─────────────┘ seed-owned ASN holder string
│
┌────────────▼─────────────┐
│ CORPORATE FAMILY │ §7.1-7.4 — GLEIF direct-child tree,
│ (subsidiaries, M&A) │ EDGAR Exhibit-21, OpenCorporates,
└────────────┬─────────────┘ Wikidata P355/P749/P1830
│
┌──────────────────┴──────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ OWNED DOMAINS │ │ OWNED NETBLOCKS / ASN │
│ (seed + related:) │ │ (seed-owned + dark) │
└───────────┬─────────────┘ └─────────────┬───────────────┘
│ §8 — reverse-WHOIS, crt.sh O=, │ §9 — org-first RIR (ARIN/RIPE),
│ infra correlation, independent- bottom-up ASN (RIPEstat/BGPView),
│ evidence combiner hyperscaler-scope guard
└──────────────────┬──────────────────────┘
▼
┌─────────────────────────┐
│ LIVE ASSETS │ hand off to `offensive-osint` once
│ (hosts, services, apps) │ ownership is operator-confirmed
└─────────────────────────┘
6.1 Why top-down closes gaps bottom-up misses
A DNS/CT-only sweep of acme.com will find mail.acme.com, api.acme.com, subsidiaries that
happen to share the parent's nameservers — and nothing else. It will never find:
- A subsidiary acquired in an M&A deal that still runs on its own pre-acquisition domain, nameservers,
and netblocks, with zero DNS/cert linkage to
acme.com. - A netblock registered directly to "Acme Corporation" at ARIN or RIPE that has never had a single A record point into it — a forgotten datacenter allocation, or IP space used only for raw non-DNS-fronted services.
- A brand's cloud footprint that shows up in Shodan/Censys under
org:"Acme Corp"with no DNS name at all — a bare IP serving a banner with the org string baked into a TLS cert or ASN registration.
Each of these requires starting from the org identity itself and querying registries/indexes by who owns it, not what resolves from the seed. That is what §7–§9 do.
6.2 Discovery direction cheat sheet
| Direction | Seeded from | Finds | Section |
|---|---|---|---|
| Top-down: entity → subsidiaries | Legal name / LEI | Corporate family tree | §7 |
| Top-down: entity → domains | Registrant org / cert O= string | Related root domains | §8 |
| Top-down: entity → netblocks/ASN | Org identity string | Dark netblocks, registered ASNs | §9.1–§9.4 |
| Top-down: entity → live IPs | Org identity string | No-DNS live hosts (index search) | §9.6 |
| Bottom-up: seed IP → ASN | Resolved seed IPs | Announced prefixes, adjacent infra | §9.3 |
Both directions run in the same pipeline and reconcile through the same ownership-scoring axis (§2, §8.4) — a bottom-up-discovered netblock and a top-down-discovered netblock get scored the same way and land in the same graph.
7. Corporate Identity Resolution
7.1 GLEIF LEI API — the corporate-family tree
The Global Legal Entity Identifier Foundation publishes, keyless, the filed parent/child relationships between legal entities under ISO 17442 LEIs. This is the highest-precision source in this entire skill because relationships are filed, not inferred.
Two-hop query pattern:
Resolve a legal name → LEI (only ever done for the seed — see why below):
LEI=$(curl -sk "https://api.gleif.org/api/v1/lei-records?filter[entity.legalName]=Acme%20Corporation&page[size]=1" \ -H "Accept: application/vnd.api+json" | jq -r '.data[0].attributes.lei') echo "$LEI" # capture it — the next step reuses $LEI$r = Invoke-RestMethod -Uri "https://api.gleif.org/api/v1/lei-records?filter[entity.legalName]=Acme%20Corporation&page[size]=1" ` -Headers @{ Accept = "application/vnd.api+json" } $lei = $r.data[0].attributes.leiFetch that LEI's direct children (one level down, filed relationships only):
curl -sk "https://api.gleif.org/api/v1/lei-records/$LEI/direct-children?page[size]=100" \ -H "Accept: application/vnd.api+json" | \ jq -r '.data[].attributes | .lei + " " + .entity.legalName.name'$children = Invoke-RestMethod -Uri "https://api.gleif.org/api/v1/lei-records/$lei/direct-children?page[size]=100" ` -Headers @{ Accept = "application/vnd.api+json" } $children.data | ForEach-Object { "$($_.attributes.lei) $($_.attributes.entity.legalName.name)" }
Recurse for a multi-level tree — but with two non-negotiable rules:
Downward-only. Only ever ask for a LEI's direct children — never ascend to the ultimate parent and fan into its siblings. Ascending turns "map Acme's subsidiaries" into "map every company under Acme's ultimate holding conglomerate," which is a different (much larger, mostly irrelevant) question the operator did not ask.
Exact-LEI expansion for every descendant, name resolution ONLY for the seed. Once you have a child's own filed LEI from step 2's response, expand that entity's children via
/lei-records/<child-LEI>/direct-childrendirectly — never by searching its name again.Why this matters:
filter[entity.legalName]=is a name search, and legal names are not unique across jurisdictions. If entity B (a direct child of your seed) shares a name with an unrelated entity B' in another country, re-resolving "B" by name can silently return B' — and you then graft B''s entire subsidiary subtree onto your target's corporate family, under an authoritative-looking GLEIF badge. Carrying the exact LEI through the recursion makes this collision structurally impossible.
Depth and fan-out caps (recommended, mirrors the production implementation):
| Cap | Suggested value | Why |
|---|---|---|
| Max recursion depth | operator-set; default OFF, 1 level for a "deep" pass | Depth grows the tree exponentially with fan-out; an unbounded walk on a conglomerate never terminates in a useful time. |
| Max nodes minted total | ~200 | Global budget; a capped tree must be flagged as truncated, never silently rendered as complete. |
| Max direct children expanded per parent | ~50 | A parent with hundreds of filed subsidiaries (a large holding company) would otherwise dominate the whole budget. |
When either cap is hit, flag the affected parent node (children_truncated) rather than silently
dropping the rest of its children — an operator reading a "complete-looking" corporate tree that
was actually cut off is worse than one that visibly says "and N more, not expanded."
Cycle safety: track visited LEIs; a diamond ownership structure (A owns B and C, both B and C own D) should mint D once, under whichever parent reaches it first — not twice, and not infinitely.
7.2 SEC-EDGAR — Exhibit-21 subsidiary lists
US-listed companies' 10-K filings include Exhibit 21, a list of subsidiary entity names. EDGAR's full-text search surfaces filings; you extract entity names, not domains — EDGAR asserts "this subsidiary exists," not "this subsidiary owns this URL."
curl -sk 'https://efts.sec.gov/LATEST/search-index?q=%22Acme%20Corporation%22&forms=10-K' \
-H "User-Agent: YourOrg-Recon research@yourdomain.example" | \
jq -r '.hits.hits[]._source.display_names[]'
$headers = @{ "User-Agent" = "YourOrg-Recon research@yourdomain.example" }
$r = Invoke-RestMethod -Uri 'https://efts.sec.gov/LATEST/search-index?q=%22Acme%20Corporation%22&forms=10-K' -Headers $headers
$r.hits.hits | ForEach-Object { $_._source.display_names }
Caveats:
- Response shape is documented, not guaranteed — EFTS full-text search returns filing-level
metadata under
_source(display_names,ciks,form, …), sodisplay_namesare the filer / co-filer entity names on matching 10-K filings — corroboration that the entity files with the SEC, not a ready-made subsidiary roster. True subsidiary lists live inside the filing's Exhibit 21 document, which you fetch separately from the filing (not from the search index). Verify the field against a live response before trusting a parse. - SEC requires a descriptive User-Agent identifying you (name + contact) on every request — a
bare
curlUA gets throttled/blocked. This is SEC's own fair-access policy, not a tooling quirk. - US-SEC-filer-only. Empty for any org that doesn't file 10-Ks (non-US, private, or a subsidiary itself rather than the filer). Treat a null result as "not applicable," not "no subsidiaries."
- Entity names from EDGAR feed the depth-1 entity→domain pivot (§8.5) — you take each returned name and pivot it through crt.sh O=, capped, to find candidate domains for that specific subsidiary.
7.3 OpenCorporates — entity corroboration (not domain discovery)
OpenCorporates confirms "is this a real registered company, and in what jurisdiction" — it does not supply domains at all. Use it to corroborate a candidate subsidiary name found elsewhere (GLEIF, EDGAR, a press release) before spending a crt.sh pivot on it.
curl -sk "https://api.opencorporates.com/v0.4/companies/search?q=Acme+Corporation" | \
jq -r '.results.companies[].company | "\(.jurisdiction_code) \(.name)"'
$r = Invoke-RestMethod -Uri "https://api.opencorporates.com/v0.4/companies/search?q=Acme+Corporation"
$r.results.companies | ForEach-Object { "$($_.company.jurisdiction_code) $($_.company.name)" }
Caveat: the free/keyless tier is aggressively rate-limited (single-digit requests/day in practice) — treat it as a spot-check corroboration source, not a bulk enumeration one. A paid API token raises the quota but is not required for the core workflow.
7.4 Wikidata SPARQL — the public corporate graph
Wikidata models corporate relationships as structured properties. Four are relevant:
| Property | Meaning |
|---|---|
P856 |
official website |
P355 |
subsidiary (parent → child) |
P749 |
parent organization (child → parent) |
P1830 |
owner of / owned by (bidirectional) |
Query: find the seed entity (by official-website URL match or by label), traverse
P355/P749/P1830 in both directions to related entities, then read their P856 website —
reduced to a registrable root domain.
curl -sk -G "https://query.wikidata.org/sparql" \
-H "Accept: application/sparql-results+json" \
-H "User-Agent: YourOrg-Recon research@yourdomain.example" \
--data-urlencode 'query=SELECT DISTINCT ?website WHERE {
{ ?mainEntity wdt:P856 ?siteUrl . FILTER(CONTAINS(STR(?siteUrl), "acme.com")) }
UNION { ?mainEntity rdfs:label "Acme Corporation"@en . }
{ ?mainEntity wdt:P355 ?related . } UNION { ?mainEntity wdt:P749 ?related . }
UNION { ?mainEntity wdt:P1830 ?related . } UNION { ?related wdt:P749 ?mainEntity . }
UNION { ?related wdt:P1830 ?mainEntity . }
?related wdt:P856 ?website .
} LIMIT 50' | jq -r '.results.bindings[].website.value'
$query = @'
SELECT DISTINCT ?website WHERE {
{ ?mainEntity wdt:P856 ?siteUrl . FILTER(CONTAINS(STR(?siteUrl), "acme.com")) }
UNION { ?mainEntity rdfs:label "Acme Corporation"@en . }
{ ?mainEntity wdt:P355 ?related . } UNION { ?mainEntity wdt:P749 ?related . }
UNION { ?mainEntity wdt:P1830 ?related . } UNION { ?related wdt:P749 ?mainEntity . }
UNION { ?related wdt:P1830 ?mainEntity . }
?related wdt:P856 ?website .
} LIMIT 50
'@
$r = Invoke-RestMethod -Uri "https://query.wikidata.org/sparql" -Body @{ format = "json"; query = $query } `
-Headers @{ Accept = "application/sparql-results+json" }
$r.results.bindings | ForEach-Object { $_.website.value }
Caveat — score this WEAKER than a filed registry record. A Wikidata label match is a best-effort
fuzzy match (rdfs:label "X"@en), not a filed legal assertion. Two unrelated companies sharing a
common name will both match the label filter. In the signal weighting (§8.4), a Wikidata hit is
explicitly non-confirming and weighted below a direct registry-listed match — it corroborates, it
does not confirm alone.
7.5 Minting the seed org identity node
Before any of the above is useful, anchor a seed org identity — the node every subsidiary and every scored candidate attaches to. Priority order:
- WHOIS
registrant_org— but only when it is not privacy-masked (§8.1'sis_pivotable_orgguard) and not a registrar's own name ("Domains By Proxy," "WhoisGuard," etc.). A privacy-masked WHOIS record is not the target's identity — it's the privacy service's — and minting the seed org from it would confirm-own the wrong entity for the rest of the walk. - A seed-owned ASN holder string — from bottom-up ASN discovery (§9.3): if one of the seed's own resolved IPs sits in an ASN whose registrant name matches a target brand, that holder string is a valid identity source (RIR data, not WHOIS privacy, so it survives even when WHOIS itself is masked).
- The seed's own leaf TLS certificate
O=field — one GET to the seed's own domain (never a third party), reading the certificate subject organization. Often the most reliable source in practice: WHOIS is frequently redacted, but a company's own production TLS cert commonly still carries its legal name inO=.
Do not mint a seed org identity from a bare brand stem alone (e.g. just "acme" from
acme.com with no other corroboration) — that is exactly the kind of low-precision namesake risk
this whole skill exists to avoid. If none of the three sources above produce a usable identity
string, degrade gracefully: report "no confirmable legal identity found; corporate-family and
netblock discovery skipped" rather than guessing from the domain name.
Once anchored, keep every alias (WHOIS org string, ASN holder string, cert O= string) attached to the same node — later stages (§9.1's RIR query, §9.6's index search) query by every alias, not just the primary name, since a company frequently registers infrastructure under a legal-name variant that differs from its WHOIS string.
8. Domain Attribution
8.1 reverse-WHOIS — paid, quota-guarded
Reverse-WHOIS ("show me every domain registered by X") is the most direct domain-attribution technique and the one part of this skill with no keyless path — both practical providers (WhoisXML, SecurityTrails) require a paid key. Treat it as enrichment on top of the keyless core (§8.2–§8.4), not a hard dependency.
Pivotability guard — never pivot on generic or privacy-masked terms:
- Reject registrant org strings under 4 characters, containing a privacy-service token ("privacy," "redacted," "domains by proxy," "withheld for privacy," …) or a registrar's own name (GoDaddy, Namecheap, Tucows, MarkMonitor, …), or with no distinctive token once generic corporate suffixes (Ltd/LLC/Inc/GmbH/Pvt/Holdings/Group/…) are stripped.
- Reject registrant email pivots on role addresses (
abuse@,admin@,hostmaster@,noreply@, …), privacy-provider domains, and free-mail domains (Gmail, Outlook, etc.).
Pivoting on a rejected term is how reverse-WHOIS turns into a false-positive generator — "privacy protection LLC" as a registrant org matches tens of thousands of unrelated domains.
Quota-protection pattern (WhoisXML two-call preview-then-purchase):
# 1. PREVIEW — free, returns a count, does not consume a purchase credit
curl -sk -X POST "https://reverse-whois.whoisxmlapi.com/api/v2" \
-H "Content-Type: application/json" \
-d '{"apiKey":"'"$WHOISXML_KEY"'","searchType":"current","mode":"preview",
"basicSearchTerms":{"include":["Acme Corporation"]}}' | jq '.domainsCount'
# 2. Only if 1 <= count < 200, PURCHASE — the real result
curl -sk -X POST "https://reverse-whois.whoisxmlapi.com/api/v2" \
-H "Content-Type: application/json" \
-d '{"apiKey":"'"$WHOISXML_KEY"'","searchType":"current","mode":"purchase",
"basicSearchTerms":{"include":["Acme Corporation"]}}' | jq -r '.domainsList[]'
$body = @{ apiKey = $env:WHOISXML_KEY; searchType = "current"; mode = "preview";
basicSearchTerms = @{ include = @("Acme Corporation") } } | ConvertTo-Json
$preview = Invoke-RestMethod -Uri "https://reverse-whois.whoisxmlapi.com/api/v2" -Method Post `
-ContentType "application/json" -Body $body
if ($preview.domainsCount -ge 1 -and $preview.domainsCount -lt 200) {
$body2 = @{ apiKey = $env:WHOISXML_KEY; searchType = "current"; mode = "purchase";
basicSearchTerms = @{ include = @("Acme Corporation") } } | ConvertTo-Json
$result = Invoke-RestMethod -Uri "https://reverse-whois.whoisxmlapi.com/api/v2" -Method Post `
-ContentType "application/json" -Body $body2
$result.domainsList
}
Skip the purchase call entirely when the count is 0 (nothing to buy) or ≥ 200 (a hit that broad is a generic-term false positive, not a real reverse-WHOIS signal — don't burn the credit).
SecurityTrails' associated-domains endpoint takes a domain (not an org/email string) and returns domains SecurityTrails has already associated with it — cheaper conceptually but still key-gated:
curl -sk "https://api.securitytrails.com/v1/domain/acme.com/associated" -H "APIKEY: $ST_KEY" | \
jq -r '.records[].hostname'
8.2 crt.sh O= — the keyless CT-organization pivot
Certificate Transparency logs are public and searchable by certificate subject organization. This is the keyless backbone of domain attribution in this skill — it runs even with zero paid keys.
curl -sk "https://crt.sh/?O=Acme+Corporation&output=json" -H "User-Agent: Mozilla/5.0" | \
jq -r '.[].name_value' | tr '\n' ' ' | tr ' ' '\n' | sort -u | grep -v '^\*'
$r = Invoke-RestMethod -Uri "https://crt.sh/?O=Acme+Corporation&output=json" -Headers @{ "User-Agent" = "Mozilla/5.0" }
$r | Select-Object -ExpandProperty name_value | ForEach-Object { $_ -split "`n" } | Sort-Object -Unique
Parsing discipline:
- Split multi-value SAN entries on newlines; strip wildcard prefixes (
*.); lowercase. - Validate each result actually looks like an FQDN before reducing to a registrable root — CT log garbage (malformed subject strings) is common enough to need a sanity filter.
- Drop the seed's own registrable root and any subdomain of it — you want cross-root candidates, not the seed re-discovering itself.
- Deduplicate by registrable root, not by raw hostname (many subdomains of the same unrelated root are one candidate, not many).
Fallback chain when crt.sh 502s: Censys Certificates, CertSpotter, Rapid7 Open Data — see
offensive-osint §27.0.1 for the full retry/fallback recipe; this skill's org-search variant of
crt.sh (?O= instead of the usual ?q=) hits the same backend and is subject to the same outages.
8.3 Infrastructure correlation — cheap, DNS-only signals
Once you have a candidate root domain (from reverse-WHOIS, crt.sh O=, or a registry pivot), enrich it with keyless DNS lookups and compare against the seed's own DNS footprint:
| Signal | What it checks | Guard |
|---|---|---|
| Shared specific NS | Candidate's NS intersects the seed's NS | Exclude generic providers (Cloudflare, Route53, Azure DNS, Google Domains, …) — their NS records mean nothing about ownership. |
| Shared custom MX | Candidate's MX intersects the seed's MX | Exclude public mail providers (Google Workspace, M365, generic ESPs). |
| Shared SaaS TXT token | Both domains carry the same SaaS-verification TXT prefix (Google Site Verification, Salesforce, etc.) | Token match only — the token value usually differs per domain even for the same SaaS tenant, so match on the verification-service prefix. |
| Netblock membership | A resolved IP for the candidate falls inside a netblock already confirmed seed-owned | Root-vs-CIDR containment check, not a substring match. |
| Reverse-DNS (PTR) root match | The PTR record for a candidate's resolved IP has the SAME registrable root as the seed | Root-vs-root only — a PTR ending in evil-acme.com.attacker.com must not match on a naive endswith check; compare registrable roots exactly. |
Each signal that fires becomes one OwnerSignal fed into the combiner (§8.4) — never a standalone
verdict.
8.4 The independent-evidence combiner
The core scoring math behind every ownership tier in this skill (§2). Every fired signal is a
(weight, confirming: bool) pair. Positives combine via:
score = 1 - Π(1 - w_i) for every non-negative signal i
...scaled to 0–100, unless a confirming signal is present, in which case the score is forced
to 100 directly (only a handful of signal types are ever marked confirming — chiefly "this IS the
seed domain itself"). If any NOT_OWNED (stranger-lock) signal is present, the score is capped at
20 regardless of what the product formula would otherwise produce.
Why multiplicative-independent, not additive: three weak, genuinely independent signals should
combine into something stronger than any one of them, but should not simply sum past 100 the way a
naive additive score would. 1 - Π(1-w_i) has the right shape — diminishing returns on stacking
correlated-looking weak signals, while still letting three truly independent ~0.5-weight signals
climb into STRONG territory (three 0.5 signals: 1 - 0.5³ = 0.875 → 87.5, STRONG).
Signal weight table (production values — reuse these, don't invent new priors per engagement):
| Signal | Weight | Confirming | Notes |
|---|---|---|---|
registry_listed |
0.95 | yes | A regulatory/government registry explicitly lists this domain as belonging to the org. |
reverse_whois_email |
0.88 | no | WHOIS registrant email matches a pivotable corporate address. |
cert_org_match |
0.85 | no | TLS certificate O= matches the seed org directly (crt.sh O= hit on the seed's own identity string). |
reverse_whois_org |
0.80 | no | WHOIS registrant org matches a pivotable org string. |
cert_san_cross_root |
0.80 | no | Candidate and seed share a SAN on a common certificate. |
asn_holder_match |
0.75 | no | Candidate's resolved IP falls in a netblock whose ASN holder matches the org. |
securitytrails_associated |
0.70 | no | SecurityTrails association, not an exact WHOIS-org match. |
netblock_reverse_dns |
0.70 | no | PTR root match against a seed-owned netblock. |
entity_cert_org_match |
0.68 | no | Cert-O= match found by pivoting a subsidiary entity name — deliberately weaker than a direct seed-org cert match (§8.5). |
registry_wikidata |
0.60 | no | Fuzzy Wikidata label match — deliberately weaker than a filed registry record (§7.4). |
shared_saas_txt |
0.55 | no | Shared SaaS TXT verification token. |
shared_mx_custom |
0.50 | no | Shared non-public MX host. |
shared_ns_specific |
0.35 | no | Shared organisation-specific nameserver. |
| (structural: the seed domain itself) | 1.0 | yes | Forces the score to 100 — this is the one confirming signal in normal operation. |
A single-signal candidate almost never reaches STRONG on its own except registry_listed and
reverse_whois_email — by design. Everything else needs a second independent signal class to clear
70. This is the mechanical enforcement of "rule of three" for this domain.
8.5 Entity → domain pivot (depth-1, from corporate registries)
EDGAR (§7.2), OpenCorporates (§7.3), and Wikidata (§7.4) give you subsidiary entity names, not domains. Pivot each pivotable entity name through crt.sh O= (§8.2) to find its candidate domains, capped (recommended ≤ 20 entity names per scan — a multinational with hundreds of listed subsidiaries would otherwise blow the crt.sh call budget), and mint each entity as its own subsidiary identity node parented to the seed org.
Score the resulting domain match with entity_cert_org_match (0.68), not cert_org_match
(0.85). The extra hop through a registry-pivoted entity name is a weaker link than a direct hit on
the seed's own identity string — a registry-confirmed entity name should not transfer its own
confidence onto whatever crt.sh happens to fuzzy-match for that name. Keep the provenance edge from
the discovered domain back to the specific subsidiary entity that yielded it, so an operator can
trace "why do you think this belongs to them" all the way back to the filed registry record.
8.6 The discover-only related: namespace
Every candidate domain this skill surfaces — regardless of ownership score — is minted under a
separate asset namespace from confirmed in-scope domains (related:acmesub.com, never
domain: or sub:). This is a structural, not a scoring, distinction:
- No module that consumes
domain:/sub:assets to seed active scanning will ever see arelated:asset — they are invisible to the active pipeline by construction, not by a runtime check that could be bypassed by a high score. - Promotion to an actual scan target is a distinct, explicit operator action — never automatic, no matter how many independent signals fired or how high the owner_score climbed.
- This is what makes the whole top-down half of this skill safe to run passively and by default: discovery breadth doesn't translate into scan breadth without a human in the loop.
9. Netblock & ASN Attribution
9.1 Org-first RIR queries — recovering "dark" netblocks
DNS-based discovery only ever finds netblocks a resolved hostname happens to point into. IP space registered to the org's legal entity with zero DNS pointing at it is invisible to every technique in §8. This is the single highest-value gap this skill closes, and it requires querying the regional internet registries by organization name, not by IP.
ARIN (Americas) — Whois-RWS, keyless JSON:
# 1. Find org handles matching a name (wildcard suffix)
curl -sk "https://whois.arin.net/rest/orgs;name=Acme*" -H "Accept: application/json" | \
jq -r '.orgs.orgRef[] | "\(."@handle") \(."@name")"'
# 2. For each matched handle, list its registered nets
curl -sk "https://whois.arin.net/rest/org/ACME-1/nets" -H "Accept: application/json" | \
jq -r '.nets.netRef[] | "\(."@startAddress") - \(."@endAddress")"'
# 3. And its registered ASNs
curl -sk "https://whois.arin.net/rest/org/ACME-1/asns" -H "Accept: application/json" | \
jq -r '.asns.asnRef[]."@handle"'
$orgs = Invoke-RestMethod -Uri "https://whois.arin.net/rest/orgs;name=Acme*" -Headers @{ Accept = "application/json" }
$orgs.orgs.orgRef | ForEach-Object { "$($_.'@handle') $($_.'@name')" }
$nets = Invoke-RestMethod -Uri "https://whois.arin.net/rest/org/ACME-1/nets" -Headers @{ Accept = "application/json" }
$nets.nets.netRef | ForEach-Object { "$($_.'@startAddress') - $($_.'@endAddress')" }
$asns = Invoke-RestMethod -Uri "https://whois.arin.net/rest/org/ACME-1/asns" -Headers @{ Accept = "application/json" }
$asns.asns.asnRef | ForEach-Object { $_.'@handle' }
ARIN returns address ranges (startAddress/endAddress), not CIDRs — convert with a
minimal-CIDR-set summarization (Python's ipaddress.summarize_address_range, or equivalent) rather
than assuming a clean CIDR boundary.
RIPE (EMEA) — RIPE Database REST API, keyless JSON:
# 1. Find organisation objects matching a name
curl -sk "https://rest.db.ripe.net/search.json?query-string=Acme%20Corporation&type-filter=organisation&flags=no-referenced" | \
jq -r '.objects.object[] | select(.type=="organisation") | .["primary-key"].attribute[] | select(.name=="organisation") | .value'
# 2. Inverse-lookup everything registered TO that org handle (inetnum/inet6num/aut-num)
curl -sk "https://rest.db.ripe.net/search.json?query-string=ORG-AC1-RIPE&inverse-attribute=org&type-filter=inetnum&type-filter=inet6num&type-filter=aut-num&flags=no-referenced" | \
jq -r '.objects.object[] | "\(.type) " + (.attributes.attribute[] | select(.name=="inetnum" or .name=="inet6num" or .name=="aut-num") | .value)'
$orgs = Invoke-RestMethod -Uri "https://rest.db.ripe.net/search.json?query-string=Acme%20Corporation&type-filter=organisation&flags=no-referenced"
# extract ORG-* handle(s) from $orgs.objects.object, then:
$inv = Invoke-RestMethod -Uri "https://rest.db.ripe.net/search.json?query-string=ORG-AC1-RIPE&inverse-attribute=org&type-filter=inetnum&type-filter=inet6num&type-filter=aut-num&flags=no-referenced"
Precision gate — non-negotiable: accept a matched RIR org only when (a) its registered name passes the same pivotability guard as §8.1 (no privacy/registrar/generic-only strings), and (b) its normalized slug exactly equals a known org-identity slug already on your identity list (§7.5's seed org name/aliases, or a seed-owned ASN holder string) — an exact normalized match, not a loose substring test. Org names collide across unrelated companies constantly ("Acme Logistics" vs. "Acme Corporation" vs. "Acme Holdings Ltd" in a different country) — substring matching here is how you attribute a stranger's netblock to your target.
Coverage caveat: ARIN + RIPE cover the Americas and EMEA. APNIC, LACNIC, and AFRINIC have no
reliable keyless free-text reverse-org search — report this explicitly as a coverage gap
(org-registered blocks in APAC/LATAM/Africa not enumerated this pass), not a silent zero.
Everything this produces is discover_only=True, INERT. Mint a REGISTERED_TO_ORG edge from
the netblock/ASN to the org node and a non-confirming NETBLOCK_MEMBER/ASN_MEMBER-typed owner
signal — never a seed_owned flag. A registry name collision must never silently drag a stranger's
IP space into an active scan queue.
9.2 Bound the fan-out
A broad org-name match can explode into hundreds of netblocks/ASNs per matched registry org handle.
Cap defensively: matched org handles per registry per query name (10), netblocks minted per matched
org (200), ASNs per matched org (~100). Log — don't silently drop — whatever the cap trims.
9.3 Bottom-up ASN discovery (compl
…(truncated)