quelltest rule pack, sanitisers and suppression directives
quell find parses each file with Babel and emits findings with a category or a rule id, a risk score, a confidence level and a stable id. This page lists every rule, what it flags, the sanitisers that stop a finding, and the comment directives that silence one. Findings are review hints, not proof of defects: they point at code that has no test executing it and that is a common place for bugs.
How are findings ranked?
Findings are sorted by risk (higher first), then by file and line. Each rule has a fixed base risk. Risk goes up by 1 when the file path or the enclosing function name matches auth, login, token, password, payment, pay, billing, charge, refund, delete, remove, admin, permission, role or secret. confidence (low, medium, high) reflects how precise the rule was on the bundled benchmark and on real code reviews; filter with --min-confidence.
One hop of data flow is tracked from req.body, req.query and req.params (also request.*): aliasing, spread, $set, destructuring, JSON.parse and new URL. Parameters that shadow a tainted name and reassignments to clean values stop the flow.
Which rules are security and robustness rules (category: risk)?
| Rule id | Flags | Risk | Confidence |
|---|---|---|---|
mass-assignment | The whole request body written to a document: Object.assign(doc, req.body), Model.create(req.body), { ...req.body }, { $set: req.body }, new Model(req.body); or a request value assigned to status, role, roles, isAdmin, isActive, permissions, plan or type. | 10 | high |
regex-injection | A RegExp, $regex value or .match/.search argument built from a non-literal that is not escaped. | 10 | medium |
nosql-injection | A request value used as an operand in a query (find, findOne, updateOne, deleteMany, countDocuments, ...), where it can be an operator object such as { "$ne": null }. | 10 | high |
path-traversal | A request value reaching fs.readFile/writeFile/unlink/readdir/rm (and sync or stream forms), res.sendFile, res.download, path.join or path.resolve. | 10 | high |
eval-input | eval, vm.runInNewContext, vm.runInThisContext or new Function with a non-literal argument. | 10 | high |
unguarded-call | jwt.verify, jsonwebtoken.verify, bcrypt.compare/hash or bcryptjs.compare/hash outside try/catch or an async wrapper; they throw on expired, malformed or non-string input. | 9 | high |
check-then-act | A function that looks up (exists, findOne, countDocuments) and then writes (create, save, insertOne) without handling a duplicate-key error (11000): racy under concurrent requests. | 9 | medium |
hard-coded-secret | A string literal of 8+ characters (no spaces) as the key of jwt.sign/verify or createHmac, or assigned to a name ending in secret, password, api_key, private_key, access_token or auth_token. Placeholders and route-like strings are ignored. | 9 | high |
open-redirect | A request value deciding the target of res.redirect or new URL(...). | 8 | medium |
missing-await | A promise-returning ORM call (save, create, updateOne, ...) used as a statement in an async function, or a field read off an un-awaited query result. Needs a Model-like receiver or an ORM mentioned in the module. | 8 | high |
unparsed-json | JSON.parse on request data outside try/catch. | 7 | high |
unguarded-network-call | fetch, axios, got, ky, http.get/request outside try/catch, a .catch chain or an async wrapper; asks for a timeout or signal when none is set. | 7 | medium |
unguarded-member-call | .map/.filter/.find/... called on a response-like value (data, res, response, result, payload, body, json) that is dereferenced without a guard or optional chaining. | 6 | medium |
unhandled-async-route | An async route handler (app.get(...), router.post(...)) with no try statement. | 5 | medium |
deep-response-chain | An indexed read or a four-level read on a response-like value (res.data.items[0].plan) without optional chaining. | 5 | medium |
falsy-fallback | || with a non-zero numeric default (pct || 10): 0 falls back too. Use ?? when 0 is valid. | 4 | medium |
Each rule's suggestion text tells you which test to write, for example path-traversal: test ../ and absolute paths and use path.basename or a base-directory check.
Which categories come from the code's own contracts?
| Category | Flags | Base risk | Confidence |
|---|---|---|---|
throws | A throw new ...Error(...). Suggestion includes the message so you assert it. | 6 | high |
schema | zod parse, safeParse, parseAsync and z.object in modules that import zod. | 5 | medium |
boundary | A relational or equality comparison in if, a ternary or while. Enum-style string comparisons are risk 2 and low confidence. | 4 | medium |
jsdoc | A function with @throws, @param or @example tags. | 3 | medium |
nullish | Optional chaining or a == null style check that is not already inside a guard or fallback. | 3 | low |
async | An async function that awaits. | 2 | low |
Which sanitisers does it recognise?
Which call neutralises a request value depends on the sink: String() stops operator objects but not ../.
| Sink | Calls that stop the finding |
|---|---|
Regular expressions (regex-injection) | Names containing escape or sanitiz (any case), or x.replace(/[...]/g, '\$&'). A sanitiser applied through a variable (const p = escapeRegex(q)) counts. |
NoSQL queries (nosql-injection) | String, Number, Boolean, parseInt, parseFloat, names containing escape or sanitiz, or ending in toString. |
File paths (path-traversal) | basename (as path.basename), names containing sanitiz, or names starting with validate. String() does not count. |
Everything else (redirects, JSON.parse, URLs) | String, Number, Boolean, parseInt, parseFloat, encodeURIComponent, encodeURI, basename, names containing escape or sanitiz, names starting with validate. |
A redirect is also accepted when the target has a fixed single-slash prefix ('/users/' + id) or was checked earlier in the same function with both startsWith('/') and startsWith('//'). A try block counts as handling a call only when the call is inside the try itself, not in catch, finally or a callback created inside it.
How do I suppress a finding?
| Directive | Where | Effect |
|---|---|---|
// quell-ignore [rule,...] | On the line of the finding or the line above | Suppresses findings there. With rule ids or categories it suppresses only those (// quell-ignore path-traversal). Counted in suppressed in the JSON report. |
// quell-ignore-file | Anywhere in a file | The scanner skips the whole file. |
// quell-ignore-mutant <reason> | On a source line or the line above | verify never mutates that line. Use it for equivalent mutants. It does not suppress scanner findings. |
// quell-allow-weaken <reason> | Inside a test | gate exempts that test. See the test gate. |
Whole files and folders can also be excluded with --exclude GLOB (alias --ignore) or a .quellignore file with one glob per line (# starts a comment). Patterns match paths relative to the scanned directory; --include GLOB restricts the scan. Always put a reason after quell-ignore-mutant and quell-allow-weaken so a reviewer can judge it.
How accurate is the rule pack?
On the bundled benchmark (npx quell eval), 11 of the 18 defects reported against a real Express and React code base are detected, which is every defect a line-level rule can see; the other 7 (wrong business rules, UI behaviour) are documented known misses. On 7 files of guarded code the risk rules produced no findings. The fixtures are written by the authors, so treat them as regression guards, not as an independent benchmark. See limits.