Scanner Methodology
What the scanner sends to your site, how each result is produced, how findings are mapped to the OWASP Top 10, and exactly how the A to F grade is calculated. Written from the scanner source, so you can check a claim on the site against what the code does. For the output itself, see the sample report.
How a scan runs
A scan takes one URL and runs in waves, each wave in parallel with its own timeout. A full scan finishes in 30 to 60 seconds. The target is validated first: addresses in private, loopback and cloud-metadata ranges are refused, DNS is resolved once and the probes connect to that validated address, and every redirect hop is re-validated before it is fetched. A check that fails or times out returns an empty result and never aborts the scan.
The scanner produces 31 result families, listed below, plus four derived values: the CVE list, the OWASP mapping, the grade and its colour. The TypeScript result type documents 28 of the 31; ai_readiness, protocol_extras and a thirty-first internal key (site_owner, used only to choose the language of follow-up e-mail and not a security check) were added later.
There is deliberately no single "number of checks" on this page. A check has no fixed unit: one family can be one handshake or seventy-one path probes. The counts you see below are the lengths of the lists in the code.
What the scanner sends, and what it does not
Most of it is what a browser or search bot would send: a GET for the homepage, GETs for 71 well-known paths, a TLS handshake, DNS queries. Response bodies are read only for the homepage and error pages (capped at 2 MB); path probes read the status line and headers and stop.
Some probes go beyond reading. These are all of them:
- TCP connects to 10 ports, with no data sent.
- One anonymous FTP login attempt on port 21.
- SMTP connections on ports 587, 465 and 25 to read TLS support. No mail is sent.
- Requests carrying an Origin header, to see whether it is echoed back.
- TRACE, DELETE and PUT requests to the URL, with no body and with redirects off.
- Up to two requests with SQL-injection-shaped query strings (a WAF probe, sent only when no WAF is recognised from headers, and one to /index.php), to read how the site responds.
- Five sequential GETs to look for rate limiting.
Nothing is exploited. No payload is sent that tries to read, change or delete data, no credentials are submitted or guessed, and no brute force is attempted. The PUT and DELETE requests carry no body, but a server that treats a bodiless PUT or DELETE on its root URL as a real operation would act on it. That is a rare configuration, and it is the reason to scan only sites you own or have permission to scan.
Some lookups go to third parties, who then see the scanned URL or domain: VirusTotal (when configured), PhishTank, AlienVault OTX, URLhaus, Spamhaus, urlscan.io (unlisted, when configured), crt.sh and Shodan InternetDB, plus NVD, OSV and the retire.js database on GitHub for version matching.
The 31 result families
For each family: how it is probed, and what it contributes to the grade. "Not graded" means the result appears in the report but changes neither the OWASP mapping nor the letter.
Transport and TLS
| Family | How it is probed | Contribution to grade |
|---|---|---|
ssl | One TLS handshake to port 443 on the validated IP with certificate verification on. Reads issuer, expiry and the negotiated protocol. | A02 when the handshake fails verification. |
ssl_deep | SSLyze against port 443: certificate chain, SSL 2.0, SSL 3.0 and TLS 1.0 to 1.3 cipher suites, Heartbleed, TLS compression, and a hostname match against the leaf certificate. | A02, but only for critical or high issues: SSL 2.0/3.0, TLS 1.0/1.1, Heartbleed, compression, hostname mismatch. A missing TLS 1.3 is reported and not graded. |
redirects | A GET to the http:// form of the URL with automatic redirects off. The Location chain is followed by hand, up to 10 hops, re-validating each host. | A02 for every redirect finding, including low-risk ones such as a 302 or a chain longer than 3 hops. |
headers | One request, headers only, for 7 security headers: Strict-Transport-Security, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy. Server, X-Powered-By and X-AspNet(Mvc)-Version are flagged only when the value contains a digit. | A02 when HSTS is absent. A05 when 3 or more of the 7 are missing, and for each versioned server header. |
cookies | Reads Set-Cookie on the homepage response and flags a missing Secure, HttpOnly or SameSite attribute per cookie. | Not graded. Shown in the report only. |
Web application surface
| Family | How it is probed | Contribution to grade |
|---|---|---|
cors | Two requests, one with Origin: https://evil.com and one with Origin: null. Flags an echoed origin, and a wildcard origin combined with credentials. | A01. |
methods | Sends TRACE, DELETE and PUT to the URL with redirects off. A method is flagged when the status is not 403, 404, 405 or 501. | A01. |
owasp | 71 GET requests for well-known paths (.env, .git, backup archives, SQL dumps, phpMyAdmin, debug and CI files, security.txt and others). Status only; a body is never downloaded. Redirects are not followed. A 200 counts as a hit. | A01 for paths containing admin, panel or manager. A03 for .env, .git, phpinfo, .htaccess, web.config. A09 when security.txt is absent. |
content | Reads the homepage HTML (capped at 2 MB). Looks for http:// resources on an https page, external scripts and stylesheets without an integrity attribute, forms that post over http, forms without a CSRF-style field, stack-trace and debug strings, and sensitive words in HTML comments. | A08 for mixed content and missing SRI. Forms and information disclosure are not graded. |
auth | GETs 18 common login and admin paths and parses any form found: CSRF field, autocomplete on the password field, CAPTCHA provider, rate-limit headers, MFA wording, password minlength and maxlength. No credential is ever submitted. | A07, and A02 when a login form is served over http. |
rate_limiting | Five sequential GET requests to the URL. Looks for a 429 or an X-RateLimit-* header. | A07 only when neither is seen and no login page was found. Five requests will rarely trigger a limit, so this is a weak signal. |
error_pages | Requests /this-path-does-not-exist-8f4a2b7c and /index.php?id=1' and matches the bodies against keyword lists for stack traces, framework names, file paths, debug text and runtime versions. | A05. |
waf | Checks response headers and the Server header against known WAF signatures. If none match, sends one request carrying ?id=1' OR 1=1-- and treats a 403 or 406 as an unnamed WAF. | Not graded. |
Network and DNS
| Family | How it is probed | Contribution to grade |
|---|---|---|
ports | A TCP connect (3 second timeout, no banner read) to 10 ports: 21, 22, 23, 25, 3306, 5432, 6379, 8080, 8443, 27017. | A03, Critical, only for 3306, 5432, 6379 and 27017. The other six are reported, not graded. |
dns | DNS queries for MX, NS, CAA, SPF, DMARC, MTA-STS, TLS-RPT and 10 DKIM selectors (default, google, selector1, selector2, k1, k2, dkim, mail, s1, s2). | A09 for a missing CAA record only. SPF, DMARC, DKIM, MTA-STS and TLS-RPT are shown and not graded. A custom DKIM selector will be reported as absent. |
ftp | Connects to port 21 and attempts an anonymous login. | Not graded. |
smtp | Connects on 587, 465 and 25 and checks for STARTTLS or implicit TLS. It does not test for open relaying and sends no mail. | Not graded. |
subdomains | Queries crt.sh for certificates issued under the domain and flags names containing staging, dev, test, admin, internal, vpn or api. | Not graded. The separate cert_transparency family below is. |
blacklist | Resolves the IPv4 address and queries 5 DNS blocklist zones. | Not graded. |
Technology and known vulnerabilities
| Family | How it is probed | Contribution to grade |
|---|---|---|
tech_stack | Matches the Server and X-Powered-By headers, the HTML generator tag, framework markers in the page and cookie names against built-in pattern lists (8 server, 5 powered-by, 11 HTML, 7 cookie patterns). | Indirectly. Each detected name and version (first 10) is looked up in NVD, and matches appear under A06. |
js_libraries | Finds versioned script URLs (CDN paths such as lib@1.2.3, lib-1.2.3.min.js) and normalises 18 well-known library names. | Indirectly. The first 20 libraries are looked up in OSV, and matches appear under A06. |
js_vulnerabilities | Compares detected library versions with the retire.js vulnerability database, fetched from GitHub on each scan. | A06, for critical and high matches only (first 5 reported). |
shodan | Looks up the resolved IP in Shodan InternetDB. This is Shodan's existing data; the scanner sends nothing to your server for it. | A06 for CVEs listed against the IP. A05 for 12 risky ports (21, 23, 25, 445, 1433, 1521, 3306, 3389, 5432, 5900, 6379, 27017). |
Reputation and third-party sources
| Family | How it is probed | Contribution to grade |
|---|---|---|
threat_intel | Asks five sources about the URL or domain: VirusTotal, PhishTank, AlienVault OTX, abuse.ch URLhaus and Spamhaus DBL. VirusTotal needs an API key and reports itself unavailable without one. | VirusTotal, URLhaus and Spamhaus: A05. PhishTank: A01. OTX pulse count above zero: A09. An unavailable source contributes nothing. |
cert_transparency | Queries crt.sh and flags subdomain names containing admin, staging, dev, test, internal, vpn, api, debug, backup, old or legacy. | A05. |
urlscan | Submits the URL to urlscan.io as an unlisted scan and reads the verdict. Needs an API key and reports itself unavailable without one. | A05 when flagged malicious. |
SEO and AI search (separate grades, never part of the security grade)
| Family | How it is probed | Contribution to grade |
|---|---|---|
seo | 21 on-page checks, reported with their own grade. | Not part of the security grade. |
ai_content | 17 checks for patterns typical of AI-generated copy, with their own grade. | Not part of the security grade. |
ai_readiness | 10 checks of how readable the page is to AI search engines, with their own grade. | Not part of the security grade. |
protocol_extras | Lightweight protocol, compression, caching and discoverability checks that share one HEAD and one GET request. | Not part of the security grade. |
OWASP Top 10 mapping
Findings are filed under OWASP Top 10 (2021) categories by rules in the analyser, not by a language model. Each category with at least one finding gets one risk level from the rule in the right-hand column. A04 Insecure Design and A10 Server-Side Request Forgery are never produced.
| Category | What files a finding here | Risk level |
|---|---|---|
| A01 Broken Access Control | CORS findings, dangerous HTTP methods, paths containing admin/panel/manager that answer 200, a PhishTank hit. | High if any finding mentions admin, otherwise Medium |
| A02 Cryptographic Failures | Failed certificate verification, missing HSTS, any redirect finding, login form over http, critical or high SSLyze issues. | High if a finding mentions ssl or certificate, otherwise Medium |
| A03 Injection | Exposed .env, .git, phpinfo, .htaccess, web.config; open database ports. | Critical for a database port or .env, otherwise High |
| A05 Security Misconfiguration | 3 or more missing headers, versioned server headers, leaking error pages, risky Shodan ports, risky subdomain names, VirusTotal, URLhaus, Spamhaus or urlscan flags. | Medium |
| A06 Vulnerable and Outdated Components | CVEs from NVD and OSV, Shodan CVEs, critical or high retire.js matches. | Critical if any CVE is critical, High if any is high, otherwise Medium |
| A07 Identification and Authentication Failures | Login-page findings (CSRF, autocomplete, CAPTCHA, MFA wording, password length rules, default admin panels), and the generic no-rate-limiting finding. | High if a finding mentions CSRF, otherwise Medium |
| A08 Software and Data Integrity Failures | External scripts and stylesheets without SRI, mixed content. | Medium |
| A09 Security Logging and Monitoring Failures | No CAA record, no security.txt, an AlienVault OTX pulse count above zero. | Low |
How the A to F grade is calculated
The grade is computed from the risk level of each OWASP category, not from individual findings. The analyser counts how many categories are Critical, High and Medium (Low is ignored). The code tests the rules from F down to A and the first match wins, so a result that satisfies both E and F is an F:
| Grade | Rule |
|---|---|
| A | No Critical category, no High category, at most 1 Medium category. |
| B | No Critical, no High, 2 or more Medium categories. |
| C | No Critical, 1 or 2 High categories. |
| D | 1 Critical category, or 3 or more High categories, with no more than 1 Critical. |
| E | 2 Critical categories (and fewer than 2 High). |
| F | 3 or more Critical categories, or 2 Critical and 2 or more High. |
In the current rules only A03 and A06 can ever be Critical, so at most two categories reach Critical. That means 3 Critical categories is out of reach and F requires the second branch: A03 and A06 both Critical plus at least two High categories.
The grade is a rough ranking of how many separate areas have problems. It is not a risk assessment, and it is not comparable with the grade of another tool.
Limitations and false positives
Each of these follows from how the code works today. They are the reasons to read a finding as a lead to verify, not as a verdict.
- A 200 on a probed path is a hit, whatever the page contains. A site that answers 200 to every URL (many single-page apps do) can look like it exposes /admin/, /.env or /api/. The scanner does not read the body to confirm.
- The error-page check matches short keywords such as rails, django or line. A page that merely contains those words can be flagged.
- The HTTP method check flags any answer other than 403, 404, 405 or 501. A catch-all that returns 200 to PUT or DELETE is flagged even if the request changed nothing.
- Version-based CVE matching only works when the version is visible. A server that hides its version produces no CVE matches, which is absence of evidence and not a clean bill.
- The no-rate-limiting finding fires whenever no login page and no X-RateLimit header is found. That is true of most sites, so A07 is typically Medium and an A grade needs every other category to be Low or absent.
- If the header request itself fails, the header result is empty and HSTS is reported as missing.
- Redirect findings of any kind, even a 302 from http to https, land in A02 and make that category Medium.
- Cookies, DNS email authentication (SPF, DMARC, DKIM, MTA-STS, TLS-RPT), FTP, SMTP and blocklist results are reported but do not change the grade. A site with a weak DMARC policy can still score A.
- Only 8 of the 10 OWASP Top 10 (2021) categories are ever produced. A04 Insecure Design and A10 Server-Side Request Forgery are not mapped. The mapping is heuristic: exposed files are filed under A03 and a missing security.txt under A09 because those are the closest buckets the scanner has, not because OWASP defines them that way.
- Grading counts categories, not findings. A category with twenty findings counts once, the same as a category with one.
- Third-party sources (VirusTotal, urlscan.io) are used only when an API key is configured; otherwise they are marked unavailable and add nothing. Any source can also time out, in which case its result is empty.
See it applied
The sample report shows real output with the evidence behind each finding. The scanner is built and run by Roy Morken. If a statement on this page does not match what you see in a scan, that is a bug in one of them, and I want to hear about it.
Scan your siteThis page covers the scanner itself. For how the SSL Labs comparison was measured, which domains it used and what it deliberately left out, see the benchmark methodology. The two are separate documents and neither supersedes the other.