# Org Attack Surface

> Org-grade attack-surface mapping: given a company's legal identity, discover its ENTIRE owned internet footprint — corporate family -> owned domains -> owned netblocks/ASN -> live assets — with attribution discipline, not just DNS breadth. The org-first attribution pyramid (legal entity -> LEI/registration -> corporate family -> owned domains -> owned netblocks/ASN -> live assets). Corporate-identity resolution via the GLEIF LEI API (legal name -> LEI, exact-LEI direct-children expansion, downward-only depth-capped BFS, NEVER a name re-resolution), SEC-EDGAR full-text search + Exhibit-21 subsidiary entity names, OpenCorporates entity corroboration, Wikidata SPARQL corporate graph (P856/P355/P749/P1830). Domain attribution via reverse-WHOIS (WhoisXML preview-then-purchase quota guard, SecurityTrails associated-domains — both paid), crt.sh O= certificate-transparency organization pivot (keyless), infrastructure correlation (shared NS/MX/SaaS-TXT, netblock membership, reverse-DNS PTR), and the independent-eviden

- Skill: `elementalsouls/org-attack-surface` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add elementalsouls/org-attack-surface`
- Raw SKILL.md: https://api.skillmd.com/api/skills/elementalsouls/org-attack-surface/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: elementalsouls (https://skillmd.com/u/elementalsouls)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/elementalsouls/org-attack-surface

---


# Org Attack Surface — Corporate-Family Footprint Mapping

> Companion skills: [`osint-methodology`](../osint-methodology/) (the "how to think" 5-stage recon
> pipeline this plugs into) and [`offensive-osint`](../offensive-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 does `acme.com` expose", 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, a `discover_only` netblock/ASN, or a `discover_only` IP.
  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:**

1. Resolve a legal name → LEI (only ever done for the **seed** — see why below):

   ```bash
   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
   ```

   ```powershell
   $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.lei
   ```

2. Fetch that LEI's **direct children** (one level down, filed relationships only):

   ```bash
   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'
   ```

   ```powershell
   $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:**

1. **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.
2. **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-children` directly — 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."*

```bash
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[]'
```

```powershell
$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`, …), so `display_names` are 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 `curl` UA 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.

```bash
curl -sk "https://api.opencorporates.com/v0.4/companies/search?q=Acme+Corporation" | \
  jq -r '.results.companies[].company | "\(.jurisdiction_code)  \(.name)"'
```

```powershell
$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.

```bash
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'
```

```powershell
$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:

1. **WHOIS `registrant_org`** — but only when it is not privacy-masked (§8.1's `is_pivotable_org`
   guard) 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.
2. **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).
3. **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 in `O=`.

**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):**

```bash
# 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[]'
```

```powershell
$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:

```bash
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.

```bash
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 '^\*'
```

```powershell
$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 a `related:`
  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:**

```bash
# 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"'
```

```powershell
$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:**

```bash
# 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)'
```

```powershell
$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)
