pgrls 0.17.0 — column-level grants + soundness/precision hardening
[0.17.0] - 2026-06-03
A soundness- and precision-hardening release: the output of a 20-round
adversarial repo-review loop (each finding independently verified before
acceptance), plus one new diff capability. No new rules; the Z3-backed
SEC038 verifier and the whole rule set are materially more trustworthy.
Added
- Column-level grants are now captured and diffed. Introspection
readspg_attribute.attacl, so aGRANT SELECT (col) … TO PUBLICon a
table with RLS off routes to the dangerousGRANT_PUBLIC_NO_RLSpath
(previously invisible — table-levelrelaclonly). Snapshot format
bumps to v15 (additive — the key is emitted only when present, so
pre-feature snapshots serialize byte-identically apart from the version
number); v3–v14 snapshots still load. pgrls diff --format json|sarifcarries the raw 4-way
classification (safe/requires_review/breaking/
dangerous) instead of collapsing it to three buckets.
Fixed
- Z3 / SEC038 soundness — abstain over fabricate. The 3VL anon-read
verifier and the diff counterexample emitter no longer mis-prove or
mis-emit on several shapes: a mismatched-sortCOALESCEfold (a bare
integer column defaulted to BoolSort and Z3 silently coerced it to
{0,1}, fabricating an "anonymous read leak" on a safe
COALESCE(level, 0) < 3gate); String-sorted arithmetic; never-NULL
current_setting/current_user/session_userleaves; and a
synthetic-marker name colliding with a real column. Uncertain shapes
degrade to no-fire / label-only rather than a false verdict. - Rule false positives eliminated on real-world-shaped policies:
SEC026 (auth value matched against a hard-coded literal /SIMILAR TO
/ ltree pattern), SEC033 (now requires the JSON chain to root in a
verified JWT source), SEC036 (a single-targetIN/= ANY (SELECT auth.uid())is recognized as a caller binding), SEC030, SEC024, and
SEC006 (a USING-only UPDATE is safe — Postgres reuses USING as the
implicit WITH CHECK). - No dangerous or un-runnable generated SQL. PERF004 abstains on a
non-IMMUTABLE index expression (incl. the STABLE 2-arglength(bytea, name)overload);generate --auth-functionrejects an ambiguous
multi-dot name; the SEC015 fixer quotes reserved-keyword search-path
tokens; andpgrls fix/Schema.to_sql()close latent
identifier-quoting and DDL-injection gaps. - SEC015 / SEC017 fixers emitted the wrong
ALTER FUNCTIONtarget
when a schema name contained a dot. Introspection now captures the
schema and function name as separate fields onSecdefFunction/
LeakproofFunctioninstead of splitting the ambiguous
nspname || '.' || pronamejoin; the fixers use them directly and
abstain when the fields are absent (a pre-v14 snapshot) rather than
guess a target. [database].urlenv interpolation is now lazy.pgrls diff
(snapshot-vs-snapshot) andpgrls explainno longer fail with exit 2
just because a[database].urlenv var referenced by an auto-loaded
pgrls.tomlis unset — the error is deferred to the commands that
actually open a connection.- Introspection determinism: the grant queries
SELECT DISTINCT, so
a privilege re-granted to the same role by two grantors no longer
duplicates in the snapshot.
Changed
- Model decode/emit hardening: empty-privilege grants are rejected at
snapshot decode;policy_to_sqlvalidates the policy command on emit
and the roles list on decode.