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> and modulepreload. 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.json or /api/config: only addresses written as fixed text in a fetch call 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.

PlatformDetected
Lovable, Bolt, v0, Replit, Base44by the hosting domain, such as *.lovable.app, or by included platform scripts
Vercel, Netlifyby the response headers
Next.js, Viteby the script paths in the HTML
Supabase, Firebaseby 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

HeaderPointsIf missing, it is a
Base value10always
Content-Security-Policy+25warning
Strict-Transport-Security (HSTS)+20warning
X-Frame-Options or frame-ancestors in the CSP+15notice
X-Content-Type-Options+10notice
Referrer-Policy+10notice
Permissions-Policy+10notice
Maximum100

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

FindingDeduction
No HTTPS connectionminus 20, and the grade is always F
Secret key in the page source or JavaScript files, such as Supabase service_role, OpenAI or Stripeminus 40 per key type
Address strangers can take over (dangling CNAME)minus 40
Version with known vulnerabilities that CISA lists as actively exploitedminus 25 per finding
Version with known vulnerabilitiesminus 8 per finding, at most minus 20
Secret pattern in the HTMLminus 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, noticeminus 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

GradePoints
A90 to 100
B75 to 89
C60 to 74
D40 to 59
E20 to 39
Fbelow 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:

SoftwareReported for versionReason
PHPbefore 7.4 and before 8.1outside security support
WordPressbefore 6.4older core versions with documented vulnerabilities
jQuerybefore 3.5.0CVE-2020-11022 and CVE-2020-11023
Bootstrapbefore 4.3.1CVE-2019-8331
OpenSSLbefore 1.1.1very 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

AccessChecks per hour
Without sign-in and without a key (browser and MCP, per IP address)20
Free account or key100
Business2,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.

Is your app
watertight?

Hullwise finds the leaks before someone else does: exposed keys, database credentials and outdated software. Every finding comes with steps to fix it.

https://

Click a finding to mark it as fixed.

See pricingHow a scan works

Ship fast. Stay hullwise.