# Domain Security Auditor — DMARC, SPF, DKIM, TLS & DNS in bulk (`datadeltas/domain-security-auditor`) Actor

Audit any list of domains for email spoofing risk and web security: SPF (with real lookup counting), DKIM, DMARC policy, MTA-STS, TLS expiry, DNSSEC, CAA and security headers — scored, graded, with the fix for every finding.

- **URL**: https://apify.com/datadeltas/domain-security-auditor.md
- **Developed by:** [Daniel Matthee](https://apify.com/datadeltas) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 domain auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Domain Security Auditor — DMARC, SPF, DKIM, TLS & DNS in bulk

Paste a list of domains. Get back, for each one, whether anybody on the
internet can send email as that domain — plus the certificate, DNS and
header problems sitting next to it. Every finding comes with the fix.

Since February 2024, Gmail and Yahoo require SPF, DKIM **and** DMARC from
bulk senders. Most domains still fail at least one. This actor tells you
which, at whatever scale you need — one domain, or your whole prospect list.

**No API keys. No account anywhere.** Everything here is public DNS and a
TLS handshake, read over DNS-over-HTTPS so it works from any container.

### What it checks

| Check | What you learn |
|---|---|
| **SPF** | Record, the `all` qualifier, and the **fully expanded DNS lookup count** |
| **DKIM** | Which of ~30 mainstream selectors resolve (Google, M365, Klaviyo, SendGrid, Zoho, Mimecast…) |
| **DMARC** | Policy (`none`/`quarantine`/`reject`), subdomain policy, `pct`, whether reports are collected |
| **MX** | Hosts, plus the mailbox provider inferred from them |
| **MTA-STS** | Both the TXT record *and* whether the policy file is actually served |
| **TLS-RPT / BIMI** | Whether reporting and brand indicators are published |
| **TLS certificate** | Issuer, expiry date, days remaining (negative when already expired) |
| **DNSSEC / CAA** | Whether answers are signed, and whether CA issuance is restricted |
| **Security headers** | HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy |

#### The SPF lookup count is the one most tools get wrong

SPF allows a receiver **ten** DNS-querying mechanisms. The limit applies to
the *fully expanded* tree, not the record you can see. Add one more SaaS
sender to a record that already includes Google, HubSpot and Zendesk and you
can cross it — at which point receivers return `permerror` and **SPF stops
working entirely**, silently.

This actor resolves every `include:` and `redirect=` recursively (with loop
protection) and reports the real number.

### Example output record

```json
{
  "domain": "example.com",
  "score": 47,
  "grade": "D",
  "dmarcPolicy": "none",
  "spfPublished": true,
  "dkimFound": true,
  "tlsDaysRemaining": 61,
  "criticalCount": 0,
  "topFinding": "SPF needs 13 DNS lookups (limit 10)",
  "checkedAt": "2026-09-03T04:11:20Z",
  "email": {
    "spf_record": "v=spf1 include:_spf.google.com include:servers.mcsv.net include:sendgrid.net ~all",
    "spf_all": "~all",
    "spf_lookups": 13,
    "dmarc_record": "v=DMARC1; p=none; rua=mailto:dmarc@example.com",
    "dmarc_policy": "none",
    "dmarc_rua": true,
    "dkim_selectors_found": ["google", "k1"],
    "mx_hosts": ["aspmx.l.google.com", "alt1.aspmx.l.google.com"],
    "mail_provider": "Google Workspace",
    "mta_sts": false,
    "tls_rpt": false,
    "bimi": false
  },
  "web": {
    "https_reachable": true,
    "tls_issuer": "Let's Encrypt",
    "tls_expires_at": "2026-11-03T09:14:00+00:00",
    "tls_days_remaining": 61,
    "hsts": true,
    "missing_headers": ["content-security-policy", "permissions-policy"]
  },
  "findings": [
    {
      "id": "spf-too-many-lookups",
      "severity": "high",
      "title": "SPF needs 13 DNS lookups (limit 10)",
      "detail": "Receivers stop evaluating and return permerror, so SPF effectively fails for every message.",
      "remediation": "Flatten or remove include: chains to get under ten lookups."
    },
    {
      "id": "dmarc-policy-none",
      "severity": "high",
      "title": "DMARC policy is p=none",
      "detail": "Monitoring only: spoofed mail is still delivered.",
      "remediation": "Move to p=quarantine, then p=reject, once reports look clean."
    }
  ]
}
```

The nine flat columns (`domain`, `score`, `grade`, `dmarcPolicy`,
`spfPublished`, `dkimFound`, `tlsDaysRemaining`, `criticalCount`,
`topFinding`) are what the Overview table and a CSV export show, so the
result drops straight into a spreadsheet. The nested `email`, `web` and
`findings` objects are there when you want the detail.

### Scoring

Every domain starts at 100 and loses weight per finding — 25 for no DMARC,
20 for no SPF, 15 for an expired certificate, down to 2 for a missing CAA
record. Grades: **A** 90+, **B** 80+, **C** 65+, **D** 50+, **E** 30+, **F**
below. The score exists to sort a list; the findings are what you act on.

### Input options

| Option | Default | What it does |
|---|---|---|
| `domains` | — | The list to audit. URLs, email addresses and bare hosts all work — each is reduced to its domain and deduplicated |
| `startUrls` | — | Alternative list input for integrations that emit URLs; merged with `domains` |
| `dkimSelectors` | — | Extra DKIM selectors to try on top of the ~30 built in |
| `includeWebChecks` | `true` | Turn off for a DNS-only run: no TLS handshake, no header fetch, faster |
| `concurrency` | `5` | Domains audited at once (max 20) |
| `maxItems` | `100` | Hard cap per run, so a large paste cannot run away with cost |

### Use cases

- **Agency prospecting.** Audit a list of target companies and open with
  "anyone can spoof your invoices" — a `p=none` domain is a live risk you
  can prove in one line. Search terms: *DMARC checker bulk*, *email
  spoofing audit*.
- **M\&A and vendor due diligence.** Score every domain a counterparty owns
  before you inherit their email security. Search terms: *domain security
  audit*, *third party risk email*.
- **Your own estate.** Schedule it across every domain you own and catch the
  certificate expiring in 14 days, or the SPF record that quietly crossed
  ten lookups when marketing added a new tool.

### Honest limits

- **DKIM cannot be enumerated.** There is no DNS mechanism to list a
  domain's selectors — they can only be guessed. `dkimFound: false` means
  the ~30 mainstream selectors did not resolve, **not** that DKIM is
  missing. Pass your own via `dkimSelectors` if you know them.
- **DNSSEC is read from the resolver's AD flag**, which is the practical
  signal that the zone is signed and validated, not a full chain walk.
- **The certificate is read with verification disabled**, on purpose — an
  expired or mismatched certificate has to remain inspectable. Trust chain
  validity is not asserted, only issuer and expiry.
- A domain with no MX is reported as such rather than being marked down for
  a missing SPF record it may not need.
- Resolver failures are recorded in `errors` and never silently reported as
  a missing record.

### Pricing

Pay per event: **$0.01 per domain audited**. A 500-domain estate audit is
$5. Runs that audit nothing cost nothing beyond the platform's start fee.

### Changelog

- **0.1** — first release: SPF (recursive lookup counting), DKIM selector
  discovery, DMARC, MX and provider detection, MTA-STS with policy fetch,
  TLS-RPT, BIMI, DNSSEC, CAA, TLS expiry, security headers, scoring and
  graded findings.

# Actor input Schema

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

Domains to audit. URLs, email addresses and bare hostnames are all accepted - each is reduced to its registrable domain, deduplicated.

## `startUrls` (type: `array`):

Optional alternative input for integrations that pass a URL list. Merged with 'domains'.

## `dkimSelectors` (type: `array`):

DKIM cannot be enumerated from DNS - a selector is either known or guessed. Around 30 mainstream selectors are always checked; add your own here.

## `includeWebChecks` (type: `boolean`):

Also open an HTTPS connection to read the certificate expiry and security headers. Turn off for a DNS-only, faster run.

## `concurrency` (type: `integer`):

How many domains to audit at once.

## `maxItems` (type: `integer`):

Hard cap on domains audited in one run, so a large paste cannot run away with cost.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "https://www.example.org/pricing",
    "sales@example.net"
  ],
  "includeWebChecks": true,
  "concurrency": 5,
  "maxItems": 100
}
```

# Actor output Schema

## `records` (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 = {
    "domains": [
        "apify.com",
        "anthropic.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datadeltas/domain-security-auditor").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 = { "domains": [
        "apify.com",
        "anthropic.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("datadeltas/domain-security-auditor").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 '{
  "domains": [
    "apify.com",
    "anthropic.com"
  ]
}' |
apify call datadeltas/domain-security-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datadeltas/domain-security-auditor"
        }
    }
}

```

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/WlKc0TWXnV7Qf8GfQ/builds/p2wqf6sA6OasNMmBy/openapi.json
