Methodology

Version 1.1, reviewed 2026-09-04. A clean result only means the completed checks did not find the specific signals they test for.

This page states exactly what xlogs does, what it refuses to do, and what it cannot see. If a security tool will not tell you where its own limits are, you have no way to judge what its results are worth.

  • Exposed keys
  • Open databases
  • Public .env files
  • Private files
  • Source maps
  • Misconfigurations
  • Risky third-party scripts

Reads your public JavaScript bundles and looks for known secret formats.

7 externally observable categories, covering 8 finding types. Read-only, and nothing is exploited to confirm it.

The short version

  • Every request is a read. There are no writes, no logins, and no exploitation attempts anywhere in the scanner.
  • No account, no credentials, and no repository access. You paste a URL, that is all we take.
  • No score. Every finding carries its own evidence instead of rolling up into one number.
  • No AI at scan time. The same app returns the same result.
  • We would rather miss a low-confidence issue than raise a false alarm.

Every request we send

This is the complete list. Each one is an ordinary GET, identifying itself as xlogs/0.1 (+read-only security probe), with a 12 to 15 second timeout. There are no POST, PUT, PATCH or DELETE requests anywhere in the scanner.

MethodWhat we requestWhy
GETyour URLThe page itself. We read the response headers and the HTML.
GETeach same-origin <script src>Your JavaScript bundles, so we can look for secrets that shipped to the browser. Capped at 25 scripts, 3 MB each.
GETa bundle's .map file, if referencedOnly to confirm whether a real source map is public. We check the body actually looks like a source map before flagging it.
GETsix files that must never be public/.env, /.env.local, /.env.production, /.git/config, /.git/HEAD, and /.vscode/sftp.json. We require the response to actually look like that file, so a catch-all page returning 200 for everything does not trigger a finding. Probed in parallel, so this costs coverage rather than time.
GETyour Supabase REST API, if we find oneThe table list, then one read per table (select=*, limit 1) as an anonymous user. Capped at 25 tables.
DNSyour CNAME, SPF and DMARC recordsPublic DNS lookups to see whether a record points at something you no longer control, and whether your domain protects itself from email spoofing.
GETa public certificate log, for your domainCertificates issued for a domain are logged publicly. We read that list to show other addresses on your domain you may have forgotten are public. We do not contact those addresses.

We also refuse to scan anything that is not a public web address. Private, loopback, link-local and cloud-metadata addresses are rejected before a request is made, and that check runs again on every redirect hop.

What we check

8 checks. Each one maps to a condition someone can actually exploit.

Anyone can read this database table

xlogs finds your public Supabase URL and anon key in your app (the same ones your frontend uses), asks the database as an anonymous user which tables it can read, and flags any that return real rows. Read-only: it never writes or deletes.

A secret key is exposed

xlogs scans your live JavaScript against 9 key formats covering Stripe, OpenAI, Anthropic, AWS, Google, GitHub, Slack, private key blocks, and Supabase service_role tokens.

Missing safety headers

xlogs reads your live site's response headers and lists the protective ones that are missing.

Your original source code is downloadable

xlogs checks whether your live JavaScript bundles expose their .map files.

Private files are being served publicly

xlogs makes a read-only request for a small set of files that must never be public (.env, .git/config).

Someone else could take over your subdomain

xlogs reads your public DNS. A CNAME pointing at a hosting provider is completely normal and is not reported. It is only flagged when the target does not resolve at all, or the provider itself answers that nothing is claimed there.

Someone could send email that looks like it is from you

xlogs reads your domain's public DNS for an SPF record and a DMARC record. It skips this entirely on platform subdomains, because those records belong to the platform and you cannot change them.

Your login or access checks may be weak

On a live scan, xlogs reads the cookies your homepage response sets and flags session cookies missing Secure or HttpOnly - passively, from the response a normal visit already receives. With source access (the CLI), it also looks for missing auth checks on mutating routes and unverified tokens (JWT alg:none).

What else we observe, and do not grade

A scan sees more than it reports as findings. All of the below is read from responses the scan already fetched, none of it carries a severity, and none of it moves a verdict. It is here because a tool that shows you observations without saying which ones are judgements is inviting you to read the wrong ones as alarms.

The technologies you are built with

Framework, host, CDN, analytics, payments and the rest, read from response headers, page markup, asset paths and public DNS records. A technology is never a finding. Knowing you run Next.js on Vercel explains how to read a missing-header result; it is not itself a result.

Versions, but only where you publish them

We report a version only when the deployment states one itself, and we never map it to a vulnerability database. A version number in a header is rated a zero-severity disclosure by the standard taxonomy precisely because on its own it is useful mainly for choosing an exploit. Naming your stack is public information; guessing your exposure from a version string is not something we will do.

How the site is configured

Canonical, robots, language, share image, structured data, an app manifest, a registered service worker. Reported present or absent with what we looked for, never scored: whether a missing canonical matters depends on the site, and a grade would manufacture a judgement out of an observation.

Public identifiers in your markup

Analytics, tag manager and ad account ids are account-level, which means they are the same on every site using that account and could be used to correlate this site with others. We show you yours so you know what is visible. We do not search for other sites using them, and we will not build that.

What your deployment ships

The bytes actually served: the document, every same-site script we could download, and which are largest. Where something could not be weighed the total says so rather than quietly leaving it out. There is no performance score here, because whether a size is a problem depends on your app and your visitors and neither is visible from a scan.

Third-party hosts your page loads code from

Counted and named, never downloaded. Fetching someone else's CDN to measure it would turn a read-only scan into traffic against their infrastructure. Only the documented-compromised list produces an actual finding.

Libraries identifiable in your bundles

Names we can still recognise in shipped code. Bundlers strip most of them, so this is an incomplete list by construction and is never presented as a dependency inventory.

Other addresses on your domain, from public certificate logs

Certificate transparency logs are public by design and every certificate ever issued for your domain is in them. Names like staging or admin are worth knowing about because you may not have realised they were public. We read the log; we do not scan what we find there.

What we deliberately do not flag

"Severity must be earned" is easy to claim and worthless unless it is falsifiable. So here is the actual list of conditions our checks see and choose not to report, and why. If you think one of these is wrong, that is a specific, checkable disagreement, which is the point.

A public Supabase anon key in your bundle

It is designed to ship in the browser. On its own it is not a leak. We only flag a Supabase key if it decodes to the service_role key, which bypasses your access rules.

Any other JWT we happen to find

Session and anon tokens look like secrets and are not. We decode the payload and flag only what is genuinely privileged.

A page that returns 200 for /.env

Plenty of single-page apps answer 200 for every path. We require the response to actually look like an env file before calling it exposed. Each of the six files we request has its own shape check.

An admin or dashboard page that loads

A 200 at /admin almost always means a login page, which is correct and safe. We cannot tell an exposed admin panel from a login form without interpreting the page, and guessing would manufacture alarms. So we do not probe those paths at all until the check can be made evidence-based.

A .map URL that resolves but is not a source map

Same reason. We parse the body and require real source-map structure.

Short strings that merely look like API keys

Our key patterns are deliberately narrow. A loose pattern once matched minified CSS class names and produced nine false criticals on a major site, so we tightened it rather than keep the extra coverage.

An empty or access-controlled database table

A table that returns no rows to an anonymous request is doing its job. We report nothing.

A CNAME pointing at your hosting provider

That is what a normal custom domain looks like. We only report a DNS record when its target does not resolve at all, or the provider itself answers that nothing is claimed there. Subdomain-takeover checks are notorious for false alarms, so this one requires real evidence.

Missing email records on a platform subdomain

If your app lives at something like my-app.vercel.app, the SPF and DMARC records belong to the platform, not to you. Reporting them would be an alarm you cannot act on, so we skip the check and say why.

Other addresses found in certificate logs

We list them so you can spot a staging or admin site you forgot was public, but a certificate proves a name was issued, not that a host is live or insecure. We do not contact those addresses, so we do not report them as problems.

A missing DKIM record

DKIM lives at a selector name only you know, such as resend._domainkey. We cannot test it without guessing, and a guess that comes back empty would be a false alarm. So we check SPF and DMARC and leave DKIM alone.

Standard identifiers, and what they are not

Every finding carries a CWE identifier and its OWASP Top 10 (2021) category, so you can look it up or match it against another tool's output. You can also download any scan as SARIF 2.1.0, the standard format, and open it in VS Code or upload it to GitHub code scanning. That export is free and includes every finding, like everything else here.

Two honest caveats. One finding, subdomain takeover, has no CWE listed, because none of the existing identifiers fit it well and forcing a wrong one would be worse than leaving it blank. And these labels are a convenience, not a compliance product: an xlogs scan is not evidence for SOC 2, ISO 27001, or PCI DSS, and we will never present it as such.

The same checks run from a command line, so you can block a deploy on them: xlogs --url https://myapp.com --fail-on high exits non-zero if anything at or above that severity is found, and --sarif out.sarif writes the same report for CI. The gate is read-only like everything else: it blocks a deploy, it never touches your repository, and it needs no token with write access.

Confirmed versus potential

Every finding carries one of two labels, because "we saw it happen" and "this could be a problem" deserve different weight and we refuse to blur them.

Confirmed means xlogs directly observed the exposure: a database table returned real rows to a logged-out request, a private file was actually served, a source map validated, or a service key decoded. There is a response behind it, not an inference.

Potential means a missing protection or a risk we did not exploit to prove: an absent security header, a domain with no SPF record, a dangling DNS name that someone could claim. These are worth fixing, but xlogs did not (and will not) carry out the attack to confirm them, so it does not dress them up as confirmed.

Related: when a leaked key is visibly a live key rather than a test key, xlogs raises its severity, because the same kind of secret is not the same risk. A public test key is real exposure but a far smaller fire than a live one.

Every number here traces back to a response

Nothing on a report is estimated, modelled or averaged. Each finding names the request that produced it and what came back, each count is derived from the findings above it rather than stored separately, and the receipt lists the checks that ran so a zero can be read as measured rather than assumed. If we cannot show you where a number came from, we do not print the number.

What we refuse to invent

A security tool has a standing incentive to make results look more dramatic, because a frightening report feels more valuable than a quiet one. These are the four places we refuse it.

  • Absence is reported as absence. When a check could not complete, it says so and the result is not clean. No evidence is not negative evidence, and it is not positive evidence either.
  • We never guess a vulnerability from a design pattern. Recognising a framework tells us what to check, never what is wrong. A finding needs an observation behind it.
  • We never report a version we did not see. Guessing a library version from a filename, then matching it against advisories, produces a long and confident list of things that may not be true.
  • We do not exploit anything to make a finding stronger. Where a scan stops is where the evidence stops, and the confidence label says which side of that line a finding sits on.

Why there is no reviewer noise here

A finding appears only when the evidence can be shown. That is not a quality target we are trying to hit, it is a property of a scanner with no language model in it: every check is a deterministic rule over a response we fetched, so there is no mechanism by which one could produce a plausible-sounding finding about something it did not observe.

Tools that generate findings with a language model carry the opposite property, and independent review of one popular code reviewer found roughly a quarter of its comments were noise or incorrect. The cost of that is not the wasted minutes; it is that people stop reading. A scanner is only useful for as long as its output is worth opening.

Why there is no security score

A single number out of 100 has to be tuned to look meaningful, and the tuning is where honesty goes. Once a score exists, there is pressure to make it move: adding checks that are easy to pass, or weighting cosmetic items so the number spreads nicely across users. Several tools in this category include performance and accessibility checks inside a security score.

We report findings instead. Each one states what we observed, where, and what an outsider could do with it. A database that hands your rows to a stranger is critical whether or not the rest of your configuration is tidy, and no average should be able to soften that.

What we publish instead of a score is how often we are wrong: the false-positive benchmark runs two adversarial corpora through the shipped scanner on every change and lists the misses by name.

What xlogs cannot see

A clean xlogs scan means the checks above found nothing, not that your app is secure. Here is what is outside our reach, and who to use instead.

Your source code

The web scanner only sees your deployed site. It never asks for repository access, and it cannot find issues that are only visible in source, such as SQL injection or unsafe HTML rendering.

Dependency and CVE scanning from a LIVE response

The web scanner never guesses a version from a response header and maps it to a CVE: banner-derived version matches are wrong more often than right. The repo scanner is different, because a lockfile states exact versions: point it at a public repo and pinned npm dependencies are checked against the OSV advisory database. Continuous dependency monitoring is still a job for Snyk, Dependabot or Aikido.

Compliance evidence

An xlogs scan is not a SOC 2, ISO 27001 or PCI DSS artifact and must not be presented as one.

Anything behind a login

We never log in, so we only see what a logged-out visitor sees.

Cloud, container and infrastructure posture

Out of scope entirely. That is what an enterprise vulnerability-management platform is for.

Firebase security rules

They are evaluated inside Firebase. We can check your keys and headers, but not those rules.

If you need dependency scanning, compliance evidence, or coverage across cloud and infrastructure, you want a full application-security platform, not this. xlogs answers one question well: is your deployed app leaking something right now, and how do you close it.

Muting a finding

You can mark a finding as reviewed, with a reason: not a real issue, risk accepted, or handled another way. That note lives in your own browser and is sent nowhere, because there is no account to attach it to.

A muted finding stays in the report, dimmed, with your reason shown, and it does not change the verdict or the counts. Muting records that you looked at something and decided. It is not a way to make a problem disappear, and we will not build one.

Privacy, and the one thing we do keep

We read only what your app already serves publicly. For the database check we record the table name, its column names, and an approximate row count as proof of exposure, never the contents of your rows. There is no account, and we do not store your scans.

We do keep an anonymous tally, so we can publish the defect index: per scan we increment counters for the month, which builder platform the app appears to use, which check ran, and whether it found something, plus a coarse bucket for how long the scan took. That is the entire list.

No URL, no hostname, no IP, no finding detail, no data from your app. There is no identifier of any kind in those counters, so nothing in them can be traced back to you. We publish counts only and will never publish a list of scanned or affected sites. The open-source command line tool sends none of this: counting happens only here. Full statement in the privacy policy.

Run it on your app

Free, read-only, no signup. Everything on this page is what will happen.