Skip to main content

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.

WhereFloorWhy
shipped defaulthighThe useful setting for a pipeline, which is where this runs.
local profilecriticalA local gate that blocks on a finding you are halfway through fixing is a gate people stop running.
ci profilehigh, pinnedPinned 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 profilehigh, pinnedSame 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:

AreaWhat it examines
SEC.AUTHAuthentication: host-based access rules, roles without a password, deprecated password hashes, wildcard hosts, a password literal in a migration.
SEC.PRIVPrivilege: 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.CFGServer 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.RLSRow-level security: disabled, no policy, a policy that is always true, FORCE not set, an unrestricted owner.
SEC.INJInjection: raw interpolation into SQL, a dynamic identifier, raw SQL with no stated reason.
SEC.PIIPrivacy: 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-undeterminedDo not let an undetermined move the exit code.
--strict-tools / --no-strict-toolsWhether a missing external tool is an error or a degradation.
--refresh-advisoriesFetch 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.