The security suite
php artisan sqlens:security
sqlens:security examines a connection for security and privacy findings across every
suite. It is the one command whose findings are not gated by the strictness level, and
that difference is the reason it exists as its own front door rather than as a category
flag on sqlens:audit.
Why security has its own axis
The level 0–9 is an appetite: how much taste and rigor a project wants applied to its schema. A team can legitimately sit at level 0 forever.
A superuser role with no password is not a matter of appetite. So security and privacy rules ignore the level gate entirely and answer to a severity axis of their own:
info · low · medium · high · critical
A run blocks when a finding reaches the configured floor. That floor is
security.min_severity, and unlike almost everything else in this package it does not
ship at its most lenient value — it ships at high.
| Where | Floor | Why |
|---|---|---|
| shipped default | high | The useful setting for a pipeline, which is where this runs. |
local profile | critical | A local gate that blocks on a finding you are halfway through fixing is a gate people stop running. |
ci profile | high, pinned | Pinned even though it equals the default: a project that lowers its own floor while working a backlog must not silently get a pipeline that blocks on nothing. |
predeploy profile | high, pinned | Same reason, at the strictest moment of the day. |
Override per run with --min-severity=, including --min-severity=none to report
everything and block on nothing while you adopt.
What it looks at
Six areas, and each rule id names its own:
| Area | What it examines |
|---|---|
SEC.AUTH | Authentication: host-based access rules, roles without a password, deprecated password hashes, wildcard hosts, a password literal in a migration. |
SEC.PRIV | Privilege: GRANT ALL, grants to PUBLIC, WITH GRANT OPTION, server-admin and FILE grants, SUPERUSER / CREATEROLE / BYPASSRLS, SECURITY DEFINER routines with a mutable search path, DDL executed at runtime. |
SEC.CFG | Server configuration: TLS disabled or below a minimum version, transport not required, statement and general logging that captures personal data or secrets, secure_file_priv, local_infile, an end-of-life patch level. |
SEC.RLS | Row-level security: disabled, no policy, a policy that is always true, FORCE not set, an unrestricted owner. |
SEC.INJ | Injection: raw interpolation into SQL, a dynamic identifier, raw SQL with no stated reason. |
SEC.PII | Privacy: a column that holds personal data and is not encrypted. |
Each id maps to a page under rules/ that explains the hazard and what to do
about it.
Undetermined is the normal case on a managed database
A great deal of what this suite reads is privileged. On RDS, Cloud SQL, Neon or any other managed offering, some of it is simply not exposed to any role you can be given.
That produces undetermined findings, and they are not failures of the run — they are the
run telling you which questions it could not ask. The severity gate reports them as their
own number, beside the breach count rather than folded into it: a breach is something the
run found, an undetermined is something it could not look at, and on a managed database the
second is often the larger.
Two flags decide what that costs:
php artisan sqlens:security --profile=ci # strict: an undetermined fails the run
php artisan sqlens:security --allow-undetermined # report them, do not let them move the exit code
--allow-undetermined hides nothing. Every undetermined stays in the report, counted and
with its reason. It changes only whether the exit code moves.
See understanding undetermined for the three ways to
resolve one, and least privilege for the role that can
answer the most questions while still being read-only.
The reading role
The suite reads the catalog and server settings. It writes nothing, takes no lock of its own, and issues no statement outside catalog and state views.
You can point it at a dedicated connection so the reading role is not the one your application runs as:
'security' => [
'audit_connection' => 'sqlens_audit',
],
The audit role documents what that role needs to be granted — and, more usefully, what it must not be. A reader granted more than it needs is itself a finding this suite would report.
Advisories, and the one command that touches the network
The end-of-life data behind SEC.CFG.PATCH_EOL is shipped with the package and refreshed
explicitly, never as a side effect of a check:
php artisan sqlens:security --refresh-advisories
This is the only thing in the package that opens a network connection. It runs before the profile, the reporter and anything that connects to your database — refreshing is what you do because a check told you the data was old, so an unreachable database must not be the reason you cannot refresh it.
See advisory data for where the data comes from.
Flags
| Flag | |
|---|---|
--connection= | The connection to examine; the resolved default when omitted. |
--host= | The one read host to address when the connection configures a choice. It refuses a host the connection does not offer. |
--profile= | local, ci or predeploy. |
--min-severity= | info|low|medium|high|critical, or none to report without blocking. Beats the profile and the config. |
--allow-undetermined | Do not let an undetermined move the exit code. |
--strict-tools / --no-strict-tools | Whether a missing external tool is an error or a degradation. |
--refresh-advisories | Fetch the end-of-life data and write it where this package reads it. |
--format= | console, json, github, sarif or agent. |
--output= | Write the report to this file instead of stdout. |
Exit codes are the package-wide four.
Where security findings also appear
The suite is a front door, not a fence. Security rules run inside the other suites too —
sqlens:lint reports SEC.INJ and the migration-scoped SEC.PRIV and SEC.AUTH rules
against the migrations it is already reading, at any level.
sqlens:security is where you go to examine a database rather than a change to one.
Migrations that ship inside a package
security.include_vendor_migrations decides whether a migration living in vendor/ counts as one
of yours. It ships off.
// config/sqlens.php
'security' => ['include_vendor_migrations' => true],
Laravel really does run them — a package registers them with loadMigrationsFrom() and they execute
against your database like any other — so the switch is not about what they are. It is about whether
you can act on them. A critical finding inside vendor/ has one available fix, and it is not yours
to make.
Turn it on when the question is genuinely about the dependency tree: an audit that has to state what everything running against this database does, not only what your own team wrote. Expect findings you cannot fix, and read them as inventory rather than as a backlog.