How we test

255 tests, and you can read every one.

Accuracy is the only thing that makes a scanner worth running. This is the whole list, pulled straight from the test suite — not a summary of it.

Roughly half of these exist to stop ShipShape reporting something that is fine. That is the harder half. A scanner that flags everything is easy to write and useless to own, so most of the work below is a fixture that looks like a vulnerability and must come back clean.

Every one runs on every commit. If any fails, nothing ships.

Sessions, billing and affiliate fraud

Includes forgery, tampering and replay against the payment webhook — a public URL that grants paid access, so it must not be forgeable.

24 tests · accounts.test.ts

  • a session token round-trips and carries the account id
  • a tampered session token is rejected
  • an expired session is rejected
  • a malformed session token never throws
  • email hashing is stable, case-insensitive, and not reversible to the address
  • a signed-out visitor gets the free instant check and nothing that needs identity
  • the FREE plan is not crippled — a full scan on your own site is included
  • paid plans widen the domain limit and unlock the artifacts
  • a LAPSED paid plan falls back to free rather than staying paid
  • [webhook] a correctly signed request is accepted
  • [webhook] AN UNSIGNED REQUEST IS REJECTED — this is the free-subscription hole
  • [webhook] a request signed with the WRONG secret is rejected
  • [webhook] a TAMPERED body invalidates a real signature
  • [webhook] a REPLAYED request outside the tolerance is rejected
  • [webhook] a missing secret fails closed
  • [webhook] subscription events map to a plan and an expiry
  • [webhook] a DELETED subscription drops the account to free
  • [webhook] an unknown price never grants a paid plan
  • Stripe reports itself unconfigured when it is
  • referral codes are validated, not sanitised
  • [fraud] self-referral is blocked
  • [fraud] no payout without tax paperwork, however large
  • [fraud] commission stops after the stated window and is held before payable
  • the published affiliate terms are internally consistent

The scanner is not cheating against its own fixture

Every route in the test target is randomised and the scan must still find everything, purely by crawling.

2 tests · antiHardcoding.test.ts

  • scanner finds all classes even when every route path is randomized
  • scanner source does not reference the sample target's default route names

What the documents are allowed to say

The scan record and questionnaire packet are forwarded to other people's security teams. Seven words are banned from them outright.

18 tests · artifacts.test.ts

  • [certificate] NONE of the forbidden words appear in what a reader sees
  • [packet] NONE of the forbidden words appear either
  • [certificate] says on its face that it is not a certification
  • [certificate] carries its own scan id, date and expiry on its face
  • [certificate] the expiry is the stated number of days after the scan
  • [certificate] lists what did NOT run, not only what passed
  • [certificate] an issue found is shown as an issue, not softened
  • [packet] answers the standard questionnaire rows
  • [packet] refuses to claim full OWASP Top 10 coverage
  • [packet] states the unauthenticated limitation as a limitation, not a pass
  • [packet] admits it is a self-assessment from a tool
  • [fixpack] one step per CONFIRMED finding, and none for unconfirmed ones
  • [fixpack] every step says WHO fixes it
  • [fixpack] every step has a repro, a prompt and a verification
  • [fixpack] prompts constrain the agent instead of saying 'fix the security issue'
  • [fixpack] a secret prompt insists on ROTATION, not just a code change
  • [fixpack] renders to markdown that pastes into an editor
  • [fixpack] a clean scan produces an empty pack that says so

authenticated scan

20 tests · authenticated-scan.test.ts

  • supabase auth sends BOTH apikey and authorization
  • bearer and cookie modes send what they say
  • a JWT's expiry is read from the token, not discovered by failure
  • an opaque (non-JWT) token reports no expiry rather than guessing
  • [refusal] an ALREADY-EXPIRED token is refused before the scan runs
  • [refusal] a token that CANNOT SURVIVE the scan is refused up front
  • a healthy token passes preflight
  • identical signed-in and signed-out responses mean the credential was IGNORED
  • a different status or a materially different body proves the session works
  • a trivial body difference is NOT treated as a session
  • an explicit signed-in marker is the strongest signal and is used when given
  • a failed authenticated request is not a live session
  • [the whole point] a session that DROPPED mid-scan never reads as covered
  • a credential that was never accepted says so plainly
  • only a session live at BOTH ends is described as covered
  • no credentials at all keeps the original wording
  • [engine] a PASSIVE scan refuses to send credentials at all
  • [engine] an unauthenticated full scan reports the surface as untested
  • [engine] a doomed token aborts the scan instead of producing a half-authenticated report
  • [engine] credentials that are ignored leave coverage.authenticated FALSE

Database exposure (Supabase and Firebase)

The check no mature competitor runs. Includes the privacy discipline: a fake person's name, email, phone and card are planted in a test database, and the build fails if any of them reach a finding or a report.

22 tests · backend-exposure.test.ts

  • [extract] finds the project, the anon key, and the tables in a Lovable bundle
  • [extract] probes with the ANON key even when a service_role key is also present
  • [extract] a bundle with no Supabase in it yields nothing
  • [TP] a table with no RLS is a CONFIRMED CRITICAL
  • [FP GUARD] a correctly-secured project produces NOTHING
  • [FP GUARD] a 401 from every table is a PASS, not a finding
  • one project produces ONE finding, however many tables are exposed
  • [PRIVACY] not one cell of the exposed data is recorded, anywhere
  • [PRIVACY] column NAMES are kept, because the owner needs to know what leaked
  • [PRIVACY] sensitive-looking columns are named so urgency is obvious
  • [PRIVACY] the read summariser never returns values, only shape
  • only limit=1 is ever requested
  • sensitiveColumns spots personal data and ignores ordinary columns
  • [extract] finds a Firebase project and its Realtime Database URL
  • [TP] a world-readable Firebase database is a confirmed critical
  • [FP GUARD] a locked-down Firebase database produces nothing
  • [FP GUARD] an empty Firebase database is not an exposure
  • no bundle means no backend probing at all
  • [wild] the CURRENT Supabase key format (sb_publishable_) is recognised
  • [wild] an sb_secret_ key is NEVER used to probe
  • [wild] a database we FOUND but could not TEST never reads as clean
  • [wild] no Supabase at all stays silent

One root cause, one finding

A single shared-template bug reflected across forty routes is one problem, not forty.

2 tests · dedupe-accuracy.test.ts

  • [1.3] one shared-template secret across two routes collapses to ONE finding with affectedLocations
  • [1.3] a singleton finding still carries affectedLocations with its own route

Outbound request safety

Byte caps, timeouts and method restrictions, so a scan cannot be turned into a denial-of-service against someone else.

5 tests · fetch-guard.test.ts

  • [fetch-guard] a literal loopback URL is refused at connect time
  • [fetch-guard] cloud metadata is refused at connect time
  • [fetch-guard] IPv6 loopback is refused at connect time
  • [fetch-guard] disallowed methods are rejected before any connection
  • [fetch-guard] the SSRF guard fires before any body is read from a loopback host

The demo target cannot drift

The deployed vulnerable demo and the in-repo fixture are byte-identical, so what we test is what we show.

1 test · fixture-sync.test.ts

  • [fixture-sync] the deployed demo target matches the CI fixture byte for byte

Precision and recall against a known-vulnerable fixture

Recall: did we catch every planted flaw. Precision: did we report anything that was not real. Both run on every commit.

7 tests · grading.test.ts

  • [grading] recall: every planted class is detected (confirmed or worth-a-look)
  • [grading] the LLM02 disclosure is a DISTINCT finding from the LLM01 injection
  • [grading] precision: NO confirmed finding is a false positive
  • [grading] NEAR-MISS: pure reflectors never CONFIRM prompt injection (no LLM there)
  • [grading] NEAR-MISS: the CSP-mitigated reflector is confirmed XSS but DOWN-RATED
  • [grading] the static secret page is tagged A05, NOT LLM02
  • [grading] the classes-tested table reports every planted class as found

Ownership verification

Active probes require proof you control the target, and the proof cannot be forged from public information.

9 tests · guards.test.ts

  • a target with ONLY safe routes produces no confirmed findings
  • the safe /health route reflects input escaped (not an XSS finding)
  • external target is refused without proof of control, and a token is issued
  • external scan throws (does not scan) when ownership is unproven
  • a bare domain (no scheme) is assumed to be https
  • a target that does not resolve returns an unreachable error, not a token
  • a bare external domain normalizes to https on ITS host, never joined to our origin
  • only the built-in demo target resolves against our own origin
  • built-in same-origin demo target is auto-verified

how we test

5 tests · how-we-test.test.ts

  • the inventory finds every test file in the suite
  • the count is real and substantial
  • every listed test name is a real string from a real file
  • the described groups explain WHY, not just what
  • an unknown new test file still appears rather than being silently dropped

integrations

27 tests · integrations.test.ts

  • [SSRF] a webhook URL cannot point at anything internal
  • [SSRF] plain HTTP is refused
  • a normal https webhook is accepted
  • garbage is rejected without throwing
  • [the muting problem] a persisting issue is NOT an alert
  • a NEW issue alerts
  • a FIXED issue alerts, because good news is also news
  • [silence is the enemy] a scan that could not run ALWAYS alerts
  • the first monitored scan alerts so the customer knows it is working
  • a failed scan is never phrased as a clean result
  • each destination gets the shape it actually expects
  • an alert names what was NOT tested rather than implying full coverage
  • an API key is stored only as a hash
  • the key prefix is recognisable so a leaked key gets caught
  • a VIEWER cannot start a scan
  • a MEMBER cannot change what the company pays
  • only an OWNER manages billing and members
  • an invited-but-not-accepted member has NO access at all
  • seats follow the plan
  • [audit] findings and secrets can never be written into the log
  • [dedupe] the same defect produces the same key across scans
  • [dedupe] different hosts and different classes get different keys
  • the dedupe key is in BOTH the title and the body
  • only CONFIRMED findings become tickets
  • a ticket carries severity, classification, evidence and the fix
  • Jira gets wiki markup, not Markdown
  • the pre-file search query is built per tracker

The policies match the code

Retention periods, the no-customer-data rule and the SSRF claim are each checked against what the software really does.

14 tests · legal-pages.test.ts

  • all three legal pages exist and are routable
  • [privacy] states the no-customer-data rule the backend probe enforces
  • [privacy] states the masking rule the passive tier enforces
  • [privacy] retention matches lib/scanStore.ts
  • [privacy] the LLM-egress claim matches writeup.ts
  • [terms] refuses to call a clean scan a certification
  • [terms] states the ownership requirement for active scanning
  • [terms] admits the scanner will miss things
  • [acceptable use] states the scan-only-what-you-own rule and the legal exposure
  • [acceptable use] states the SSRF guard that ipGuard.ts actually implements
  • [acceptable use] carries a responsible-disclosure promise with a stated response time
  • [acceptable use] tells finders not to demand money
  • every legal page admits it has not been lawyer-reviewed
  • the footer links all three

The claims on this website match the scanner

Every number on the site is asserted against a real scan. Change the scanner without changing the copy and the build fails.

2 tests · marketing-numbers.test.ts

  • the site's class counts match the scanner's actual registry
  • the retired claims do not come back

The free tier only sends what a browser sends

No payloads, no state changes, secret values masked, and no scan record for a site nobody has proved they own.

9 tests · passive-mode.test.ts

  • [passive] runs on an UNVERIFIED target without throwing
  • [passive] sends ONLY GET requests and no attack payloads
  • [passive] active classes are reported as skipped, never as 'not found'
  • [passive] secret VALUES are masked, but the finding is still reported
  • [full] a verified scan still shows the secret in full (owner ruling unchanged)
  • [passive] still finds real posture issues (this is what makes the free tier worth running)
  • [passive] earns NO badge — not in the result, not in the downloadable report
  • [passive] buildBadge THROWS rather than minting a passive badge
  • [full] a verified scan still earns the normal badge

Storage, monitoring and the monthly report

Including what happens when storage is unavailable: it must report failure, never claim a save that did not happen.

17 tests · persistence.test.ts

  • a scan id is 128 unguessable bits
  • only a well-formed id is ever looked up
  • with no storage configured, saving reports failure rather than pretending
  • a shareable link is offered for a VERIFIED scan and refused for a passive one
  • hydrate rebuilds the report and badge that storage deliberately drops
  • a stored PASSIVE scan still earns no badge when rehydrated
  • hosts are normalised so one site is one history
  • a manage token cannot be forged or reused across sites
  • the next run is one month out
  • obvious non-addresses are rejected, plausible ones are not
  • an address is never written to a log in full
  • email reports itself as unconfigured when it is
  • the diff names what appeared, what was fixed, and what is still open
  • the QUIET month still produces a subject worth opening
  • the monthly email leads with what was CHECKED and names the untested classes
  • an email with no score explains WHY there is none, instead of hiding it
  • the email never renders a score number without its coverage

platforms

10 tests · platforms.test.ts

  • every platform page names checks the scanner ACTUALLY runs
  • every platform page states what it does NOT cover
  • no platform page claims a partnership or an endorsement
  • the Lovable page describes the RLS failure accurately
  • [the whole point] every compliance page has a blunt 'what we do NOT do'
  • no compliance page claims to make anyone compliant or certified
  • the PCI page states outright that we are NOT an ASV
  • SOC 2 and ISO pages disclaim being an auditor or a certification body
  • each compliance page cites ONE specific control rather than gesturing at a standard
  • the questionnaire page is honest that it is a self-assessment

State-changing requests are off unless you say so

A POST to a discovered form on a live site can send email or create records. It never happens without explicit consent.

5 tests · post-consent.test.ts

  • [post-consent] a POST-only endpoint is NOT probed when POST probes are not authorized
  • [post-consent] a POST endpoint with a text param is also skipped without authorization
  • [post-consent] authorizing POST probes does send them
  • [post-consent] GET endpoints are still probed without POST authorization
  • [post-consent] the default context is closed (no explicit ctx == no POSTs)

End-to-end scanning

The whole engine, run against a deliberately vulnerable target.

4 tests · scanner.test.ts

  • finds all four seeded vulnerability classes, confirmed
  • every confirmed finding carries evidence, a plain-English why, and a fix
  • report and badge both carry the not-a-certification disclaimer
  • badge reflects the confirmed count

The posture score refuses to be a grade

It will not produce a number for a scan that could not test everything, because a score whose denominator moves is not comparable to anything.

12 tests · score.test.ts

  • [refusal] a PASSIVE scan gets no score at all
  • [refusal] an INCOMPLETE full scan gets no score
  • a clean full scan scores 100, with its coverage attached
  • a confirmed CRITICAL caps the score, whatever the rest of the arithmetic says
  • a confirmed HIGH caps below a clean score even on an otherwise tidy site
  • unconfirmed findings move the number a little, and are capped
  • the score never leaves 0..100
  • the denominator counts classes that actually RAN, not ones with nothing to test
  • the trend compares to this site's own previous month, and says so plainly
  • a real full scan of the vulnerable fixture scores badly and is capped
  • [guard] no UI renders the score number without its coverage string
  • scoreDisplay is one indivisible string

Credential detection, and what must NOT be flagged

Every pattern has a positive fixture and a near-miss negative. A Supabase anon key and a Firebase apiKey are public by design — flagging one would tell you your correct setup is a critical.

10 tests · secret-patterns.test.ts

  • [TP] Supabase service_role key is caught, by DECODING the role claim
  • [FP GUARD] Supabase ANON key is NOT a finding — it is designed to ship in the browser
  • [FP GUARD] a JWT with no role claim at all is not reported
  • [TP] OpenAI project, Anthropic, and legacy OpenAI keys are caught and named apart
  • [FP GUARD] a placeholder key from a tutorial or .env.example is not a finding
  • [TP] GitHub tokens are caught
  • [TP] a SendGrid key is caught
  • [FP GUARD] a Firebase web config is surfaced at info, NEVER as an exposed secret
  • [TP/FP] Stripe live, restricted and test keys are graded differently
  • [FP GUARD] ordinary bundle text does not trip any pattern

Secret detection against look-alikes

Structured data, documentation examples and placeholder keys must not be reported as live credentials.

6 tests · secrets-accuracy.test.ts

  • [1.1] schema.org JSON-LD page yields ZERO secret findings
  • [1.1] a raw https URL with no basic-auth is not a credentialed-URL finding
  • [1.1] AWS access key id ALONE is info/worth-a-look, never a confirmed critical
  • [1.1] AWS's own documentation example key is not reported at all
  • [1.1] a bare BEGIN-PRIVATE-KEY header with no body is NOT a confirmed secret
  • [1.1 TP] a real sk_live key and a real credentialed DB URL ARE confirmed

Single-page apps — the stack you actually build on

Lovable, Bolt, v0 and Replit all ship a bundle rather than pages. These grade both halves: finding the keys that are there, and staying silent on an app that is built correctly.

10 tests · spa-grading.test.ts

  • [SPA] the scanner OPENS the JavaScript bundle
  • [SPA recall] keys planted in the bundle are found
  • [SPA recall] a key that survives only in the SOURCE MAP is found
  • [SPA precision] a CORRECTLY configured SPA produces ZERO confirmed secrets
  • [SPA precision] the catch-all route does NOT become an exposed .env finding
  • [SPA recall] a genuinely exposed .env IS reported, without reprinting its contents
  • [SPA] the passive report says how much of the app it actually read
  • [SPA] a passive scan still masks the keys it found in the bundle
  • [SPA] a verified owner sees the full key value from the bundle
  • [wild] a JS chunk referenced ONLY by modulepreload is still read

Server-side request forgery guards

The scanner must refuse to be pointed at internal networks or cloud metadata, checked at the moment the socket opens rather than only when the address is typed.

5 tests · ssrf.test.ts

  • SSRF guard refuses ${label} (${target})
  • a public IP literal is NOT blocked by the SSRF guard (falls through to pending)
  • http:// is refused by default (https required)
  • token check requires an EXACT trimmed-body match, not substring
  • tokenForHost is deterministic per host+salt (re-scan finds the same token)

The verdict can never overstate a scan

A scan that could not finish must never read as a pass. Includes a source-level check that no part of the app derives a verdict from a finding count.

5 tests · verdict.test.ts

  • [regression] a FULL scan with a crashed probe is PARTIAL, never PASS
  • a clean FULL scan where every class ran is PASS
  • a PASSIVE scan is PARTIAL and names the classes it never tested
  • buildBadge refuses to mint a badge for a passive scan
  • no module outside engine.ts derives a verdict from a confirmed count

Cross-site scripting — true and false positives

A page that reflects your text but escapes it is not a vulnerability. These separate a real breakout from a harmless reflection.

4 tests · xss-accuracy.test.ts

  • [1.2] marker reflected into quoted-attr + JSON-LD + escaped title yields ZERO xss findings
  • [1.2] a reflected marker on a 404 error page is NOT confirmed
  • [1.2 TP] raw element-text reflection IS confirmed as XSS
  • [1.2 TP] quoted-attribute breakout (unescaped quote) IS confirmed

Counted from the test files themselves. A handful of tests are generated in loops and are not listed individually, so the real number is a little higher than the one at the top of this page — we would rather undercount than overstate.