Method
The technical details behind How we check: exactly what a check loads and within which bounds, how the points add up, where the data comes from and which limits apply.
1. What a check reads
A check consists of exactly these steps. Nothing else happens.
- One GET request to your address, just like any normal page visit. We read the response headers and the served HTML, at most 800 KB, and wait at most 8 seconds.
- Redirects we follow ourselves, at most 5. Before each hop, we verify that it does not point to an internal or private address.
- The JavaScript files the page includes itself, that is scripts from
<script src>andmodulepreload. Every browser loads these files during a page visit as well. We only load files from the same domain, including subdomains such as cdn.your-company.com, at first at most 10 files, 2 MB per file and 6 MB in total, within at most 6 seconds. If a script address redirects, we don't follow. Before each request, we verify that the address is not internal or private. - Lazy-loaded parts these files fetch when someone switches to a subpage, including parts that load further parts, and for Next.js the parts of every page from the build manifest: at most 40 more files in up to three rounds, within at most 5 seconds and the same 6 MB.
- Configuration files from the same domain that the code fetches itself on startup, such as
/config.jsonor/api/config: only addresses written as fixed text in afetchcall that look like configuration, at most 4, read-only. - The page's stylesheets from the same website, at most 3, for the data protection check (for example Google Fonts via
@import). - One request over http:// to the same address, only to see whether it redirects to https.
- The file
/.well-known/security.txt(RFC 9116), which is meant for security reports. - One TLS handshake on port 443, the same one every browser performs. From it we read the validity, issuer, expiry date and protocol of the certificate.
- Public DNS lookups: SPF and DMARC (for a subdomain such as shop.example.com also at example.com), the SPF records your record includes, MX, MTA-STS and DANE, CAA and the address's CNAME chain. None of this talks to your server.
- One DNSSEC lookup via DNS-over-HTTPS at Cloudflare. Only the domain name goes there.
- Lookups at OSV.dev, CISA, FIRST, endoflife.date and WordPress.org for detected software. Only package names, versions and CVE numbers go there, never your address.
We don't load images, stylesheets or scripts from other domains. We don't probe paths the page does not include or fetch itself, except security.txt. If we find a key, we never use it. The report states the type and where it was found, the value only shortened, for example sk_live_…9f2A. We never show or store the full value.
Detecting platforms
The platforms field of the report states what an app runs on. We only name a platform when there is an unambiguous trace. Tools such as Cursor, Claude Code or Codex leave no trace of their own. Apps built with them show up as Vite or Next.js, for example.
| Platform | Detected |
|---|---|
| Lovable, Bolt, v0, Replit, Base44 | by the hosting domain, such as *.lovable.app, or by included platform scripts |
| Vercel, Netlify | by the response headers |
| Next.js, Vite | by the script paths in the HTML |
| Supabase, Firebase | by addresses in the JavaScript files |
2. How the grade is calculated
Every check starts with 10 points. Each security header that is present adds points, up to 100 in total. We then deduct for other findings. The result is always between 0 and 100.
Points for security headers
| Header | Points | If missing, it is a |
|---|---|---|
| Base value | 10 | always |
| Content-Security-Policy | +25 | warning |
| Strict-Transport-Security (HSTS) | +20 | warning |
| X-Frame-Options or frame-ancestors in the CSP | +15 | notice |
| X-Content-Type-Options | +10 | notice |
| Referrer-Policy | +10 | notice |
| Permissions-Policy | +10 | notice |
| Maximum | 100 |
What counts is whether a header is present. For the Content-Security-Policy, the content counts too: if it doesn't govern scripts, allows 'unsafe-inline' without a nonce or hash, or allows scripts from any address (*, https:, data:), it earns 10 points instead of 25 and the notice header.csp.weak. With 'strict-dynamic' it counts as strong. Several policies apply together; one strong policy is enough.
Deductions
| Finding | Deduction |
|---|---|
| No HTTPS connection | minus 20, and the grade is always F |
| Secret key in the page source or JavaScript files, such as Supabase service_role, OpenAI or Stripe | minus 40 per key type |
| Address strangers can take over (dangling CNAME) | minus 40 |
| Version with known vulnerabilities that CISA lists as actively exploited | minus 25 per finding |
| Version with known vulnerabilities | minus 8 per finding, at most minus 20 |
| Secret pattern in the HTML | minus 25 per key type |
| Other code finding with a warning (mixed content, error output, AI call directly from the browser, secret-named variables) | minus 10 |
| Code finding as a notice (Google API key, SRI, source maps, sensitive paths, Supabase with a public key) | minus 3 |
| Certificate, email protection or missing redirect to https, warning (also SPF with more than 10 lookups) | minus 5 |
| Certificate, email protection or missing redirect to https, notice | minus 2 |
In the report but without a deduction: a visible server version, a set X-Powered-By header, cookies without Secure or HttpOnly, all data protection findings and DNS hygiene (CAA, DNSSEC, security.txt, MTA-STS). The grade measures the attack surface. A certificate that expires in less than 14 days is a warning; under 30 days, a notice.
Grade thresholds
| Grade | Points |
|---|---|
| A | 90 to 100 |
| B | 75 to 89 |
| C | 60 to 74 |
| D | 40 to 59 |
| E | 20 to 39 |
| F | below 20, or no HTTPS |
Every finding means "potentially affected", not proof. 100 points mean that none of our checks triggered. That does not prove the site is secure. What lies behind the surface, such as databases and access permissions, only a Security Audit can see.
3. Key search
In the HTML, in embedded data such as __NEXT_DATA__, in the JavaScript files and in configuration files, we look for secret keys and credentials, among them: Supabase master key (service_role), secret Supabase key (sb_secret), OpenAI, Anthropic, OpenRouter, Groq and xAI, Stripe (sk_live, rk_live), AWS, GitHub, Slack, SendGrid, Twilio, webhook URLs for Slack, Discord, Teams and Zapier, database credentials with a password, private keys (PEM). On top of that come over 100 more providers from the rules of the open-source project gitleaks. Values Base64-encoded with atob() are read decoded. Google API keys are only reported as a notice, because they are often public on purpose. We also report when an app calls an AI API directly from the browser, when build variables with VITE_, NEXT_PUBLIC_, REACT_APP_ or EXPO_PUBLIC_ carry words such as SECRET or PASSWORD in their name, and when the files reference source maps. If an app talks to Supabase with the public key, we add a notice: that is only safe with access rules (Row Level Security) on every table. Whether they are correct, we cannot see from the outside.
We report the type and where it was found, the value only shortened, for example sk_live_…9f2A or AKIA…MPLE. We never show the full value.
4. Data sources
Technology detection
We detect the software a site uses with the rules of webappanalyzer (a maintained fork of the Wappalyzer rules) and the detection patterns of Retire.js, from response headers, cookies, HTML and the loaded scripts.
OSV.dev, CISA KEV and FIRST EPSS
About 50 widely used JavaScript libraries, such as jQuery, Bootstrap, React, Vue, Angular, Next.js, Lodash, Moment, Axios, Chart.js, DOMPurify, TinyMCE, are matched live against OSV.dev, an open database of known vulnerabilities, with up to 6 entries per library. If a vulnerability is in the US agency CISA's catalog of actively exploited vulnerabilities, the finding is critical. We give the probability of exploitation in the next 30 days according to FIRST EPSS. If OSV.dev does not respond within 4.5 seconds, we use our own list and say so.
End of support
Whether a detected version still receives security updates, for example PHP, WordPress, nginx or Node.js, we check via endoflife.date. WordPress.org itself lists insecure WordPress core versions.
Key patterns
Besides our own patterns we use the gitleaks rules (MIT license) without generic and intentionally public patterns. We check every change against over 100 real sites for false alarms and against a test course with typical leak locations.
Our own list
For software without an npm package, a short, fixed list applies:
| Software | Reported for version | Reason |
|---|---|---|
| PHP | before 7.4 and before 8.1 | outside security support |
| WordPress | before 6.4 | older core versions with documented vulnerabilities |
| jQuery | before 3.5.0 | CVE-2020-11022 and CVE-2020-11023 |
| Bootstrap | before 4.3.1 | CVE-2019-8331 |
| OpenSSL | before 1.1.1 | very old release line |
We detect versions from the Server header, from X-Powered-By, from the generator tag and from the addresses of included files.
5. Limits
| Access | Checks per hour |
|---|---|
| Without sign-in and without a key (browser and MCP, per IP address) | 20 |
| Free account or key | 100 |
| Business | 2,000 |
The REST API always requires a key. The limits apply per hour and are there to slow down abuse. Each check also has fixed bounds. The request may take 8 seconds, the TLS handshake 5 seconds. We read at most 800 KB of HTML and follow at most 5 redirects. Of the JavaScript files, we load at most 10, 2 MB per file and 6 MB in total, within at most 6 seconds.
Without a paid plan, the report contains the grade, the score and the number of findings, but only 2 findings in full. The shortening happens on our server. With Business, the report contains every finding with a fix guide.
We don't check internal addresses, localhost or names ending in .local, .internal and .test.