# Subdomain Finder via CT Logs: DNS, Takeover Hints & Alerts (`foresighted_sunset/subdomain-finder-ct-logs`) Actor

Passive CT-log subdomain discovery with source-linked certificate dates, optional DNS and dangling-CNAME hints, and only-new monitoring. crt.sh by default; Cert Spotter requires explicit opt-in. Transparent per-host billing and strict run-budget guards.

- **URL**: https://apify.com/foresighted_sunset/subdomain-finder-ct-logs.md
- **Developed by:** [Dano Desk](https://apify.com/foresighted_sunset) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 subdomains

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Subdomain Finder via CT Logs: DNS, Takeover Hints & Alerts

Find subdomains observed in public TLS certificates, with dates and source evidence—not a claim of exhaustive DNS coverage. The data comes from **Certificate Transparency (CT) logs**, the public, append-only record of issued certificates. It's passive: the Actor never scans or brute-forces the target. It reads public CT search services and, if you ask, makes ordinary DNS lookups.

**Why this one**

- **It doesn't die when crt.sh does.** crt.sh is free and often overloaded (HTTP 502 or timeouts), and that's why single-source CT Actors fail so often. This Actor asks crt.sh for the small deduplicated result first. If that fails, it tries crt.sh's plain query once. SSLMate **Cert Spotter is opt-in, disabled by default**, and is never queried merely because an API key exists. Every domain gets a clear status and error code.
- **Clean results.** You get one row per host, with wildcard names merged or kept separate as you choose. Precertificates and their final certificates are counted once. Names from other domains on the same cert, plus emails and junk, are dropped.
- **Useful fields.** `firstSeen` / `lastSeen` (certificate notBefore), `latestExpiry`, `activeCert`, `certCount`, `latestIssuer`, `issuers`, `sources` and a crt.sh link.
- **Optional DNS.** A / AAAA / CNAME, plus `dnsStatus` (RESOLVED, CNAME_NO_TARGET, NO_RECORDS, NXDOMAIN, TIMEOUT). There's also a **dangling-CNAME hint** that names the service (GitHub Pages, S3, Azure, Heroku, Netlify…) and a DNS-wildcard check per domain.
- **Monitor mode.** It remembers which hosts it has already seen in a named key-value store. Schedule it daily and you get only **new subdomains**.
- Bulk: up to 100 domains per run. No API key needed.

### Quick start

- **Demo:** run with `{}` for three labelled archived real CT rows from 2026-10-29. No current DNS assertion, external requests or custom event charges. Supply `domains` for a live query.
- **Bulk:**

```json
{ "domains": ["example.com", "https://www.example.org/page", "ops@example.net"] }
```

- **Live hosts only, full history:**

```json
{ "domains": ["example.com"], "includeExpired": true, "onlyResolving": true }
```

- **Daily new-subdomain alert** (schedule it, then connect a webhook, Slack or email integration to the dataset):

```json
{ "domains": ["example.com", "example.org"], "monitor": true, "monitorKey": "my-brands", "resolveDns": true }
```

### Input

| Field | Default | Notes |
|---|---|---|
| `domains` | demo | Root domains, max 100. URLs, emails, `*.x` and `www.` are cleaned. Invalid entries get an `INVALID_DOMAIN` error row and don't stop the run. |
| `sources` | `crtsh` | `auto` can fall back only when `enableCertSpotter=true`. `both`/`certspotter` require that same explicit terms opt-in. |
| `enableCertSpotter` | false | Separate consent even if a key is provided. Review SSLMate plan/use terms; a key alone does not establish permitted use. |
| `includeExpired` | false | Also returns hosts that appear only in expired certificates. Cert Spotter has unexpired certificates only. |
| `wildcardMode` | `merge` | `merge`: `*.dev.x.com` becomes `dev.x.com` with `hasWildcardCert=true`. `separate`: it gets its own row. `drop`: it's ignored. |
| `includeRootDomain` | true | Includes a row for the domain itself. |
| `maxSubdomainsPerDomain` | 5000 | Keeps the most recently seen hosts and logs a warning. |
| `resolveDns` / `onlyResolving` | off | DNS lookups only, never ports or HTTP. |
| `takeoverHints` | on | A dangling CNAME is a **hint to check by hand**, not a confirmed vulnerability. |
| `dnsServers`, `dnsConcurrency` | platform, 10 | |
| `monitor`, `onlyNew`, `monitorKey`, `stateStoreName` | off, on, `default`, `subdomain-finder-ct-state` | One memory record per (monitorKey, domain). The first run is the baseline: everything counts as new. |
| `certspotterApiKey` | none | Optional, secret. Raises Cert Spotter limits. |
| `certspotterMaxPages` | 5 (50 with key) | One request per page. |
| `crtshTimeoutSecs`, `maxResponseMb` | 60, 40 | Answers over the size cap fail clearly; opt-in auto mode can try Cert Spotter. For very large domains, raise these together with run memory. |

### Output

The dataset has one row per host (`recordType: "subdomain"`) and one row per failed or invalid domain (`recordType: "error"`, `errorCode`, `message`). There are four views: Subdomains, DNS & takeover hints, Monitor, and Errors.

Example row (from a real run, shortened):

```json
{ "domain": "apify.com", "subdomain": "mail.apify.com", "level": 1, "firstSeen": "2026-01-26T00:00:00.000Z", "lastSeen": "2026-01-26T00:00:00.000Z",
  "latestExpiry": "2027-02-24T23:59:59.000Z", "activeCert": true, "certCount": 1, "latestIssuer": "Amazon Trust Services",
  "sources": ["certspotter"], "dnsStatus": "RESOLVED", "resolves": true, "a": ["…"], "cname": null, "takeoverHint": null, "crtshUrl": "https://crt.sh/?q=mail.apify.com" }
```

`firstSeen` and `lastSeen` are the earliest and latest certificate *notBefore* dates that name the host, so they reflect certificate dates, not DNS history. `firstDetectedAt` (monitor) is the time this Actor first saw the host.

The `OUTPUT` record in the key-value store has a per-domain status (`ok` / `partial` / `failed` / `invalid`), the status of each source (certificates, pages, notes), counts, warnings and request stats.

#### Error codes

- **Per domain:** `INVALID_DOMAIN`, `CRTSH_<code>` or `CERTSPOTTER_<code>` (when only that source was tried), `ALL_SOURCES_FAILED`, `SOME_SOURCES_FAILED` (status partial, results kept).
- **Source codes:** `TIMEOUT`, `UNAVAILABLE` (5xx), `RATE_LIMITED` (429), `TOO_LARGE`, `BAD_RESPONSE`, `NETWORK_ERROR`, `BAD_CREDENTIALS`, `FORBIDDEN`, `BAD_REQUEST`.
- **Run level:** `INVALID_INPUT` (nothing usable in the input) and `ALL_DOMAINS_FAILED`. The run fails only if *every* domain failed.

### Fair use of the sources

- **crt.sh** (operated by Sectigo) is a free community service. The Actor sends one request at a time, at least 3 s apart. It asks for the small deduplicated result, honours `Retry-After`, and has a limited retry budget (at most 3 crt.sh requests per domain) before reporting failure or trying the separately enabled Cert Spotter fallback.
- **Cert Spotter** (SSLMate) allows a limited number of unauthenticated queries per hour for personal or evaluation use. Requests are at least 1 s apart and `Retry-After` is honoured. For permitted regular/commercial use, select a suitable SSLMate plan and your own key; enabling this option does not grant a licence.
- DNS lookups go through normal resolvers. The Actor does no port scanning, HTTP probing or brute-forcing.

Use this for domains you own or are authorised to assess, and for brand monitoring and research. CT data is public by design.

### Limits and next steps

- Hosts that never had a public certificate (internal-only names, or names covered only by a wildcard cert) can't be found through CT.
- There's no HTTP probing (status or title) and no lookalike/typosquat permutations yet. Both are planned as opt-in modes.
- Cert Spotter without a key returns a few pages per domain. Very large domains need crt.sh, or a key.

### Pricing

**`subdomain`: $0.0007 per delivered host row.** 1,000 hosts cost **$0.70**; 50 hosts cost **$0.035**, before any standard start fee/platform usage. Empty results, errors, filtered/seen hosts and archived demo rows have no custom event fee.

The former `domain-scanned` fee is **retired/included**, not stacked on top: the leading per-host peer charges $0.0008/row, and adding an arbitrary domain fee would make the stated undercut misleading. Configure only the named `subdomain` event—not a second priced default dataset-item event. The automatic `apify-actor-start` event costs $0.00005 per started GB (minimum one), including empty runs. Worked result examples exclude this start fee and any applicable platform usage, storage, integrations or taxes. No default dataset-row event is priced.

Set Apify run option `maxTotalChargeUsd` plus a sensible `maxSubdomainsPerDomain`. Exact active prices and the remaining budget are checked before each row, so the SDK never has to overrun a depleted cap. Hosts not delivered due to a billing cap are removed from the new seen-state commit and remain eligible next run. Never run concurrent writers against one monitor key.

### Opt-in fallback example

```json
{"domains":["example.com"],"sources":"auto","enableCertSpotter":true,"certspotterMaxPages":2}
```

Use this only where SSLMate permits the intended use. Commercial users should supply their own suitable-plan secret key; do not treat the anonymous evaluation tier as a commercial backend.

### FAQ

**Is this a vulnerability scanner?** No. DNS dangling-CNAME indicators are manual-review hints, not confirmed takeovers. No port scan, HTTP probing or exploitation occurs.

**Does a certificate prove a hostname is live?** No. A certificate may be expired, historical or wildcard-only. Optional DNS status and wildcard-zone detection explain those limits.

**What happens when crt.sh is down?** The default reports a clear source error. Only explicit opt-in enables Cert Spotter; it is not an undeclared dependency.

**Are firstSeen/lastSeen discovery dates?** They are certificate notBefore dates. `firstDetectedAt` is this monitor's observation time, not proof of historic DNS use.

### Troubleshooting

- `INVALID_INPUT`: supply valid domains; `both`/`certspotter` require `enableCertSpotter=true`.
- `CRTSH_UNAVAILABLE`, `TIMEOUT`, `TOO_LARGE`: the service may be overloaded; retry later or reduce scope. The client paces requests and retries transient failures within a bounded budget, honoring Retry-After.
- `CERTSPOTTER_BAD_CREDENTIALS`/`RATE_LIMITED`: check your permitted plan/key and lower page caps; partial results stay labelled.
- `BILLING_CAP_REACHED`: increase the run cap or reduce expected rows. Unsent hosts are not silently marked delivered.
- No output: CT coverage and filters can legitimately yield zero hosts. The Actor never manufactures records to fill a result cap.

# Actor input Schema

## `domains` (type: `array`):

Root domains to search (max 100). Empty input returns 3 clearly labelled archived CT rows from 2026-10-29, without external requests or custom event fees. Set domains for live data.

## `sources` (type: `string`):

crtsh (default): crt.sh only. auto: crt.sh with fallback ONLY if enableCertSpotter=true. both/certspotter also require explicit opt-in.

## `includeExpired` (type: `boolean`):

Off = only hosts with a currently valid certificate. On = full history (good for finding forgotten hosts). Cert Spotter lists unexpired certificates only.

## `wildcardMode` (type: `string`):

merge = count \*.dev.example.com as dev.example.com and flag hasWildcardCert. separate = keep \*.dev.example.com as its own row. drop = ignore wildcard names.

## `includeRootDomain` (type: `boolean`):

Output a row for example.com itself when it appears in certificates.

## `maxSubdomainsPerDomain` (type: `integer`):

Cap per root domain. If more are found, the most recently seen are kept and a warning is logged. Default 5000 (50 in the demo).

## `resolveDns` (type: `boolean`):

Look up each found host. Adds dnsStatus, a, aaaa, cname and a dangling-CNAME hint. Passive: DNS queries only, no ports or HTTP. On by default in the demo.

## `onlyResolving` (type: `boolean`):

Drop hosts without A/AAAA records (turns DNS resolve on).

## `takeoverHints` (type: `boolean`):

Flag hosts whose CNAME points to a name that no longer resolves, naming the service (GitHub Pages, S3, Azure, Heroku…). A hint to verify manually, not a confirmed vulnerability.

## `dnsServers` (type: `array`):

Resolver IPs, e.g. 1.1.1.1. Empty = the platform resolver.

## `dnsConcurrency` (type: `integer`):

Hosts looked up at the same time.

## `monitor` (type: `boolean`):

Remember found hosts in a named key-value store, one record per domain. Schedule the Actor to get alerts for new subdomains. The first run outputs everything (baseline).

## `onlyNew` (type: `boolean`):

With monitor mode, output only hosts not seen in earlier runs.

## `monitorKey` (type: `string`):

Separate memories for separate watch lists (letters, digits, - \_ . ! ' ( ), max 60).

## `stateStoreName` (type: `string`):

Named key-value store that keeps the monitor memory between runs.

## `certspotterApiKey` (type: `string`):

Without a key, SSLMate allows a limited number of Cert Spotter queries per hour for personal/evaluation use. Add your own key (sslmate.com) for regular or heavier use.

## `certspotterMaxPages` (type: `integer`):

Each page is one request. Default 5 without a key, 50 with one.

## `crtshTimeoutSecs` (type: `integer`):

Per request. On timeout or 5xx the Actor tries crt.sh's plain query once, then falls back to Cert Spotter (in auto mode).

## `maxResponseMb` (type: `integer`):

Bigger crt.sh answers are not loaded; the domain falls back to Cert Spotter. Raise together with memory for very large domains.

## `enableCertSpotter` (type: `boolean`):

Off by default, even with an API key. Enable only if your SSLMate plan permits the intended commercial/personal use. Needed for auto fallback, both, or certspotter sources.

## Actor input object example

```json
{
  "sources": "crtsh",
  "includeExpired": false,
  "wildcardMode": "merge",
  "includeRootDomain": true,
  "onlyResolving": false,
  "takeoverHints": true,
  "dnsConcurrency": 10,
  "monitor": false,
  "onlyNew": true,
  "monitorKey": "default",
  "stateStoreName": "subdomain-finder-ct-state",
  "crtshTimeoutSecs": 60,
  "maxResponseMb": 40,
  "enableCertSpotter": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("foresighted_sunset/subdomain-finder-ct-logs").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("foresighted_sunset/subdomain-finder-ct-logs").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call foresighted_sunset/subdomain-finder-ct-logs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,foresighted_sunset/subdomain-finder-ct-logs"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/va1pFWomhDCi17DcZ/builds/TRDcIR23EwDT3Iy9j/openapi.json
