TLS · DNS · IP reputation — three signals, one grade, streamed as they arrive.
Type a domain. Press Enter. Within seconds you know:
- TLS — certificate chain trust, expiry, algorithm strength, OCSP stapling, CT logs, forward secrecy
- DNS — SPF, DMARC, DKIM, MTA-STS, DNSSEC, CAA, MX configuration, lint findings
- IP reputation — every resolved address classified: residential, datacenter, VPN, Tor, botnet C2
All three checks run in parallel. Results stream in as each backend responds — no spinner, no wait-and-dump. The page comes alive progressively. A weighted aggregate produces an A+–F grade with per-section breakdowns and hard-fail overrides for baseline security requirements.
Checking netray.info — A+ across all three signals.
|
Dark theme |
Light theme |
|
Expanded — full check list |
Mobile |
Browser — lens.netray.info
Terminal:
# SSE stream (results arrive as they complete)
curl -N 'https://lens.netray.info/api/check/example.com'
# Single JSON response — better for scripts and LLMs
curl -s -H 'Accept: application/json' 'https://lens.netray.info/api/check/example.com' | jq .Shareable link: https://lens.netray.info/?d=yourdomain.com
lens has first-class Claude integration via MCP. Once installed, you can ask Claude to check domain health directly in any conversation — no copy-pasting curl output, no manual JSON parsing.
Claude Code — run this skill from the lens repo directory:
/lens-mcp-codeThe skill writes ~/.claude/mcp-servers/lens/server.mjs, installs dependencies, and registers the server with claude mcp add. Requires Node.js ≥ 18.
Claude Desktop — same skill, different registration target:
/lens-mcp-desktopEdits ~/Library/Application Support/Claude/claude_desktop_config.json automatically. Restart Claude Desktop after the skill completes.
| Tool | What it does |
|---|---|
check_domain |
Full DNS + TLS + IP check for one domain — structured JSON with grade, scores, hard-fail details |
check_domains |
Check up to 10 domains sequentially — per-domain results, errors captured per-entry |
lens_meta |
Server metadata: version, backends, scoring profile, rate limits |
Once installed, ask Claude things like:
"Check the domain health of example.com — is anything hard-failing?"
"Check these three domains and compare their TLS grades: example.com, example.org, example.net"
"My deploy is failing the health gate. Check example.com and tell me what's causing the F grade."
GET /api/check/{domain}
POST /api/check {"domain": "example.com"}
Default output: SSE stream. Set Accept: application/json, ?stream=false, or "stream": false in the POST body to get a single merged JSON object (sync mode).
# SSE — events stream in as backends respond
curl -N 'https://lens.netray.info/api/check/example.com'
# Sync — one JSON object, all sections merged
curl -s 'https://lens.netray.info/api/check/example.com?stream=false'
curl -s -H 'Accept: application/json' 'https://lens.netray.info/api/check/example.com'
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"domain":"example.com","stream":false}' \
'https://lens.netray.info/api/check'| Event | Payload |
|---|---|
dns |
DNS findings, resolved IPs, per-check results |
tls |
Certificate chain, quality checks, grade |
http |
HTTP security headers, HTTPS redirect, CORS, cookie posture (omitted when http_url not configured) |
ip |
Per-IP classification: network type, ASN, geo |
summary |
Overall grade, score, section grades, hard_fail, hard_fail_reason |
done |
Domain, duration_ms, cached flag |
{
"dns": { "status": "ok", "findings": [...], "resolved_ips": [...] },
"tls": { "status": "ok", "checks": [...], "grade": "A" },
"ip": { "status": "ok", "ips": [...] },
"summary": {
"overall": "A", "grade": "A", "score": 87.0,
"hard_fail": false, "hard_fail_reason": null,
"sections": { "dns": "B", "tls": "A", "ip": "A+" }
},
"done": { "domain": "example.com", "duration_ms": 412, "cached": false }
}hard_fail_reason is a human-readable string when hard_fail is true (e.g. "SPF Record, Chain of Trust"), otherwise null.
Results are cached for 5 minutes. Cache hits return X-Cache: HIT and complete in milliseconds.
| Endpoint | Description |
|---|---|
GET /api/meta |
Server version, backends, scoring profile, rate limits |
GET /health |
Liveness probe |
GET /ready |
Readiness probe |
GET /api-docs/openapi.json |
OpenAPI 3.1 spec |
GET /docs |
Interactive API docs (Scalar UI) |
Gate deploys on domain health — if the grade drops below B, fail the build:
- name: Domain health check
run: |
curl -sf -H 'Accept: application/json' \
"https://lens.netray.info/api/check/$DOMAIN" \
| jq -e '.summary.grade | test("^(A|B)")'Or pull structured data for reporting:
curl -s -H 'Accept: application/json' 'https://lens.netray.info/api/check/example.com' \
| jq '{grade: .summary.grade, score: .summary.score, hard_fail: .summary.hard_fail_reason}'This section is the authoritative description of the scoring algorithm. Any change to src/scoring/ must update this section in the same commit.
Each backend returns a set of named checks. Every check has a status: pass, warn, fail, not_found, skip, or error.
- Per-check score:
pass= full weight,warn= half weight,fail/not_found= 0.skipanderrorare excluded entirely. - Section score: weighted sum of earned points ÷ weighted sum of possible points, as a percentage.
- Overall score: weighted average of section scores.
- Hard-fail overrides: certain failures force the overall grade to F regardless of the numeric score.
- Letter grade: score mapped to thresholds.
| Section | Weight | Rationale |
|---|---|---|
| TLS | 40% | Certificate validity and transport security are foundational |
| DNS | 30% | Email authentication and DNS health have major deliverability impact |
| HTTP | 20% | HTTP security headers, HTTPS redirect, CORS, and cookie posture (requires spectra backend) |
| IP | 10% | Reputation informs risk but is beyond the domain owner's direct control |
The HTTP section is optional. When http_url is not configured, lens scores from TLS, DNS, and IP only. Section weights are rebalanced proportionally across the active sections by the scoring engine.
| Grade | Score | Meaning |
|---|---|---|
| A+ | ≥ 97% | Exemplary — all checks pass |
| A | ≥ 90% | Excellent — minor gaps only |
| B | ≥ 75% | Good — some non-critical findings |
| C | ≥ 60% | Fair — notable gaps, action recommended |
| D | ≥ 40% | Poor — significant issues present |
| F | < 40% | Failing — or hard-fail override triggered |
These conditions force an F regardless of the numeric score:
| Condition | Why |
|---|---|
| Untrusted TLS chain | Certificate not signed by a trusted CA — browsers reject it |
| Expired certificate | Any certificate in the chain |
| No SPF record | Missing entirely (not misconfigured) |
| No DMARC record | Missing entirely |
| Weight | Meaning |
|---|---|
| 10 | Security-critical — failure has immediate, severe consequences |
| 5 | Important — strongly recommended by standards or best practice |
| 3 | Significant — meaningful impact on deliverability or security posture |
| 2 | Advisory — good practice, but failure is not operationally harmful |
| 1 | Informational — low-cost improvement opportunity |
The scoring profile is defined in TOML. The default is embedded in the binary; override it with scoring.profile_path in lens.toml.
Profile format (v2)
[meta]
name = "default"
version = 2
[sections.tls]
weight = 40
hard_fail = ["chain_trusted", "not_expired"]
[sections.tls.checks]
chain_trusted = 10
not_expired = 10
hostname_match = 10
chain_complete = 5
strong_signature = 5
key_strength = 5
expiry_window = 5
tls_version = 5
forward_secrecy = 5
aead_cipher = 5
ocsp_stapled = 3
ct_logged = 3
[sections.dns]
weight = 30
hard_fail = ["spf", "dmarc"]
[sections.dns.checks]
spf = 10
dmarc = 10
dnssec = 5
caa = 5
mx = 5
[sections.http]
weight = 20
hard_fail = []
[sections.http.checks]
https_redirect = 5
hsts = 5
security_headers = 5
cors = 3
cookie_secure = 2
hygiene = 2
[sections.ip]
weight = 10
hard_fail = []
[sections.ip.checks]
reputation = 5
[thresholds]
"A+" = 97
"A" = 90
"B" = 75
"C" = 60
"D" = 40
"F" = 0cp lens.example.toml lens.toml[server]
bind = "0.0.0.0:8082"
metrics_bind = "127.0.0.1:9090"
# trusted_proxies = ["10.0.0.0/8"]
[backends]
dns_url = "https://dns.netray.info"
tls_url = "https://tls.netray.info"
ip_url = "https://ip.netray.info"
# backend_timeout_secs = 20
[cache]
enabled = true
ttl_seconds = 300
[rate_limit]
per_ip_per_minute = 10
per_ip_burst = 3
global_per_minute = 100
global_burst = 20
[scoring]
# profile_path = "profiles/default.toml" # override built-in defaultOverride any value with environment variables: LENS_ prefix, __ for nesting — e.g. LENS_SERVER__BIND=0.0.0.0:8082.
Prerequisites: Rust toolchain, Node.js (for the frontend).
make # frontend + release binary
make dev # cargo run (hot-reloads nothing, but starts quickly)
make test # Rust unit + integration tests + frontend tests
make ci # full gate: fmt, clippy, test, frontend build, audit
# Two-terminal dev workflow
make frontend-dev # Vite dev server on :5174 (proxies /api/* to :8082)
make dev # cargo run on :8082The release binary embeds the compiled frontend — no separate static file hosting required.
Backend — Rust · Axum 0.8 · reqwest · tokio · utoipa (OpenAPI 3.1) · tower-governor (GCRA rate limiting) · moka (TTL cache)
Frontend — SolidJS 1.9 · Vite · TypeScript (strict) · @netray-info/common-frontend
Part of — netray.info suite: IP · DNS · TLS · HTTP · Email
MIT — see LICENSE.





