@safegres/test 0.0.2 → 1.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +825 -412
- package/adapters.d.ts +8 -0
- package/adapters.js +18 -0
- package/ast/helpers.d.ts +16 -0
- package/ast/helpers.js +40 -0
- package/ast/parse.d.ts +31 -0
- package/ast/parse.js +87 -0
- package/ast/walk.d.ts +27 -0
- package/ast/walk.js +79 -0
- package/callgraph/baseline.d.ts +28 -0
- package/callgraph/baseline.js +55 -0
- package/callgraph/extract.d.ts +93 -0
- package/callgraph/extract.js +340 -0
- package/callgraph/graph.d.ts +82 -0
- package/callgraph/graph.js +276 -0
- package/checks/anti-patterns.d.ts +53 -0
- package/checks/anti-patterns.js +247 -0
- package/checks/column-grants.d.ts +20 -0
- package/checks/column-grants.js +74 -0
- package/checks/coverage.d.ts +43 -0
- package/checks/coverage.js +110 -0
- package/checks/definer-function.d.ts +91 -0
- package/checks/definer-function.js +458 -0
- package/checks/definer-view.d.ts +167 -0
- package/checks/definer-view.js +392 -0
- package/checks/indexes.d.ts +82 -0
- package/checks/indexes.js +263 -0
- package/checks/lattice.d.ts +179 -0
- package/checks/lattice.js +487 -0
- package/checks/object-acls.d.ts +32 -0
- package/checks/object-acls.js +129 -0
- package/checks/policy-index.d.ts +78 -0
- package/checks/policy-index.js +349 -0
- package/checks/revocable-grants.d.ts +140 -0
- package/checks/revocable-grants.js +377 -0
- package/checks/rls-flags.d.ts +25 -0
- package/checks/rls-flags.js +82 -0
- package/checks/role-reach.d.ts +329 -0
- package/checks/role-reach.js +277 -0
- package/checks/role-trust.d.ts +45 -0
- package/checks/role-trust.js +91 -0
- package/checks/set-role.d.ts +21 -0
- package/checks/set-role.js +66 -0
- package/checks/stats.d.ts +52 -0
- package/checks/stats.js +192 -0
- package/checks/view-exposure.d.ts +68 -0
- package/checks/view-exposure.js +236 -0
- package/checks/view-writes.d.ts +88 -0
- package/checks/view-writes.js +350 -0
- package/ci/baseline.d.ts +104 -0
- package/ci/baseline.js +111 -0
- package/ci/github.d.ts +47 -0
- package/ci/github.js +197 -0
- package/cli/audit.d.ts +3 -0
- package/cli/audit.js +560 -0
- package/cli/baseline.d.ts +3 -0
- package/cli/baseline.js +104 -0
- package/cli/commands.d.ts +2 -0
- package/cli/commands.js +60 -0
- package/cli/doctor.d.ts +3 -0
- package/cli/doctor.js +72 -0
- package/cli/eval.d.ts +3 -0
- package/cli/eval.js +153 -0
- package/cli/print-config.d.ts +3 -0
- package/cli/print-config.js +54 -0
- package/cli/shared.d.ts +43 -0
- package/cli/shared.js +153 -0
- package/cli.d.ts +2 -0
- package/cli.js +69 -0
- package/commands/audit.d.ts +65 -0
- package/commands/audit.js +779 -0
- package/commands/doctor.d.ts +19 -0
- package/commands/doctor.js +181 -0
- package/commands/eval.d.ts +67 -0
- package/commands/eval.js +93 -0
- package/config/fingerprint.d.ts +40 -0
- package/config/fingerprint.js +71 -0
- package/config/loader.d.ts +44 -0
- package/config/loader.js +210 -0
- package/config/presets.d.ts +95 -0
- package/config/presets.js +327 -0
- package/config/resolve.d.ts +47 -0
- package/config/resolve.js +192 -0
- package/config/schema.d.ts +54 -0
- package/config/schema.js +269 -0
- package/config/types.d.ts +562 -0
- package/config/validate.d.ts +14 -0
- package/config/validate.js +125 -0
- package/corpus/README.md +83 -0
- package/corpus/bootstrap.sql +14 -0
- package/corpus/cases/01-anon-write-grant/case.json +26 -0
- package/corpus/cases/01-anon-write-grant/schema.sql +19 -0
- package/corpus/cases/02-grant-to-public/case.json +25 -0
- package/corpus/cases/02-grant-to-public/schema.sql +18 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/case.json +30 -0
- package/corpus/cases/03-rls-off-reachable-by-anon/schema.sql +13 -0
- package/corpus/cases/04-permissive-write-policy/case.json +30 -0
- package/corpus/cases/04-permissive-write-policy/schema.sql +21 -0
- package/corpus/cases/05-public-read-policy/case.json +25 -0
- package/corpus/cases/05-public-read-policy/schema.sql +16 -0
- package/corpus/cases/06-update-without-with-check/case.json +25 -0
- package/corpus/cases/06-update-without-with-check/schema.sql +19 -0
- package/corpus/cases/07-rls-enabled-no-policies/case.json +29 -0
- package/corpus/cases/07-rls-enabled-no-policies/schema.sql +14 -0
- package/corpus/cases/08-select-grant-no-policy/case.json +25 -0
- package/corpus/cases/08-select-grant-no-policy/schema.sql +20 -0
- package/corpus/cases/09-write-grant-no-policy/case.json +24 -0
- package/corpus/cases/09-write-grant-no-policy/schema.sql +18 -0
- package/corpus/cases/10-rls-not-forced/case.json +24 -0
- package/corpus/cases/10-rls-not-forced/schema.sql +18 -0
- package/corpus/cases/11-session-user-in-policy/case.json +24 -0
- package/corpus/cases/11-session-user-in-policy/schema.sql +18 -0
- package/corpus/cases/12-volatile-function-in-policy/case.json +24 -0
- package/corpus/cases/12-volatile-function-in-policy/schema.sql +21 -0
- package/corpus/cases/13-security-definer-policy-wrapper/case.json +24 -0
- package/corpus/cases/13-security-definer-policy-wrapper/schema.sql +21 -0
- package/corpus/cases/14-dead-policy/case.json +24 -0
- package/corpus/cases/14-dead-policy/schema.sql +18 -0
- package/corpus/cases/15-unreachable-grant/case.json +24 -0
- package/corpus/cases/15-unreachable-grant/schema.sql +18 -0
- package/corpus/cases/16-anon-permissive-write-policy/case.json +34 -0
- package/corpus/cases/16-anon-permissive-write-policy/schema.sql +17 -0
- package/corpus/cases/17-foreign-key-without-index/case.json +24 -0
- package/corpus/cases/17-foreign-key-without-index/schema.sql +18 -0
- package/corpus/cases/18-policy-column-unindexed/case.json +24 -0
- package/corpus/cases/18-policy-column-unindexed/schema.sql +23 -0
- package/corpus/cases/19-policy-column-cast/case.json +24 -0
- package/corpus/cases/19-policy-column-cast/schema.sql +19 -0
- package/corpus/cases/20-stable-function-per-row/case.json +24 -0
- package/corpus/cases/20-stable-function-per-row/schema.sql +25 -0
- package/corpus/cases/21-table-without-primary-key/case.json +24 -0
- package/corpus/cases/21-table-without-primary-key/schema.sql +14 -0
- package/corpus/cases/22-redundant-index/case.json +24 -0
- package/corpus/cases/22-redundant-index/schema.sql +16 -0
- package/corpus/cases/23-tsvector-without-index/case.json +24 -0
- package/corpus/cases/23-tsvector-without-index/schema.sql +13 -0
- package/corpus/cases/24-anon-set-role-escalation/case.json +29 -0
- package/corpus/cases/24-anon-set-role-escalation/schema.sql +30 -0
- package/corpus/cases/25-definer-view-bypass/case.json +31 -0
- package/corpus/cases/25-definer-view-bypass/schema.sql +36 -0
- package/corpus/cases/26-invoker-view-no-bypass/case.json +31 -0
- package/corpus/cases/26-invoker-view-no-bypass/schema.sql +44 -0
- package/corpus/cases/27-definer-view-write/case.json +36 -0
- package/corpus/cases/27-definer-view-write/schema.sql +40 -0
- package/corpus/cases/28-invoker-view-no-write/case.json +31 -0
- package/corpus/cases/28-invoker-view-no-write/schema.sql +37 -0
- package/corpus/cases/29-rewrite-rule-bypass/case.json +36 -0
- package/corpus/cases/29-rewrite-rule-bypass/schema.sql +52 -0
- package/corpus/cases/30-instead-nothing-rule/case.json +31 -0
- package/corpus/cases/30-instead-nothing-rule/schema.sql +44 -0
- package/corpus/cases/31-matview-snapshot/case.json +32 -0
- package/corpus/cases/31-matview-snapshot/schema.sql +36 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/case.json +30 -0
- package/corpus/cases/32-matview-no-rls-no-bypass/schema.sql +18 -0
- package/corpus/cases/33-leaky-filter-view/case.json +36 -0
- package/corpus/cases/33-leaky-filter-view/schema.sql +29 -0
- package/corpus/cases/34-barrier-view-no-leak/case.json +32 -0
- package/corpus/cases/34-barrier-view-no-leak/schema.sql +29 -0
- package/corpus/cases/35-column-grant-projection/case.json +27 -0
- package/corpus/cases/35-column-grant-projection/schema.sql +21 -0
- package/corpus/cases/35-policy-function-not-leakproof/case.json +24 -0
- package/corpus/cases/35-policy-function-not-leakproof/schema.sql +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/case.json +27 -0
- package/corpus/cases/36-column-grant-rls-mediated/schema.sql +32 -0
- package/corpus/cases/36-policy-function-leakproof/case.json +24 -0
- package/corpus/cases/36-policy-function-leakproof/schema.sql +22 -0
- package/corpus/cases/37-sort-column-unindexed/case.json +24 -0
- package/corpus/cases/37-sort-column-unindexed/schema.sql +14 -0
- package/corpus/cases/37-unaudited-base-relation/case.json +30 -0
- package/corpus/cases/37-unaudited-base-relation/schema.sql +45 -0
- package/corpus/cases/38-foreign-key-with-index/case.json +22 -0
- package/corpus/cases/38-foreign-key-with-index/schema.sql +17 -0
- package/corpus/cases/38-invoker-view-unaudited-base/case.json +30 -0
- package/corpus/cases/38-invoker-view-unaudited-base/schema.sql +39 -0
- package/corpus/cases/39-definer-view-column-subset/case.json +30 -0
- package/corpus/cases/39-definer-view-column-subset/schema.sql +29 -0
- package/corpus/cases/39-policy-cast-with-expression-index/case.json +23 -0
- package/corpus/cases/39-policy-cast-with-expression-index/schema.sql +19 -0
- package/corpus/cases/40-definer-view-column-overreach/case.json +27 -0
- package/corpus/cases/40-definer-view-column-overreach/schema.sql +29 -0
- package/corpus/cases/40-stable-function-hoisted/case.json +23 -0
- package/corpus/cases/40-stable-function-hoisted/schema.sql +25 -0
- package/corpus/cases/41-index-prefix-not-redundant/case.json +21 -0
- package/corpus/cases/41-index-prefix-not-redundant/schema.sql +15 -0
- package/corpus/cases/41-unreadable-view-body/case.json +33 -0
- package/corpus/cases/41-unreadable-view-body/schema.sql +28 -0
- package/corpus/cases/42-invoker-view-unreadable-body/case.json +30 -0
- package/corpus/cases/42-invoker-view-unreadable-body/schema.sql +27 -0
- package/corpus/cases/42-search-column-indexed/case.json +21 -0
- package/corpus/cases/42-search-column-indexed/schema.sql +12 -0
- package/corpus/cases/43-anon-sequence-grant/case.json +27 -0
- package/corpus/cases/43-anon-sequence-grant/schema.sql +23 -0
- package/corpus/cases/44-serial-sequence-usage/case.json +27 -0
- package/corpus/cases/44-serial-sequence-usage/schema.sql +25 -0
- package/corpus/cases/45-anon-foreign-table/case.json +27 -0
- package/corpus/cases/45-anon-foreign-table/schema.sql +23 -0
- package/corpus/cases/46-unchecked-writable-view/case.json +41 -0
- package/corpus/cases/46-unchecked-writable-view/schema.sql +37 -0
- package/corpus/cases/47-checked-writable-view/case.json +37 -0
- package/corpus/cases/47-checked-writable-view/schema.sql +38 -0
- package/corpus/cases/48-definer-function-reach/case.json +38 -0
- package/corpus/cases/48-definer-function-reach/schema.sql +41 -0
- package/corpus/cases/49-invoker-function-no-reach/case.json +32 -0
- package/corpus/cases/49-invoker-function-no-reach/schema.sql +38 -0
- package/corpus/cases/50-instead-of-trigger-write/case.json +38 -0
- package/corpus/cases/50-instead-of-trigger-write/schema.sql +58 -0
- package/corpus/cases/51-invoker-trigger-no-write/case.json +33 -0
- package/corpus/cases/51-invoker-trigger-no-write/schema.sql +54 -0
- package/corpus/cases/52-revocable-execute-grant/case.json +30 -0
- package/corpus/cases/52-revocable-execute-grant/schema.sql +45 -0
- package/corpus/cases/53-policy-predicate-retains-execute/case.json +23 -0
- package/corpus/cases/53-policy-predicate-retains-execute/schema.sql +49 -0
- package/corpus/index.d.ts +79 -0
- package/corpus/index.js +125 -0
- package/esm/adapters.d.ts +8 -0
- package/esm/adapters.js +6 -0
- package/esm/ast/helpers.d.ts +16 -0
- package/esm/ast/helpers.js +36 -0
- package/esm/ast/parse.d.ts +31 -0
- package/esm/ast/parse.js +82 -0
- package/esm/ast/walk.d.ts +27 -0
- package/esm/ast/walk.js +72 -0
- package/esm/callgraph/baseline.d.ts +28 -0
- package/esm/callgraph/baseline.js +48 -0
- package/esm/callgraph/extract.d.ts +93 -0
- package/esm/callgraph/extract.js +333 -0
- package/esm/callgraph/graph.d.ts +82 -0
- package/esm/callgraph/graph.js +273 -0
- package/esm/checks/anti-patterns.d.ts +53 -0
- package/esm/checks/anti-patterns.js +240 -0
- package/esm/checks/column-grants.d.ts +20 -0
- package/esm/checks/column-grants.js +71 -0
- package/esm/checks/coverage.d.ts +43 -0
- package/esm/checks/coverage.js +104 -0
- package/esm/checks/definer-function.d.ts +91 -0
- package/esm/checks/definer-function.js +452 -0
- package/esm/checks/definer-view.d.ts +167 -0
- package/esm/checks/definer-view.js +382 -0
- package/esm/checks/indexes.d.ts +82 -0
- package/esm/checks/indexes.js +256 -0
- package/esm/checks/lattice.d.ts +179 -0
- package/esm/checks/lattice.js +475 -0
- package/esm/checks/object-acls.d.ts +32 -0
- package/esm/checks/object-acls.js +125 -0
- package/esm/checks/policy-index.d.ts +78 -0
- package/esm/checks/policy-index.js +342 -0
- package/esm/checks/revocable-grants.d.ts +140 -0
- package/esm/checks/revocable-grants.js +373 -0
- package/esm/checks/rls-flags.d.ts +25 -0
- package/esm/checks/rls-flags.js +77 -0
- package/esm/checks/role-reach.d.ts +329 -0
- package/esm/checks/role-reach.js +270 -0
- package/esm/checks/role-trust.d.ts +45 -0
- package/esm/checks/role-trust.js +86 -0
- package/esm/checks/set-role.d.ts +21 -0
- package/esm/checks/set-role.js +63 -0
- package/esm/checks/stats.d.ts +52 -0
- package/esm/checks/stats.js +184 -0
- package/esm/checks/view-exposure.d.ts +68 -0
- package/esm/checks/view-exposure.js +231 -0
- package/esm/checks/view-writes.d.ts +88 -0
- package/esm/checks/view-writes.js +344 -0
- package/esm/ci/baseline.d.ts +104 -0
- package/esm/ci/baseline.js +107 -0
- package/esm/ci/github.d.ts +47 -0
- package/esm/ci/github.js +161 -0
- package/esm/cli/audit.d.ts +3 -0
- package/esm/cli/audit.js +525 -0
- package/esm/cli/baseline.d.ts +3 -0
- package/esm/cli/baseline.js +102 -0
- package/esm/cli/commands.d.ts +2 -0
- package/esm/cli/commands.js +53 -0
- package/esm/cli/doctor.d.ts +3 -0
- package/esm/cli/doctor.js +67 -0
- package/esm/cli/eval.d.ts +3 -0
- package/esm/cli/eval.js +115 -0
- package/esm/cli/print-config.d.ts +3 -0
- package/esm/cli/print-config.js +52 -0
- package/esm/cli/shared.d.ts +43 -0
- package/esm/cli/shared.js +112 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +67 -0
- package/esm/commands/audit.d.ts +65 -0
- package/esm/commands/audit.js +776 -0
- package/esm/commands/doctor.d.ts +19 -0
- package/esm/commands/doctor.js +178 -0
- package/esm/commands/eval.d.ts +67 -0
- package/esm/commands/eval.js +90 -0
- package/esm/config/fingerprint.d.ts +40 -0
- package/esm/config/fingerprint.js +68 -0
- package/esm/config/loader.d.ts +44 -0
- package/esm/config/loader.js +172 -0
- package/esm/config/presets.d.ts +95 -0
- package/esm/config/presets.js +324 -0
- package/esm/config/resolve.d.ts +47 -0
- package/esm/config/resolve.js +180 -0
- package/esm/config/schema.d.ts +54 -0
- package/esm/config/schema.js +265 -0
- package/esm/config/types.d.ts +562 -0
- package/esm/config/validate.d.ts +14 -0
- package/esm/config/validate.js +122 -0
- package/esm/corpus/index.d.ts +79 -0
- package/esm/corpus/index.js +85 -0
- package/esm/exposure/adapters.d.ts +152 -0
- package/esm/exposure/adapters.js +463 -0
- package/esm/exposure/planes.d.ts +59 -0
- package/esm/exposure/planes.js +144 -0
- package/esm/exposure/reach.d.ts +78 -0
- package/esm/exposure/reach.js +105 -0
- package/esm/exposure/routines.d.ts +36 -0
- package/esm/exposure/routines.js +54 -0
- package/esm/index.d.ts +96 -0
- package/esm/index.js +57 -6
- package/esm/lint/index.d.ts +11 -0
- package/esm/lint/index.js +10 -0
- package/esm/perf/baseline.d.ts +40 -0
- package/esm/perf/baseline.js +83 -0
- package/esm/perf/explain.d.ts +46 -0
- package/esm/perf/explain.js +201 -0
- package/esm/pg/acl.d.ts +72 -0
- package/esm/pg/acl.js +186 -0
- package/esm/pg/behaviors.d.ts +114 -0
- package/esm/pg/behaviors.js +206 -0
- package/esm/pg/exposure.d.ts +74 -0
- package/esm/pg/exposure.js +198 -0
- package/esm/pg/functions.d.ts +52 -0
- package/esm/pg/functions.js +128 -0
- package/esm/pg/indexes.d.ts +192 -0
- package/esm/pg/indexes.js +395 -0
- package/esm/pg/introspect.d.ts +134 -0
- package/esm/pg/introspect.js +274 -0
- package/esm/pg/objects.d.ts +37 -0
- package/esm/pg/objects.js +108 -0
- package/esm/pg/paths.d.ts +114 -0
- package/esm/pg/paths.js +159 -0
- package/esm/pg/proc.d.ts +23 -0
- package/esm/pg/proc.js +55 -0
- package/esm/pg/reach-inputs.d.ts +47 -0
- package/esm/pg/reach-inputs.js +133 -0
- package/esm/pg/roles.d.ts +28 -0
- package/esm/pg/roles.js +50 -0
- package/esm/pg/stats.d.ts +65 -0
- package/esm/pg/stats.js +129 -0
- package/esm/pgpm-test.d.ts +22 -0
- package/esm/pgpm-test.js +39 -0
- package/esm/report/callgraph.d.ts +9 -0
- package/esm/report/callgraph.js +74 -0
- package/esm/report/compare.d.ts +143 -0
- package/esm/report/compare.js +195 -0
- package/esm/report/github.d.ts +74 -0
- package/esm/report/github.js +290 -0
- package/esm/report/json.d.ts +4 -0
- package/esm/report/json.js +3 -0
- package/esm/report/markdown.d.ts +25 -0
- package/esm/report/markdown.js +352 -0
- package/esm/report/pretty.d.ts +14 -0
- package/esm/report/pretty.js +318 -0
- package/esm/report/sarif.d.ts +40 -0
- package/esm/report/sarif.js +189 -0
- package/esm/report/view.d.ts +82 -0
- package/esm/report/view.js +126 -0
- package/esm/rules/registry.d.ts +66 -0
- package/esm/rules/registry.js +562 -0
- package/esm/score/score.d.ts +84 -0
- package/esm/score/score.js +233 -0
- package/esm/score/scorecards.d.ts +68 -0
- package/esm/score/scorecards.js +157 -0
- package/esm/types.d.ts +292 -0
- package/esm/types.js +26 -0
- package/esm/version.d.ts +8 -0
- package/esm/version.js +9 -0
- package/exposure/adapters.d.ts +152 -0
- package/exposure/adapters.js +468 -0
- package/exposure/planes.d.ts +59 -0
- package/exposure/planes.js +151 -0
- package/exposure/reach.d.ts +78 -0
- package/exposure/reach.js +109 -0
- package/exposure/routines.d.ts +36 -0
- package/exposure/routines.js +57 -0
- package/index.d.ts +96 -6
- package/index.js +229 -6
- package/lint/index.d.ts +11 -0
- package/lint/index.js +19 -0
- package/package.json +54 -48
- package/perf/baseline.d.ts +40 -0
- package/perf/baseline.js +92 -0
- package/perf/explain.d.ts +46 -0
- package/perf/explain.js +204 -0
- package/pg/acl.d.ts +72 -0
- package/pg/acl.js +191 -0
- package/pg/behaviors.d.ts +114 -0
- package/pg/behaviors.js +217 -0
- package/pg/exposure.d.ts +74 -0
- package/pg/exposure.js +205 -0
- package/pg/functions.d.ts +52 -0
- package/pg/functions.js +131 -0
- package/pg/indexes.d.ts +192 -0
- package/pg/indexes.js +400 -0
- package/pg/introspect.d.ts +134 -0
- package/pg/introspect.js +280 -0
- package/pg/objects.d.ts +37 -0
- package/pg/objects.js +111 -0
- package/pg/paths.d.ts +114 -0
- package/pg/paths.js +166 -0
- package/pg/proc.d.ts +23 -0
- package/pg/proc.js +58 -0
- package/pg/reach-inputs.d.ts +47 -0
- package/pg/reach-inputs.js +136 -0
- package/pg/roles.d.ts +28 -0
- package/pg/roles.js +54 -0
- package/pg/stats.d.ts +65 -0
- package/pg/stats.js +133 -0
- package/pgpm-test.d.ts +22 -0
- package/pgpm-test.js +42 -0
- package/report/callgraph.d.ts +9 -0
- package/report/callgraph.js +81 -0
- package/report/compare.d.ts +143 -0
- package/report/compare.js +205 -0
- package/report/github.d.ts +74 -0
- package/report/github.js +333 -0
- package/report/json.d.ts +4 -0
- package/report/json.js +6 -0
- package/report/markdown.d.ts +25 -0
- package/report/markdown.js +355 -0
- package/report/pretty.d.ts +14 -0
- package/report/pretty.js +324 -0
- package/report/sarif.d.ts +40 -0
- package/report/sarif.js +193 -0
- package/report/view.d.ts +82 -0
- package/report/view.js +132 -0
- package/rules/registry.d.ts +66 -0
- package/rules/registry.js +569 -0
- package/schema/safegres.schema.json +1119 -0
- package/score/score.d.ts +84 -0
- package/score/score.js +238 -0
- package/score/scorecards.d.ts +68 -0
- package/score/scorecards.js +162 -0
- package/types.d.ts +292 -0
- package/types.js +32 -0
- package/version.d.ts +8 -0
- package/version.js +12 -0
- package/LICENSE +0 -23
- package/admin.d.ts +0 -26
- package/admin.js +0 -182
- package/connect.d.ts +0 -19
- package/connect.js +0 -83
- package/dist/README.md +0 -557
- package/dist/package.json +0 -72
- package/esm/admin.js +0 -178
- package/esm/connect.js +0 -78
- package/esm/manager.js +0 -136
- package/esm/roles.js +0 -32
- package/esm/seed/adapters.js +0 -23
- package/esm/seed/csv.js +0 -44
- package/esm/seed/index.js +0 -16
- package/esm/seed/json.js +0 -18
- package/esm/seed/launchql.js +0 -19
- package/esm/seed/sqitch.js +0 -17
- package/esm/stream.js +0 -43
- package/esm/test-client.js +0 -150
- package/manager.d.ts +0 -25
- package/manager.js +0 -140
- package/roles.d.ts +0 -17
- package/roles.js +0 -38
- package/seed/adapters.d.ts +0 -4
- package/seed/adapters.js +0 -28
- package/seed/csv.d.ts +0 -9
- package/seed/csv.js +0 -49
- package/seed/index.d.ts +0 -16
- package/seed/index.js +0 -33
- package/seed/json.d.ts +0 -6
- package/seed/json.js +0 -21
- package/seed/launchql.d.ts +0 -2
- package/seed/launchql.js +0 -22
- package/seed/sqitch.d.ts +0 -2
- package/seed/sqitch.js +0 -20
- package/seed/types.d.ts +0 -13
- package/stream.d.ts +0 -2
- package/stream.js +0 -46
- package/test-client.d.ts +0 -49
- package/test-client.js +0 -154
- /package/{seed → config}/types.js +0 -0
- /package/esm/{seed → config}/types.js +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "One column past the caller's grant, and the same shape is a definer-view bypass",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_view_columns_wide"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L8",
|
|
19
|
+
"relation": "c_view_columns_wide.people",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "`email` is in no grant corpus_anon holds; the finding names the escaping columns, so the report says which ones"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"worstSeverity": "info",
|
|
25
|
+
"fix": "Narrow c_view_columns_wide.contacts to the columns the caller is entitled to, recreate it `WITH (security_invoker = true)`, or give it an owner whose reach matches what it is meant to expose. Do not revoke the column grants — they are what the narrow projection legitimately serves.",
|
|
26
|
+
"id": "40-definer-view-column-overreach"
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_view_columns_wide CASCADE;
|
|
2
|
+
CREATE SCHEMA c_view_columns_wide;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_view_columns_wide_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_view_columns_wide TO corpus_anon, corpus_user, c_view_columns_wide_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_view_columns_wide.people (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
handle text NOT NULL,
|
|
14
|
+
email text NOT NULL,
|
|
15
|
+
ssn text NOT NULL
|
|
16
|
+
);
|
|
17
|
+
ALTER TABLE c_view_columns_wide.people OWNER TO c_view_columns_wide_owner;
|
|
18
|
+
|
|
19
|
+
-- corpus_anon already reads these two columns, by column-level grant. No RLS,
|
|
20
|
+
-- so the owner's read returns exactly the rows corpus_anon's own read would.
|
|
21
|
+
GRANT SELECT (id, handle) ON c_view_columns_wide.people TO corpus_anon;
|
|
22
|
+
|
|
23
|
+
-- One column past the grant, and the view is a bypass: `email` escapes only
|
|
24
|
+
-- because the read runs as the owner. The projection is the whole difference
|
|
25
|
+
-- between this case and case 39.
|
|
26
|
+
CREATE VIEW c_view_columns_wide.contacts AS
|
|
27
|
+
SELECT id, handle, email FROM c_view_columns_wide.people;
|
|
28
|
+
ALTER VIEW c_view_columns_wide.contacts OWNER TO c_view_columns_wide_owner;
|
|
29
|
+
GRANT SELECT ON c_view_columns_wide.contacts TO corpus_anon;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A STABLE policy function wrapped in a scalar sub-select is hoisted",
|
|
3
|
+
"dimension": "perf",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_stable_hoisted"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [],
|
|
17
|
+
"forbid": [
|
|
18
|
+
"X2",
|
|
19
|
+
"X4",
|
|
20
|
+
"X9"
|
|
21
|
+
],
|
|
22
|
+
"fix": "Nothing to fix — this is case 20 fixed: the sub-select references no column, so the planner evaluates it once per query."
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_stable_hoisted CASCADE;
|
|
2
|
+
CREATE SCHEMA c_stable_hoisted;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_stable_hoisted TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
-- LEAKPROOF as well as STABLE: reading a GUC and casting it tells a caller
|
|
6
|
+
-- nothing about any row, and without the marking X4 would fire here on top of
|
|
7
|
+
-- the X9 this case is about.
|
|
8
|
+
CREATE FUNCTION c_stable_hoisted.current_tenant() RETURNS uuid
|
|
9
|
+
LANGUAGE sql STABLE LEAKPROOF AS $$
|
|
10
|
+
SELECT nullif(current_setting('jwt.claims.tenant_id', true), '')::uuid
|
|
11
|
+
$$;
|
|
12
|
+
|
|
13
|
+
CREATE TABLE c_stable_hoisted.ledger (
|
|
14
|
+
id bigserial PRIMARY KEY,
|
|
15
|
+
tenant_id uuid NOT NULL,
|
|
16
|
+
amount numeric
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX ledger_tenant_idx ON c_stable_hoisted.ledger (tenant_id);
|
|
19
|
+
ALTER TABLE c_stable_hoisted.ledger ENABLE ROW LEVEL SECURITY;
|
|
20
|
+
ALTER TABLE c_stable_hoisted.ledger FORCE ROW LEVEL SECURITY;
|
|
21
|
+
|
|
22
|
+
CREATE POLICY ledger_tenant ON c_stable_hoisted.ledger FOR SELECT TO corpus_user
|
|
23
|
+
USING (tenant_id = (SELECT c_stable_hoisted.current_tenant()));
|
|
24
|
+
|
|
25
|
+
GRANT SELECT ON c_stable_hoisted.ledger TO corpus_user;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A single-column index is not redundant with a composite that ends in it",
|
|
3
|
+
"dimension": "perf",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_index_not_redundant"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [],
|
|
17
|
+
"forbid": [
|
|
18
|
+
"X5"
|
|
19
|
+
],
|
|
20
|
+
"fix": "Nothing to fix — dropping either index costs a plan. X5 may only fire on a leading-column prefix, and (tenant_id) is not a prefix of (region, tenant_id)."
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_index_not_redundant CASCADE;
|
|
2
|
+
CREATE SCHEMA c_index_not_redundant;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_index_not_redundant TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_index_not_redundant.shipments (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
tenant_id uuid NOT NULL,
|
|
8
|
+
region text NOT NULL
|
|
9
|
+
);
|
|
10
|
+
-- Neither index covers the other: tenant_id leads the first and trails the
|
|
11
|
+
-- second, so only the first can serve a lookup by tenant alone.
|
|
12
|
+
CREATE INDEX shipments_tenant_idx ON c_index_not_redundant.shipments (tenant_id);
|
|
13
|
+
CREATE INDEX shipments_region_tenant_idx ON c_index_not_redundant.shipments (region, tenant_id);
|
|
14
|
+
|
|
15
|
+
GRANT SELECT ON c_index_not_redundant.shipments TO corpus_user;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A definer view whose body runs SQL of its own is reported as a gap, not scanned clean",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_unreadable_view"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L15",
|
|
19
|
+
"relation": "c_unreadable_view.activity",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "`query_to_xml` carries its query in a string, so the relations it reaches are unknowable here — the reach is proven and its far end is not"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"code": "L8",
|
|
25
|
+
"relation": "c_unreadable_view.ledger",
|
|
26
|
+
"role": "corpus_anon",
|
|
27
|
+
"note": "the half the analysis *could* read is still graded: taint suppresses nothing that was proven"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"worstSeverity": "info",
|
|
31
|
+
"fix": "Rewrite the body so the relations it reads are visible statically — a plain sub-select instead of `query_to_xml` — or satisfy yourself that what it returns is safe to serve corpus_anon. The gap is in what the audit could see, not proof of a leak, so nothing here justifies a revoke.",
|
|
32
|
+
"id": "41-unreadable-view-body"
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_unreadable_view CASCADE;
|
|
2
|
+
CREATE SCHEMA c_unreadable_view;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_unreadable_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_unreadable_view TO corpus_anon, corpus_user, c_unreadable_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_unreadable_view.ledger (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
subject text NOT NULL,
|
|
14
|
+
amount numeric NOT NULL
|
|
15
|
+
);
|
|
16
|
+
ALTER TABLE c_unreadable_view.ledger OWNER TO c_unreadable_owner;
|
|
17
|
+
|
|
18
|
+
-- A definer view that reads one relation the analysis can see and one it
|
|
19
|
+
-- cannot: `query_to_xml` takes its query as a *string*, so whatever it
|
|
20
|
+
-- touches is invisible to any AST walk. The ledger read is still graded (L8);
|
|
21
|
+
-- the part that cannot be followed is reported as a coverage gap (L15) rather
|
|
22
|
+
-- than being silently treated as "reaches nothing else".
|
|
23
|
+
CREATE VIEW c_unreadable_view.activity AS
|
|
24
|
+
SELECT l.id, l.subject,
|
|
25
|
+
query_to_xml('SELECT count(*) FROM c_unreadable_view.ledger', false, true, '') AS extra
|
|
26
|
+
FROM c_unreadable_view.ledger l;
|
|
27
|
+
ALTER VIEW c_unreadable_view.activity OWNER TO c_unreadable_owner;
|
|
28
|
+
GRANT SELECT ON c_unreadable_view.activity TO corpus_anon;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "An invoker view with the same unreadable body confers nothing, so there is no gap to report",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_invoker_unreadable"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A2",
|
|
19
|
+
"relation": "c_invoker_unreadable.ledger",
|
|
20
|
+
"note": "the real finding here, and unrelated to the view: a grant on a table with no RLS"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"forbid": [
|
|
24
|
+
"L15",
|
|
25
|
+
"L8"
|
|
26
|
+
],
|
|
27
|
+
"worstSeverity": "high",
|
|
28
|
+
"fix": "Enable RLS on c_invoker_unreadable.ledger. The view needs no fix: `security_invoker` means the body runs with the caller's own privileges, so an unreadable body confers nothing and reporting a coverage gap on it would make every view calling a SQL-executing function into noise.",
|
|
29
|
+
"id": "42-invoker-view-unreadable-body"
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_invoker_unreadable CASCADE;
|
|
2
|
+
CREATE SCHEMA c_invoker_unreadable;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_invoker_unreadable_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_invoker_unreadable TO corpus_anon, corpus_user, c_invoker_unreadable_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_invoker_unreadable.ledger (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
subject text NOT NULL
|
|
14
|
+
);
|
|
15
|
+
ALTER TABLE c_invoker_unreadable.ledger OWNER TO c_invoker_unreadable_owner;
|
|
16
|
+
GRANT SELECT ON c_invoker_unreadable.ledger TO corpus_anon;
|
|
17
|
+
|
|
18
|
+
-- The same unreadable body, with `security_invoker`. Nothing the body does
|
|
19
|
+
-- runs with privileges corpus_anon does not already have, so there is no
|
|
20
|
+
-- ungraded owner-privileged reach to report. An unreadable body is only a
|
|
21
|
+
-- coverage gap when the view confers something.
|
|
22
|
+
CREATE VIEW c_invoker_unreadable.activity WITH (security_invoker = true) AS
|
|
23
|
+
SELECT l.id,
|
|
24
|
+
query_to_xml('SELECT count(*) FROM c_invoker_unreadable.ledger', false, true, '') AS extra
|
|
25
|
+
FROM c_invoker_unreadable.ledger l;
|
|
26
|
+
ALTER VIEW c_invoker_unreadable.activity OWNER TO c_invoker_unreadable_owner;
|
|
27
|
+
GRANT SELECT ON c_invoker_unreadable.activity TO corpus_anon;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A tsvector column with a GIN index is not a finding",
|
|
3
|
+
"dimension": "perf",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_search_indexed"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [],
|
|
17
|
+
"forbid": [
|
|
18
|
+
"X7"
|
|
19
|
+
],
|
|
20
|
+
"fix": "Nothing to fix — this is case 23 fixed, and the index is the one full-text search can actually use."
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_search_indexed CASCADE;
|
|
2
|
+
CREATE SCHEMA c_search_indexed;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_search_indexed TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_search_indexed.articles (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
body text,
|
|
8
|
+
search_doc tsvector
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX articles_search_idx ON c_search_indexed.articles USING gin (search_doc);
|
|
11
|
+
|
|
12
|
+
GRANT SELECT ON c_search_indexed.articles TO corpus_user;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "An untrusted role holds privileges on a sequence, which no policy filters",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_seq_grant"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L16",
|
|
19
|
+
"relation": "c_seq_grant.invoice_number",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "USAGE is `nextval`/`setval` and SELECT is `last_value`; the table beside it is fully RLS-protected, and none of that protection reaches the sequence"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"worstSeverity": "info",
|
|
25
|
+
"fix": "Nothing here is row-filterable, so the question is whether corpus_anon needs the sequence at all. This one feeds no column (no `OWNED BY`), so no insert depends on it — but confirm a DEFAULT elsewhere does not name it before revoking. Where a serial column is involved, `GENERATED ... AS IDENTITY` removes the need for the grant instead of breaking the insert.",
|
|
26
|
+
"id": "43-anon-sequence-grant"
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_seq_grant CASCADE;
|
|
2
|
+
CREATE SCHEMA c_seq_grant;
|
|
3
|
+
|
|
4
|
+
GRANT USAGE ON SCHEMA c_seq_grant TO corpus_anon, corpus_user;
|
|
5
|
+
|
|
6
|
+
-- A free-standing sequence: nothing inserts through it, so the grant is not
|
|
7
|
+
-- load-bearing for any write path. USAGE lets corpus_anon burn the counter
|
|
8
|
+
-- and SELECT lets it read `last_value`. RLS has nothing to say about either:
|
|
9
|
+
-- a sequence cannot carry a policy.
|
|
10
|
+
CREATE SEQUENCE c_seq_grant.invoice_number;
|
|
11
|
+
GRANT USAGE, SELECT ON SEQUENCE c_seq_grant.invoice_number TO corpus_anon;
|
|
12
|
+
|
|
13
|
+
CREATE TABLE c_seq_grant.invoices (
|
|
14
|
+
id bigserial PRIMARY KEY,
|
|
15
|
+
subject text NOT NULL
|
|
16
|
+
);
|
|
17
|
+
ALTER TABLE c_seq_grant.invoices ENABLE ROW LEVEL SECURITY;
|
|
18
|
+
ALTER TABLE c_seq_grant.invoices FORCE ROW LEVEL SECURITY;
|
|
19
|
+
CREATE POLICY invoices_own ON c_seq_grant.invoices
|
|
20
|
+
FOR SELECT TO corpus_user
|
|
21
|
+
USING (subject = (SELECT current_setting('app.subject', true)));
|
|
22
|
+
CREATE INDEX invoices_subject_idx ON c_seq_grant.invoices (subject);
|
|
23
|
+
GRANT SELECT ON c_seq_grant.invoices TO corpus_user;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "The same grant on a sequence that backs a serial insert path",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_seq_serial"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L16",
|
|
19
|
+
"relation": "c_seq_serial.tickets_id_seq",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "reported with the column it feeds, which is what turns the advice from `REVOKE` into `AS IDENTITY`: a `serial` column cannot be written by any role without USAGE on its sequence"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"worstSeverity": "info",
|
|
25
|
+
"fix": "Establish whether corpus_anon writes c_seq_serial.tickets before touching this grant — a `serial` column cannot be inserted without USAGE on its sequence, so a revoke on the wrong assumption breaks the write path rather than closing a hole. Converting the column to `GENERATED ALWAYS AS IDENTITY` removes the need for any grant on the sequence, which is the fix that does not trade one break for another.",
|
|
26
|
+
"id": "44-serial-sequence-usage"
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_seq_serial CASCADE;
|
|
2
|
+
CREATE SCHEMA c_seq_serial;
|
|
3
|
+
|
|
4
|
+
GRANT USAGE ON SCHEMA c_seq_serial TO corpus_anon, corpus_user;
|
|
5
|
+
|
|
6
|
+
-- The load-bearing shape: `tickets.id` is a `serial`, so the sequence backs a
|
|
7
|
+
-- real insert path and USAGE on it is a prerequisite for writing the table,
|
|
8
|
+
-- not an ornament. corpus_anon holds that USAGE. Whether it is gratuitous or
|
|
9
|
+
-- required depends on a write path the ACL alone does not show, which is
|
|
10
|
+
-- exactly why the finding must carry the OWNED BY link and must not turn into
|
|
11
|
+
-- a revoke recommendation.
|
|
12
|
+
CREATE TABLE c_seq_serial.tickets (
|
|
13
|
+
id bigserial PRIMARY KEY,
|
|
14
|
+
subject text NOT NULL
|
|
15
|
+
);
|
|
16
|
+
ALTER TABLE c_seq_serial.tickets ENABLE ROW LEVEL SECURITY;
|
|
17
|
+
ALTER TABLE c_seq_serial.tickets FORCE ROW LEVEL SECURITY;
|
|
18
|
+
CREATE POLICY tickets_own ON c_seq_serial.tickets
|
|
19
|
+
FOR ALL TO corpus_user
|
|
20
|
+
USING (subject = (SELECT current_setting('app.subject', true)))
|
|
21
|
+
WITH CHECK (subject = (SELECT current_setting('app.subject', true)));
|
|
22
|
+
CREATE INDEX tickets_subject_idx ON c_seq_serial.tickets (subject);
|
|
23
|
+
|
|
24
|
+
GRANT SELECT, INSERT ON c_seq_serial.tickets TO corpus_user;
|
|
25
|
+
GRANT USAGE ON SEQUENCE c_seq_serial.tickets_id_seq TO corpus_anon;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A grant on a foreign table, where RLS is not merely absent but impossible",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_foreign_grant"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L17",
|
|
19
|
+
"relation": "c_foreign_grant.remote_customers",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "the table snapshot reads relkind 'r'/'p' and never saw this relation at all, so no rule graded the grant before"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"worstSeverity": "info",
|
|
25
|
+
"fix": "Expose the foreign table through a view that carries the filter and grant on the view instead, or push the filter to the remote side. Adding RLS is not an option here — Postgres refuses it on a foreign table — so the usual A2 remedy does not apply.",
|
|
26
|
+
"id": "45-anon-foreign-table"
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_foreign_grant CASCADE;
|
|
2
|
+
CREATE SCHEMA c_foreign_grant;
|
|
3
|
+
|
|
4
|
+
GRANT USAGE ON SCHEMA c_foreign_grant TO corpus_anon, corpus_user;
|
|
5
|
+
|
|
6
|
+
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
|
|
7
|
+
|
|
8
|
+
DO $$ BEGIN
|
|
9
|
+
CREATE SERVER c_foreign_grant_srv FOREIGN DATA WRAPPER postgres_fdw
|
|
10
|
+
OPTIONS (host 'localhost', dbname 'postgres');
|
|
11
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
12
|
+
END $$;
|
|
13
|
+
|
|
14
|
+
-- A foreign table cannot carry RLS: Postgres rejects
|
|
15
|
+
-- `ALTER FOREIGN TABLE ... ENABLE ROW LEVEL SECURITY` outright. So unlike the
|
|
16
|
+
-- A2 shape it resembles, the remedy "add a policy" does not exist, and every
|
|
17
|
+
-- row the remote side returns is returned to corpus_anon.
|
|
18
|
+
CREATE FOREIGN TABLE c_foreign_grant.remote_customers (
|
|
19
|
+
id int,
|
|
20
|
+
email text
|
|
21
|
+
) SERVER c_foreign_grant_srv OPTIONS (schema_name 'public', table_name 'customers');
|
|
22
|
+
|
|
23
|
+
GRANT SELECT ON c_foreign_grant.remote_customers TO corpus_anon;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A writable view's row filter is a read filter: no WITH CHECK OPTION, so writes escape it",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_unchecked_view_write"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L18",
|
|
19
|
+
"relation": "c_unchecked_view_write.notes",
|
|
20
|
+
"note": "the view filters on `tenant` and carries no WITH CHECK OPTION, so corpus_anon's INSERT/UPDATE through it stores rows for any tenant \u2014 rows the view will not then show it"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"code": "L9",
|
|
24
|
+
"relation": "c_unchecked_view_write.notes",
|
|
25
|
+
"note": "the same view is auto-updatable and not security_invoker, so the write reaches `notes` as c_unchecked_write_owner at all \u2014 L9 is *whether* the write lands, L18 is whether the view's own condition constrains it"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"code": "A3",
|
|
29
|
+
"relation": "c_unchecked_view_write.notes",
|
|
30
|
+
"note": "RLS is not FORCEd, which is why the owner's rewritten write is not filtered by the table's own policy either"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"forbid": [
|
|
34
|
+
"L8",
|
|
35
|
+
"L10",
|
|
36
|
+
"L11",
|
|
37
|
+
"L12"
|
|
38
|
+
],
|
|
39
|
+
"worstSeverity": "low",
|
|
40
|
+
"fix": "Recreate c_unchecked_view_write.tenant_notes WITH LOCAL CHECK OPTION so a row written through it must satisfy the condition it is served under, or put the tenant predicate in an RLS policy's WITH CHECK clause on `notes`, which applies however the row arrives. Do not revoke corpus_anon's INSERT on the view \u2014 that grant is what the API serves."
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_unchecked_view_write CASCADE;
|
|
2
|
+
CREATE SCHEMA c_unchecked_view_write;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_unchecked_write_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_unchecked_view_write TO corpus_anon, corpus_user, c_unchecked_write_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_unchecked_view_write.notes (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
tenant text NOT NULL,
|
|
14
|
+
body text NOT NULL
|
|
15
|
+
);
|
|
16
|
+
ALTER TABLE c_unchecked_view_write.notes OWNER TO c_unchecked_write_owner;
|
|
17
|
+
ALTER TABLE c_unchecked_view_write.notes ENABLE ROW LEVEL SECURITY;
|
|
18
|
+
|
|
19
|
+
CREATE POLICY notes_own_tenant ON c_unchecked_view_write.notes
|
|
20
|
+
FOR ALL TO corpus_user
|
|
21
|
+
USING (tenant = (SELECT current_setting('app.tenant', true)))
|
|
22
|
+
WITH CHECK (tenant = (SELECT current_setting('app.tenant', true)));
|
|
23
|
+
CREATE INDEX notes_tenant_idx ON c_unchecked_view_write.notes (tenant);
|
|
24
|
+
GRANT SELECT, INSERT ON c_unchecked_view_write.notes TO corpus_user;
|
|
25
|
+
GRANT USAGE ON SEQUENCE c_unchecked_view_write.notes_id_seq TO corpus_user;
|
|
26
|
+
|
|
27
|
+
-- The flaw: the view's `WHERE` decides which rows come *out* and nothing about
|
|
28
|
+
-- which rows go *in*. `WITH CHECK OPTION` is not the default, so an INSERT
|
|
29
|
+
-- through this view may name any tenant at all — the row lands in `notes`,
|
|
30
|
+
-- checked against the view's owner, and the writer cannot then see it. The
|
|
31
|
+
-- condition reads like a boundary and is not one.
|
|
32
|
+
CREATE VIEW c_unchecked_view_write.tenant_notes AS
|
|
33
|
+
SELECT id, tenant, body
|
|
34
|
+
FROM c_unchecked_view_write.notes
|
|
35
|
+
WHERE tenant = current_setting('app.tenant', true);
|
|
36
|
+
ALTER VIEW c_unchecked_view_write.tenant_notes OWNER TO c_unchecked_write_owner;
|
|
37
|
+
GRANT INSERT, UPDATE ON c_unchecked_view_write.tenant_notes TO corpus_anon;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "The same writable view WITH LOCAL CHECK OPTION constrains its writes, and L18 stays silent",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_checked_view_write"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L9",
|
|
19
|
+
"relation": "c_checked_view_write.notes",
|
|
20
|
+
"note": "the write still reaches `notes` as the view's owner \u2014 the check option constrains which rows may be written, not whose privileges write them"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"code": "A3",
|
|
24
|
+
"relation": "c_checked_view_write.notes",
|
|
25
|
+
"note": "RLS is still not FORCEd, unchanged from case 46"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"forbid": [
|
|
29
|
+
"L18",
|
|
30
|
+
"L8",
|
|
31
|
+
"L10",
|
|
32
|
+
"L11",
|
|
33
|
+
"L12"
|
|
34
|
+
],
|
|
35
|
+
"worstSeverity": "low",
|
|
36
|
+
"fix": "Nothing on the check option. This case is case 46 after its fix, and exists to pin that L18 stops firing once the view's condition governs writes as well as reads."
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_checked_view_write CASCADE;
|
|
2
|
+
CREATE SCHEMA c_checked_view_write;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_checked_write_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_checked_view_write TO corpus_anon, corpus_user, c_checked_write_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_checked_view_write.notes (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
tenant text NOT NULL,
|
|
14
|
+
body text NOT NULL
|
|
15
|
+
);
|
|
16
|
+
ALTER TABLE c_checked_view_write.notes OWNER TO c_checked_write_owner;
|
|
17
|
+
ALTER TABLE c_checked_view_write.notes ENABLE ROW LEVEL SECURITY;
|
|
18
|
+
|
|
19
|
+
CREATE POLICY notes_own_tenant ON c_checked_view_write.notes
|
|
20
|
+
FOR ALL TO corpus_user
|
|
21
|
+
USING (tenant = (SELECT current_setting('app.tenant', true)))
|
|
22
|
+
WITH CHECK (tenant = (SELECT current_setting('app.tenant', true)));
|
|
23
|
+
CREATE INDEX notes_tenant_idx ON c_checked_view_write.notes (tenant);
|
|
24
|
+
GRANT SELECT, INSERT ON c_checked_view_write.notes TO corpus_user;
|
|
25
|
+
GRANT USAGE ON SEQUENCE c_checked_view_write.notes_id_seq TO corpus_user;
|
|
26
|
+
|
|
27
|
+
-- Case 46 with the one clause that makes the filter apply to writes: a row
|
|
28
|
+
-- written through the view must satisfy the view's own condition. L18 must
|
|
29
|
+
-- stay silent. L9 still fires — the write does still land on `notes` as the
|
|
30
|
+
-- view's owner, which is what the view is for; the check option only settles
|
|
31
|
+
-- *which* rows may be written.
|
|
32
|
+
CREATE VIEW c_checked_view_write.tenant_notes AS
|
|
33
|
+
SELECT id, tenant, body
|
|
34
|
+
FROM c_checked_view_write.notes
|
|
35
|
+
WHERE tenant = current_setting('app.tenant', true)
|
|
36
|
+
WITH LOCAL CHECK OPTION;
|
|
37
|
+
ALTER VIEW c_checked_view_write.tenant_notes OWNER TO c_checked_write_owner;
|
|
38
|
+
GRANT INSERT, UPDATE ON c_checked_view_write.tenant_notes TO corpus_anon;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Anonymous role reads a locked table by executing a SECURITY DEFINER function",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_definer_fn"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L19",
|
|
19
|
+
"relation": "c_definer_fn.secrets",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "the body runs as c_definer_fn_owner, who owns `secrets`, so corpus_anon reads every row through EXECUTE alone — a privilege no ACL entry on the table confers on it"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"code": "A3",
|
|
25
|
+
"relation": "c_definer_fn.secrets",
|
|
26
|
+
"note": "RLS is not FORCEd, which is why the owner's read inside the body is not filtered by the table's own policy"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"forbid": [
|
|
30
|
+
"L8",
|
|
31
|
+
"L9",
|
|
32
|
+
"L15",
|
|
33
|
+
"L20"
|
|
34
|
+
],
|
|
35
|
+
"worstSeverity": "low",
|
|
36
|
+
"fix": "Narrow the body to what the function is meant to expose, authorize the caller inside it, or give the function an owner whose reach matches its job — and make it SECURITY INVOKER if the hand-off of c_definer_fn_owner's privileges was not intended. Revoking EXECUTE is not the fix: that grant is what the API serves.",
|
|
37
|
+
"id": "48-definer-function-reach"
|
|
38
|
+
}
|