Security & vulnerability testing
Three of the chapters in this pillar ask a different question about the same thing — the response your test already fetched. What does it say about itself? Can somebody else fetch it? What does the endpoint behind it do when the input is not what it expected? A fourth finds responses your suite never asked for, and a fifth is about what happens to everything the first four raise.
None of it is a separate tool or a separate run. Every one of these is an assertion inside an
ordinary test, evaluated by tflw run alongside your functional suite, and reported in the same
report.html.
What each chapter answers
Section titled “What each chapter answers”| chapter | the question it answers |
|---|---|
| Hygiene scanning | what does this response say about itself? |
| Authorization testing | can somebody who should not have it fetch this? |
| Input-handling testing | what does the endpoint do with input it did not expect? |
| Crawling an undocumented surface | what about the routes nobody wrote a test for? |
| Findings, baselines & the gate | a finding exists — now what? |
Read them in that order. The first three are the matchers, the fourth is a different way of producing responses for those same matchers to judge, and the fifth is the machinery all four feed.
Three questions, one response
Section titled “Three questions, one response”The first three chapters each teach their matcher alone. Together they are three lines:
test "an order is safe, private, and hard to confuse" as shopper api GET /orders capture body[0].id as orderId
api GET /orders/{orderId} expect response has no security violations expect response has no authorization violations expect response has no input handling violationsThe subject is the same word three times, and it means the same thing three times — the response
the api step above actually received. What changes is what each matcher does with it: one reads
the response, one re-sends the request as a different principal, one re-sends it with the payload
altered. Their chapters are where those differences live, and the differences matter enough that
folding the three into one matcher was rejected.
Note the ordinary two-step setup above them. A scan is not pointed at a URL from a config file; it judges a request your suite made, under an identity your suite chose, against a resource your suite knows the owner of. That is the whole reason these live in a test rather than in a scanner.
What a green scan does not claim
Section titled “What a green scan does not claim”Every scanner anyone has switched off was switched off for one of two reasons: it cried wolf, or it said "clean" about something it never looked at. The four scans in this pillar are built against both, and the way they get there is the same in all four — so it is stated once, here, rather than four times.
A finding is a difference this run caused, measured against a baseline this run observed. Not a pattern seen somewhere in a response — several rules do read patterns, and on their own a pattern is not evidence of anything. What makes it a finding is that it was not there before the scan acted: the input-handling rules subtract the control response's own hits, so an app that prints a stack trace on its happy path reports nothing; authorization compares what the owner got against what a stranger got, rather than judging one response alone. And a rule whose precondition is unmet reports not applicable rather than guessing — which is why a scan's output is mostly arithmetic, and why those counts print on the passing line too. A green assertion that will not say how much it checked is a green assertion you cannot use.
And the states that are not answers are never rendered as clean. Each scan has a small set of outcomes per thing it examined, and only some of them mean judged:
- not applicable — the rule's precondition was not met. Nothing to say, and nothing claimed.
- not probed — the scan was permitted to look and did not, most often because a
probe …sub-clause the target needed is absent. It counts as not clean. - inconclusive — the scan looked and the answer does not settle the question: a
429, a refusal that came from something in front of the code being asked about. Also not clean.
What differs between the chapters is which outcomes exist and what falls into each — and the
sharpest case is a 5xx. To input-handling testing
it is a first-class answer, because the application did process the payload, which is the thing
being asked; to authorization testing it is
inconclusive, because a crash tells you nothing about who may read what. Each chapter carries its
own table, and reading it is the difference between using a number and believing one.
A crawl adds an identity on top of that: discovered = withheld + sent, with every route in
discovered - reached named in the report with the reason. A crawler that quietly dropped the
routes it could not build would report a smaller denominator and look like better coverage —
see everything discovered is accounted
for.
The through-line is that none of these are the scanner being modest. Each is a place where the comfortable output and the true one differ, and the true one ships.
The order you adopt this in
Section titled “The order you adopt this in”Each chapter is written to be read alone, so the sequence across them is easy to miss. It is four
steps, and skipping the first is a TF060 and skipping the last is why scanners get switched off:
- Declare what you are allowed to scan. One
authorized targetline with a written reason, intflw.config— see Config & environments for where it goes and hygiene scanning for what it affirms. - Add one matcher to one test and read what comes back, including the counts on the passing line. A green scan that says how much it checked is the point of the format.
- Record what is already there.
--baseline-writeon the first run against an existing application, so today's findings do not have to be fixed today — Findings, baselines & the gate. - Turn it into a gate.
--fail-on, the exit code and, if you use GitHub code scanning,findings.sarif— the same chapter, then CI, reporting & safety.
Step 3 is the one people skip, and it is the one that decides whether any of this is still running in a month.
What tflw itself does, and does not
Section titled “What tflw itself does, and does not”A tool that scans other people's software should say what it does on yours. Five things, each of them a claim you can check:
- No telemetry. Nothing tflw runs reports anything to anyone — no usage collection, no update check, no crash reporting.
- No network beyond the targets you name. A run talks to the hosts your config and your tests
name, and to nothing else;
allow hostsintflw.configrefuses anything outside the list before a request is made.tflw install-browsersis the one command that downloads anything, and it says so. - A
useloads from where you said. The JS/TS escape hatch is arbitrary code, sotflw.config'shelpersdirective names the directories it may come from —./helpersand./tests/helperswhen you declare none. Ausethat resolves anywhere else isTF083fromtflw checkand from the editor, and the run does not start.tflw checkprints one line per module a run would load, so a review sees the suite's code without opening every file, andtflw run --no-helpersrefuses everyusefor that run. See Config. - The page is yours.
tflw uibinds to loopback, requires a per-start token that is in the URL it prints and nowhere else, checksHostandOriginon every request, takes JSON bodies only and caps them, runs only files inside the project, and serves every document under a Content-Security-Policy withX-Content-Type-Options: nosniffandReferrer-Policy: no-referrer— the same headers its ownsec/*rules ask of anyone else's page, and the page suite grades them with those rules. Reaching a remote page isssh -L; the token travels in the URL you paste. See What the page will not do. - Secrets stay out of the artefacts. Values resolved from environment variables are redacted from every file a run writes and from the console.
Supported versions. tflw is pre-1.0: the latest release is the only one that receives fixes,
on Node 22 and Node 24, which are the two versions CI tests on. Vulnerability reports go through
GitHub's private reporting on the repository and are acknowledged within 72 hours — the policy is
SECURITY.md.
Where to go next
Section titled “Where to go next”- Start here: Hygiene scanning — the smallest thing that works, and the chapter that introduces the declaration all four scans need.
- The highest-value scan: Authorization testing. It is the one class of flaw a scanner cannot find on its own, because only your suite knows who owns what.
- A surface with no test coverage: Crawling an undocumented surface.
- The other pillars: Functional testing and Performance testing.
The same work, in the page
Section titled “The same work, in the page”tflw ui's Run tab lists every finding a run produced, grouped by rule and by severity, with
the baseline's verdict beside it — which is how a finding that was accepted last week and is
gating now becomes visible. The authorized target declaration is still required and still written
in the file: the page does not grant permission, it reads it. See The page.