# How DomainGuard tests your domains, and how to re-test it
DomainGuard makes claims about your domains: this host runs WooCommerce 8.4.0, CVE-2026-57777 applies to it, your DMARC record is `p=none`. Every one of those claims can be checked from outside with ordinary tools, and we want you (or your AI agent) to check them. Each verdict you send back, agreeing or not, goes into how we measure and fix the scanner.
This page is the method. The same text is the MCP resource `domainguard://verification`, and every task the API hands out carries its own copy of the relevant steps.
## Connect an agent
**From the dashboard (two clicks).** Settings → API & MCP access → **Connect an AI agent**. Pick your agent and click **Create key & copy setup**. For Claude Code that is one terminal command. For Cursor, VS Code and Claude Desktop it is a config snippet, and for any other agent it is a prompt to paste into the chat.
**By hand.** Create an API key (Pro or Enterprise; read-only is enough) and point any MCP client at:
```
URL: https://nhmohio.com/api/mcp (streamable HTTP, POST)
Header: Authorization: Bearer nhm_…
```
Claude Code:
```
claude mcp add --transport http --scope user domainguard https://nhmohio.com/api/mcp --header "Authorization: Bearer nhm_…"
claude "Use the DomainGuard verify_findings prompt and report back."
```
No MCP? The same work runs over REST with the same header:
```
GET https://nhmohio.com/api/verification/tasks # your domains, most exposed first
GET https://nhmohio.com/api/verification/tasks?domain_id=53 # that domain's tasks
POST https://nhmohio.com/api/verification/results # your verdicts (JSON below)
```
## Rules of engagement
1. **Only your domains.** A key only sees its own account's domains, and a task only names hosts we found on them. Don't point the commands at anything else.
2. **Read-only and passive.** Every step is a DNS lookup, one HTTPS GET or HEAD, a TLS handshake, or reading the line a service sends when you connect. Nothing logs in, submits a form, brute-forces, fuzzes or tries an exploit.
3. **A version plus the published range is the whole proof** for a CVE. Never try the exploit to "confirm" it.
4. **Report what you saw, with the command or source.** Confirmations count as much as disputes.
5. **If you can't run a step** (no shell, a blocked port, an OpenSSL without TLS 1.0), answer `could_not_verify` and say why. Don't guess.
## The protocol
1. `get_verification_tasks { domain_id }` → tasks, highest stakes first (known-exploited, then confirmed, then by severity). A finding you verified in the last 7 days is left out while our claim stays the same. Pass `include_verified: true` to see them anyway.
2. Run each task's `how_to_verify` steps and compare with `compare`.
3. `submit_verification_results`:
```json
{
"domain_id": 53,
"agent": "Claude Code 2.x",
"results": [
{ "finding_key": "component:412", "verdict": "confirmed",
"observed_value": "8.4.0",
"method": "curl …/wp-content/plugins/woocommerce/readme.txt | grep -m3 '^= '",
"evidence": "= 8.4.0 2023-12-12 =" },
{ "finding_key": "exposure:9981@www.example.com", "verdict": "incorrect",
"observed_value": "8.4.0 is below the affected range 8.8.0 - 8.9.2",
"method": "CVE record affected[] + NVD versionStartIncluding",
"evidence": "versionStartIncluding 8.8.0, versionEndExcluding 8.9.3" }
],
"missed": [
{ "title": "Theme Enfold 4.5.2 has no advisory listed",
"detail": "style.css says Version: 4.5.2; DomainGuard lists no theme findings for this host.",
"evidence": "curl …/themes/enfold/style.css | grep Version" }
]
}
```
Verdicts are `confirmed` (we're right), `incorrect` (we're wrong: `observed_value` or `evidence` is required) or `could_not_verify`. Limits: 60 results and 5 missed per call, 400 a day per account.
### What happens to a verdict
- **Every verdict is stored** next to a snapshot of what we claimed at the time, per test method. That gives us a running accuracy figure for each way we test (plugin `?ver=` versus readme, SSH greetings, each checkup rule). The worst-scoring method is the next scanner fix.
- **`incorrect` files a scanner report.** Our operator gets one message per domain, and the finding goes back into the queue for a re-check. It does **not** change your result by itself. We re-test first, because an agent can be wrong too.
- **`missed`** becomes a false-negative report the same way.
- You can see what your agents confirmed and disputed under Settings → API & MCP access.
## How we test each kind of claim
### `component`: "HOST runs PRODUCT VERSION"
We read the version from what the site itself publishes. The task's `method_key` says which reader produced it.
| method_key | What we read | Re-test it with |
|---|---|---|
| `asset-ver`, `wordpress-asset` | the `?ver=` query string on a plugin or theme's own files in the homepage HTML | `curl -sL https://HOST/ \| grep -oE 'plugins/SLUG/[^"' ]*\?ver=[^"' &]+'` |
| `readme` | the newest changelog entry in the plugin's `readme.txt` | `curl -s https://HOST/wp-content/plugins/SLUG/readme.txt \| grep -m3 -E '^(= \|#+ )[0-9]'` |
| `generator` | `` | `curl -sL https://HOST/ \| grep -oiE ']*>'` |
| `style-css` | the `Version:` header of the theme's `style.css` | `curl -s https://HOST/wp-content/themes/SLUG/style.css \| grep -i '^Version:'` |
| `feed-generator` | the `` in the RSS feed (WordPress core) | `curl -sL https://HOST/feed/ \| grep -o '[^<]*'` |
| `banner` | the greeting a service sends on connect (SSH, SMTP), or a device's public status JSON (UniFi `/status`) | `nc -w5 HOST 22 = 1.12.0` and NVD says `>= 1.2`. We follow NVD, so call a finding `incorrect` only when NVD's range excludes the version too. Otherwise answer `could_not_verify` and name both ranges.
### `check`: a security-checkup finding
| Check | We test | Re-test it with |
|---|---|---|
| SPF Record / DNS Lookups / Policy Strength | the TXT record starting `v=spf1`, lookups counted recursively (limit 10) | `dig +short TXT DOMAIN` |
| DMARC Record | `_dmarc` TXT: `p=`, `rua=` (plain ASCII `mailto:` only), `pct=` | `dig +short TXT _dmarc.DOMAIN` |
| DKIM Record | the key at the selector we found (M365 `selector1/2`, Google `google`) | `dig +short TXT SELECTOR._domainkey.DOMAIN` |
| MX Records, Nameservers | the published records | `dig +short MX DOMAIN`, `dig +short NS DOMAIN` |
| MTA-STS, TLS-RPT | `_mta-sts` TXT + the policy file, `_smtp._tls` TXT | `dig`, `curl https://mta-sts.DOMAIN/.well-known/mta-sts.txt` |
| CAA | CAA records (none = any CA may issue) | `dig +short CAA DOMAIN` |
| Security headers (HSTS, CSP, X-Frame-Options, …) | response headers on each host that serves HTML, the apex redirect included | `curl -sI https://HOST/ \| grep -i '^header:'` |
| Certificate (as served) | the certificate from a real TLS handshake | `echo \| openssl s_client -connect HOST:443 -servername HOST \| openssl x509 -noout -issuer -enddate` |
| Certificate Name / Expiry / Trust, TLS 1.3 | every TLS endpoint we found (subdomains, mail and appliance ports) | one `openssl s_client -connect HOST:PORT` per endpoint the finding names |
| TLS Versions | which protocol versions the server accepts | `openssl s_client -tls1_1 …` per version |
| HTTP to HTTPS Redirect, HTTP Status | the redirect and the final status | `curl -sI http://DOMAIN/` |
| Domain Expiry, Registrar, Age | the registry's RDAP record | `curl -sL https://rdap.org/domain/DOMAIN` |
| Subresource Integrity, Third-Party Scripts | external `