Case study · Our own tooling
A validated security sweep for production web apps, without a scanner bill
We built a repeatable security sweep for the web apps we run and maintain, and it reports only what it can prove: every finding comes with a file and line or a reproduced request.
At a glance
- Sector
- Our own tooling
- Duration
- Since September 2026, run on demand and before releases
- Our role
- Built the tooling and the triage procedure; we run it and deliver the findings
- Team
- TQM Invest, one engineer
- gitleaks
- trivy
- semgrep
- nuclei
- Docker
- Next.js
- Cloudflare
- Vercel
Headline results
- Three production web apps swept, code and live, each in under ten minutes of collection.
- Every reported finding is validated against the source or a live request before it is reported.
- Two of our own earlier findings corrected on re-run: one false alarm, one missed gap that is now fixed.
The situation
Small teams ship web apps without a security budget. Commercial scanners charge per target or per seat, and their reports mix real issues with long lists of findings that do not apply. The usual outcome is a report that goes unread.
We wanted something we could run before every release on the apps we look after: our own site and the client web apps we maintain. It had to be cheap enough to run often and strict enough that a finding in the report always means something.
What we did
A collector that does the same thing every time
The tools run in a disposable sandbox container that is created for the run and removed after it. A plain shell collector clones the repository, runs the scanners against the code and the live site, and writes the raw output into a dated run directory:
- gitleaks for committed secrets,
- trivy for known vulnerabilities in dependencies,
- semgrep for risky code patterns,
- an HTTP probe that records the response headers,
- exposure probes for files that should never be public, such as
.envor.git/config, - nuclei for known web issues,
- a screenshot of the homepage, to confirm the site rendered and shows no error page.
An earlier version let a model decide which tools to run. It was unreliable: runs skipped tools and produced different coverage from one week to the next. Splitting the work fixed that. The collector is deterministic, and judgment comes only after the data exists.
Triage that has to prove each finding
The raw output then goes through an AI triage step, validated by a human, with one rule: a finding is reported only with a file and line or a reproduced request. Scanner hits that fail the test are written down as discarded, with the reason. Typical discards are test keys in test files, a deliberate dummy password hash, and a dependency CVE in code the app never calls.
The review also covers what scanners miss. For Next.js apps that means every server action (is the user checked before any data access, and is the record scoped to them), the route protection file, payment webhooks (signature verified on the raw body, prices taken from the database) and any place the server fetches a URL it was given.
A report that credits what works
Each run produces a findings file and a short report: counts, the findings, the controls we verified as working, and what the run could not cover. The positive controls matter. A report that lists only problems gives no way to tell a careful app from one that was never checked.
Results go to a team channel on every run, including clean ones, so a missing message means the job broke. Findings go to the owner of each app. This page describes only findings that have been fixed.
Results
| September 2026 | October 2026 | |
|---|---|---|
| Apps swept, code and live | 3 | 3 |
| Known vulnerable dependencies (trivy) | some, triaged | 0 on all three |
| Findings without file and line or a reproduced request | 0 reported | 0 reported |
| Client database connection verifying the server certificate | No | Yes, pinned CA |
Between the two runs, the client app's runtime database connection moved from encrypted to encrypted and verified: it now checks the server certificate against a pinned certificate authority instead of trusting whatever answers.
Limits we state in every report
- nuclei returns little behind Cloudflare and Vercel, which throttle it. We record that instead of treating an empty result as clean.
- The sweep does not test signed-in flows. Access control is checked by reading the source.
Related case studies
An events and entertainment brand
Moving an events brand off WordPress, with one gated path to production
We moved an events and entertainment brand off WordPress onto one application whose only road to production is a test-and-audit gate, and resolved every legacy URL that search engines reported as missing.
One gated path to production since July 2026. Every production deploy passes a dependency audit and the unit suite; lint joined the gate on September 6, 2026.
- Next.js
- React
- TypeScript
- Prisma
- Postgres (Neon)
Our own product
A nightly batch pipeline that cannot report an empty run as success
We rebuilt our own nightly Docker pipeline on one small VM so that a run that delivers nothing fails with a written reason, and cut its runtime from 2h43m to under an hour.
Nightly runtime fell from 2h43m45s (91% of a three-hour cap) on September 2, 2026 to 54m59s (17% of a five-hour cap) on September 5, and all eleven nights from September 20 to 30 exited clean.
- Python
- Docker Compose
- systemd
- PostgreSQL (managed)
- Prisma
An event-ticketing platform
A multi-tenant ticketing platform: zero-downtime releases and a payment path tested against double charges
We build and run a multi-tenant ticketing platform whose largest release applied eight database migrations in under 20 seconds and served the old code on the new schema for about 2.5 minutes with zero errors.
Eight migrations in under 20 seconds in the September 2026 release, then about 2.5 minutes of old code on the new schema with zero errors and production answering 200 throughout, as observed in logs and monitoring.
- Next.js
- TypeScript
- PostgreSQL
- Prisma
- Stripe
Running into something like this?
Thirty minutes, no commitment. Bring the problem; we will tell you what we would check first and whether we are the right people for it.