@safegres/test 0.0.1 → 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,41 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_definer_fn CASCADE;
|
|
2
|
+
CREATE SCHEMA c_definer_fn;
|
|
3
|
+
|
|
4
|
+
-- The role the function executes as, and the owner of the table behind it.
|
|
5
|
+
DO $$ BEGIN
|
|
6
|
+
CREATE ROLE c_definer_fn_owner NOLOGIN;
|
|
7
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
8
|
+
END $$;
|
|
9
|
+
|
|
10
|
+
GRANT USAGE ON SCHEMA c_definer_fn TO corpus_anon, corpus_user, c_definer_fn_owner;
|
|
11
|
+
|
|
12
|
+
CREATE TABLE c_definer_fn.secrets (
|
|
13
|
+
id bigserial PRIMARY KEY,
|
|
14
|
+
subject text NOT NULL,
|
|
15
|
+
body text NOT NULL
|
|
16
|
+
);
|
|
17
|
+
ALTER TABLE c_definer_fn.secrets OWNER TO c_definer_fn_owner;
|
|
18
|
+
ALTER TABLE c_definer_fn.secrets ENABLE ROW LEVEL SECURITY;
|
|
19
|
+
|
|
20
|
+
-- Signed-in users see their own rows. corpus_anon holds no grant on the table
|
|
21
|
+
-- at all, and no policy names it.
|
|
22
|
+
CREATE POLICY secrets_own_rows ON c_definer_fn.secrets
|
|
23
|
+
FOR SELECT TO corpus_user
|
|
24
|
+
USING (subject = (SELECT current_setting('app.subject', true)));
|
|
25
|
+
CREATE INDEX secrets_subject_idx ON c_definer_fn.secrets (subject);
|
|
26
|
+
GRANT SELECT ON c_definer_fn.secrets TO corpus_user;
|
|
27
|
+
|
|
28
|
+
-- The flaw: the function is SECURITY DEFINER, so its body runs as
|
|
29
|
+
-- c_definer_fn_owner — who owns `secrets` and, because RLS is not FORCEd, is
|
|
30
|
+
-- not filtered by its policy. EXECUTE is the only grant corpus_anon needs, and
|
|
31
|
+
-- nothing in the ACL of `secrets` names it. Postgres grants EXECUTE to PUBLIC
|
|
32
|
+
-- by default, so this reach exists even where nobody wrote a grant.
|
|
33
|
+
CREATE FUNCTION c_definer_fn.recent_subjects()
|
|
34
|
+
RETURNS TABLE (id bigint, subject text)
|
|
35
|
+
LANGUAGE sql
|
|
36
|
+
SECURITY DEFINER
|
|
37
|
+
SET search_path TO c_definer_fn, pg_catalog
|
|
38
|
+
AS $$
|
|
39
|
+
SELECT s.id, s.subject FROM c_definer_fn.secrets s ORDER BY s.id DESC LIMIT 20;
|
|
40
|
+
$$;
|
|
41
|
+
ALTER FUNCTION c_definer_fn.recent_subjects() OWNER TO c_definer_fn_owner;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "An invoker function over the same body is not a reach edge",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_invoker_fn"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A3",
|
|
19
|
+
"relation": "c_invoker_fn.secrets",
|
|
20
|
+
"note": "the only real finding left: RLS is not FORCEd, so the table owner still reads every row"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"forbid": [
|
|
24
|
+
"L8",
|
|
25
|
+
"L15",
|
|
26
|
+
"L19",
|
|
27
|
+
"L20"
|
|
28
|
+
],
|
|
29
|
+
"worstSeverity": "low",
|
|
30
|
+
"fix": "ALTER TABLE c_invoker_fn.secrets FORCE ROW LEVEL SECURITY. The function needs no fix — running as the caller is what the definer-function reach (case 48) is fixed *into*.",
|
|
31
|
+
"id": "49-invoker-function-no-reach"
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_invoker_fn CASCADE;
|
|
2
|
+
CREATE SCHEMA c_invoker_fn;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_invoker_fn_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_invoker_fn TO corpus_anon, corpus_user, c_invoker_fn_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_invoker_fn.secrets (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
subject text NOT NULL,
|
|
14
|
+
body text NOT NULL
|
|
15
|
+
);
|
|
16
|
+
ALTER TABLE c_invoker_fn.secrets OWNER TO c_invoker_fn_owner;
|
|
17
|
+
ALTER TABLE c_invoker_fn.secrets ENABLE ROW LEVEL SECURITY;
|
|
18
|
+
|
|
19
|
+
CREATE POLICY secrets_own_rows ON c_invoker_fn.secrets
|
|
20
|
+
FOR SELECT TO corpus_user
|
|
21
|
+
USING (subject = (SELECT current_setting('app.subject', true)));
|
|
22
|
+
CREATE INDEX secrets_subject_idx ON c_invoker_fn.secrets (subject);
|
|
23
|
+
GRANT SELECT ON c_invoker_fn.secrets TO corpus_user;
|
|
24
|
+
|
|
25
|
+
-- The same body over the same shape, with the one difference that matters:
|
|
26
|
+
-- a function is SECURITY INVOKER unless it says otherwise, so the body runs
|
|
27
|
+
-- with the caller's privileges and corpus_anon is denied on `secrets` exactly
|
|
28
|
+
-- as a direct SELECT would be. There is no reach edge here, and a rule that
|
|
29
|
+
-- reported one would be flagging a correct schema — which is why this case
|
|
30
|
+
-- forbids L19 rather than merely omitting it.
|
|
31
|
+
CREATE FUNCTION c_invoker_fn.recent_subjects()
|
|
32
|
+
RETURNS TABLE (id bigint, subject text)
|
|
33
|
+
LANGUAGE sql
|
|
34
|
+
SET search_path TO c_invoker_fn, pg_catalog
|
|
35
|
+
AS $$
|
|
36
|
+
SELECT s.id, s.subject FROM c_invoker_fn.secrets s ORDER BY s.id DESC LIMIT 20;
|
|
37
|
+
$$;
|
|
38
|
+
ALTER FUNCTION c_invoker_fn.recent_subjects() OWNER TO c_invoker_fn_owner;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Anonymous role writes a locked table through an INSTEAD OF trigger's definer function",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_trigger_write"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L20",
|
|
19
|
+
"relation": "c_trigger_write.submissions",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "the write never reaches `submissions` directly — it becomes the trigger function's body, which is SECURITY DEFINER, so it lands as c_trigger_write_owner. This is the write L9 suppressed as unfollowable"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"code": "A3",
|
|
25
|
+
"relation": "c_trigger_write.submissions",
|
|
26
|
+
"note": "RLS is not FORCEd, which is why the owner's insert inside the trigger body is not checked against the table's own policy"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"forbid": [
|
|
30
|
+
"L8",
|
|
31
|
+
"L9",
|
|
32
|
+
"L15",
|
|
33
|
+
"L19"
|
|
34
|
+
],
|
|
35
|
+
"worstSeverity": "low",
|
|
36
|
+
"fix": "Make c_trigger_write.tg_submission_inbox() SECURITY INVOKER so the caller's own grants apply, authorize the caller inside the body, or give the function an owner whose reach matches what the view is meant to accept. Revoking corpus_anon's INSERT on the view is not the fix — that grant is what the API serves.",
|
|
37
|
+
"id": "50-instead-of-trigger-write"
|
|
38
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_trigger_write CASCADE;
|
|
2
|
+
CREATE SCHEMA c_trigger_write;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_trigger_write_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_trigger_write TO corpus_anon, corpus_user, c_trigger_write_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_trigger_write.submissions (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
author text NOT NULL,
|
|
14
|
+
body text NOT NULL
|
|
15
|
+
);
|
|
16
|
+
ALTER TABLE c_trigger_write.submissions OWNER TO c_trigger_write_owner;
|
|
17
|
+
ALTER TABLE c_trigger_write.submissions ENABLE ROW LEVEL SECURITY;
|
|
18
|
+
|
|
19
|
+
CREATE POLICY submissions_own_rows ON c_trigger_write.submissions
|
|
20
|
+
FOR ALL TO corpus_user
|
|
21
|
+
USING (author = (SELECT current_setting('app.author', true)))
|
|
22
|
+
WITH CHECK (author = (SELECT current_setting('app.author', true)));
|
|
23
|
+
CREATE INDEX submissions_author_idx ON c_trigger_write.submissions (author);
|
|
24
|
+
GRANT SELECT, INSERT ON c_trigger_write.submissions TO corpus_user;
|
|
25
|
+
GRANT USAGE ON SEQUENCE c_trigger_write.submissions_id_seq TO corpus_user;
|
|
26
|
+
|
|
27
|
+
-- The flaw, in the function rather than the view: an INSTEAD OF trigger
|
|
28
|
+
-- replaces the write against the view with this body, and the body is
|
|
29
|
+
-- permission-checked against whoever the *function* runs as. SECURITY DEFINER
|
|
30
|
+
-- makes that c_trigger_write_owner, so corpus_anon's INSERT on the view lands
|
|
31
|
+
-- in `submissions` under the table owner. The view's own owner and its
|
|
32
|
+
-- security_invoker setting do not enter into it.
|
|
33
|
+
CREATE FUNCTION c_trigger_write.tg_submission_inbox()
|
|
34
|
+
RETURNS trigger
|
|
35
|
+
LANGUAGE plpgsql
|
|
36
|
+
SECURITY DEFINER
|
|
37
|
+
SET search_path TO c_trigger_write, pg_catalog
|
|
38
|
+
AS $$
|
|
39
|
+
BEGIN
|
|
40
|
+
INSERT INTO c_trigger_write.submissions (author, body) VALUES (NEW.author, NEW.body);
|
|
41
|
+
RETURN NEW;
|
|
42
|
+
END;
|
|
43
|
+
$$;
|
|
44
|
+
ALTER FUNCTION c_trigger_write.tg_submission_inbox() OWNER TO c_trigger_write_owner;
|
|
45
|
+
|
|
46
|
+
-- security_invoker, precisely so the finding cannot be attributed to the view:
|
|
47
|
+
-- the escalation is the trigger function's, and L9's auto-update path does not
|
|
48
|
+
-- apply to a view carrying INSTEAD OF triggers at all.
|
|
49
|
+
CREATE VIEW c_trigger_write.submission_inbox
|
|
50
|
+
WITH (security_invoker = true) AS
|
|
51
|
+
SELECT id, author, body FROM c_trigger_write.submissions;
|
|
52
|
+
ALTER VIEW c_trigger_write.submission_inbox OWNER TO c_trigger_write_owner;
|
|
53
|
+
|
|
54
|
+
CREATE TRIGGER submission_inbox_insert
|
|
55
|
+
INSTEAD OF INSERT ON c_trigger_write.submission_inbox
|
|
56
|
+
FOR EACH ROW EXECUTE FUNCTION c_trigger_write.tg_submission_inbox();
|
|
57
|
+
|
|
58
|
+
GRANT INSERT ON c_trigger_write.submission_inbox TO corpus_anon;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "An INSTEAD OF trigger whose function runs as the caller is not a write bypass",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_invoker_trigger"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A3",
|
|
19
|
+
"relation": "c_invoker_trigger.submissions",
|
|
20
|
+
"note": "the only real finding left: RLS is not FORCEd, so the table owner still writes every row"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"forbid": [
|
|
24
|
+
"L8",
|
|
25
|
+
"L9",
|
|
26
|
+
"L15",
|
|
27
|
+
"L19",
|
|
28
|
+
"L20"
|
|
29
|
+
],
|
|
30
|
+
"worstSeverity": "low",
|
|
31
|
+
"fix": "ALTER TABLE c_invoker_trigger.submissions FORCE ROW LEVEL SECURITY. Neither the trigger nor the view needs a fix — an invoker trigger function is what case 50 is fixed *into*, and the view being a definer changes nothing, because it is the function's security attribute that decides who the write runs as.",
|
|
32
|
+
"id": "51-invoker-trigger-no-write"
|
|
33
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_invoker_trigger CASCADE;
|
|
2
|
+
CREATE SCHEMA c_invoker_trigger;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_invoker_trigger_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_invoker_trigger
|
|
10
|
+
TO corpus_anon, corpus_user, c_invoker_trigger_owner;
|
|
11
|
+
|
|
12
|
+
CREATE TABLE c_invoker_trigger.submissions (
|
|
13
|
+
id bigserial PRIMARY KEY,
|
|
14
|
+
author text NOT NULL,
|
|
15
|
+
body text NOT NULL
|
|
16
|
+
);
|
|
17
|
+
ALTER TABLE c_invoker_trigger.submissions OWNER TO c_invoker_trigger_owner;
|
|
18
|
+
ALTER TABLE c_invoker_trigger.submissions ENABLE ROW LEVEL SECURITY;
|
|
19
|
+
|
|
20
|
+
CREATE POLICY submissions_own_rows ON c_invoker_trigger.submissions
|
|
21
|
+
FOR ALL TO corpus_user
|
|
22
|
+
USING (author = (SELECT current_setting('app.author', true)))
|
|
23
|
+
WITH CHECK (author = (SELECT current_setting('app.author', true)));
|
|
24
|
+
CREATE INDEX submissions_author_idx ON c_invoker_trigger.submissions (author);
|
|
25
|
+
GRANT SELECT, INSERT ON c_invoker_trigger.submissions TO corpus_user;
|
|
26
|
+
GRANT USAGE ON SEQUENCE c_invoker_trigger.submissions_id_seq TO corpus_user;
|
|
27
|
+
|
|
28
|
+
-- The same trigger over the same shape, with the one difference that matters:
|
|
29
|
+
-- the trigger function is SECURITY INVOKER, so its body runs as the caller and
|
|
30
|
+
-- Postgres denies corpus_anon on `submissions` exactly as it would deny a
|
|
31
|
+
-- direct insert. The view here is *not* security_invoker, which is the point:
|
|
32
|
+
-- a view's owner does not decide who a trigger function's body runs as, and a
|
|
33
|
+
-- rule that read the view's setting instead of the function's would fire here.
|
|
34
|
+
CREATE FUNCTION c_invoker_trigger.tg_submission_inbox()
|
|
35
|
+
RETURNS trigger
|
|
36
|
+
LANGUAGE plpgsql
|
|
37
|
+
SET search_path TO c_invoker_trigger, pg_catalog
|
|
38
|
+
AS $$
|
|
39
|
+
BEGIN
|
|
40
|
+
INSERT INTO c_invoker_trigger.submissions (author, body) VALUES (NEW.author, NEW.body);
|
|
41
|
+
RETURN NEW;
|
|
42
|
+
END;
|
|
43
|
+
$$;
|
|
44
|
+
ALTER FUNCTION c_invoker_trigger.tg_submission_inbox() OWNER TO c_invoker_trigger_owner;
|
|
45
|
+
|
|
46
|
+
CREATE VIEW c_invoker_trigger.submission_inbox AS
|
|
47
|
+
SELECT id, author, body FROM c_invoker_trigger.submissions;
|
|
48
|
+
ALTER VIEW c_invoker_trigger.submission_inbox OWNER TO c_invoker_trigger_owner;
|
|
49
|
+
|
|
50
|
+
CREATE TRIGGER submission_inbox_insert
|
|
51
|
+
INSTEAD OF INSERT ON c_invoker_trigger.submission_inbox
|
|
52
|
+
FOR EACH ROW EXECUTE FUNCTION c_invoker_trigger.tg_submission_inbox();
|
|
53
|
+
|
|
54
|
+
GRANT INSERT ON c_invoker_trigger.submission_inbox TO corpus_anon;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A role holds EXECUTE on a trigger function it can neither call nor fire — revocable",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_revocable"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "L21",
|
|
19
|
+
"relation": "c_revocable.stamp_created_by",
|
|
20
|
+
"role": "corpus_anon",
|
|
21
|
+
"note": "corpus_anon's EXECUTE arrives from a blanket default-privileges grant, but the reachability closure never lands on the function: a trigger function is not directly callable, and corpus_anon holds no write grant on `events` to fire the trigger. Granted minus reachable is non-empty, so the grant is revocable."
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"forbid": [
|
|
25
|
+
"L19",
|
|
26
|
+
"L20"
|
|
27
|
+
],
|
|
28
|
+
"fix": "REVOKE EXECUTE ON FUNCTION c_revocable.stamp_created_by() FROM corpus_anon. No path exercises the grant — corpus_anon cannot call a trigger function directly, and cannot fire the trigger without a write grant on c_revocable.events — so dropping it changes nothing corpus_anon can do.",
|
|
29
|
+
"id": "52-revocable-execute-grant"
|
|
30
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_revocable CASCADE;
|
|
2
|
+
CREATE SCHEMA c_revocable;
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE ROLE c_revocable_owner NOLOGIN;
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
8
|
+
|
|
9
|
+
GRANT USAGE ON SCHEMA c_revocable TO corpus_anon, corpus_user, c_revocable_owner;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE c_revocable.events (
|
|
12
|
+
id bigserial PRIMARY KEY,
|
|
13
|
+
body text NOT NULL
|
|
14
|
+
);
|
|
15
|
+
ALTER TABLE c_revocable.events OWNER TO c_revocable_owner;
|
|
16
|
+
ALTER TABLE c_revocable.events ENABLE ROW LEVEL SECURITY;
|
|
17
|
+
ALTER TABLE c_revocable.events FORCE ROW LEVEL SECURITY;
|
|
18
|
+
|
|
19
|
+
-- corpus_anon may read, nothing more.
|
|
20
|
+
CREATE POLICY events_read ON c_revocable.events
|
|
21
|
+
FOR SELECT TO corpus_anon USING (id > 0);
|
|
22
|
+
GRANT SELECT ON c_revocable.events TO corpus_anon;
|
|
23
|
+
|
|
24
|
+
-- A SECURITY DEFINER trigger function. It fires only on write, and corpus_anon
|
|
25
|
+
-- holds no write grant on `events`, so it can never make the trigger run. A
|
|
26
|
+
-- trigger function is not directly callable either: Postgres refuses a direct
|
|
27
|
+
-- call however wide its EXECUTE ACL is.
|
|
28
|
+
CREATE FUNCTION c_revocable.stamp_created_by()
|
|
29
|
+
RETURNS trigger
|
|
30
|
+
LANGUAGE plpgsql
|
|
31
|
+
AS $$
|
|
32
|
+
BEGIN
|
|
33
|
+
RETURN NEW;
|
|
34
|
+
END;
|
|
35
|
+
$$;
|
|
36
|
+
ALTER FUNCTION c_revocable.stamp_created_by() OWNER TO c_revocable_owner;
|
|
37
|
+
|
|
38
|
+
CREATE TRIGGER stamp_created_by BEFORE INSERT ON c_revocable.events
|
|
39
|
+
FOR EACH ROW EXECUTE FUNCTION c_revocable.stamp_created_by();
|
|
40
|
+
|
|
41
|
+
-- The blanket grant: corpus_anon gets EXECUTE the way a schema created with
|
|
42
|
+
-- `ALTER DEFAULT PRIVILEGES ... GRANT ALL ON FUNCTIONS TO anonymous` hands it
|
|
43
|
+
-- out. corpus_anon now holds EXECUTE on a trigger function it can neither call
|
|
44
|
+
-- nor fire — a grant `granted − reachable` should prove revocable.
|
|
45
|
+
GRANT EXECUTE ON FUNCTION c_revocable.stamp_created_by() TO corpus_anon;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "A grant that looks dead but is reached through an RLS policy predicate — retained, not revocable",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_policy_reach",
|
|
7
|
+
"c_policy_priv"
|
|
8
|
+
],
|
|
9
|
+
"roles": [
|
|
10
|
+
"corpus_anon",
|
|
11
|
+
"corpus_user"
|
|
12
|
+
],
|
|
13
|
+
"anonRoles": [
|
|
14
|
+
"corpus_anon"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"expect": [],
|
|
18
|
+
"forbid": [
|
|
19
|
+
"L21"
|
|
20
|
+
],
|
|
21
|
+
"fix": "Do not revoke. corpus_anon's EXECUTE on c_policy_priv.can_read(bigint) is exercised by the docs_visible policy predicate on every read of c_policy_reach.docs, which evaluates as the querying role. Revoking it makes the policy raise `permission denied for function can_read` and fails authorization closed for every anonymous read — the load-bearing grant the rule is built to protect.",
|
|
22
|
+
"id": "53-policy-predicate-retains-execute"
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_policy_reach CASCADE;
|
|
2
|
+
DROP SCHEMA IF EXISTS c_policy_priv CASCADE;
|
|
3
|
+
CREATE SCHEMA c_policy_reach;
|
|
4
|
+
CREATE SCHEMA c_policy_priv;
|
|
5
|
+
|
|
6
|
+
DO $$ BEGIN
|
|
7
|
+
CREATE ROLE c_policy_owner NOLOGIN;
|
|
8
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
9
|
+
END $$;
|
|
10
|
+
|
|
11
|
+
-- corpus_anon can reach the API schema, but holds no USAGE on the private one —
|
|
12
|
+
-- exactly the constructive-db shape, where the `*_private` schemas hold the
|
|
13
|
+
-- check functions the policies call. The private schema is still audited (it is
|
|
14
|
+
-- in scope), it is simply not part of the surface corpus_anon addresses.
|
|
15
|
+
GRANT USAGE ON SCHEMA c_policy_reach TO corpus_anon, c_policy_owner;
|
|
16
|
+
GRANT USAGE ON SCHEMA c_policy_priv TO c_policy_owner;
|
|
17
|
+
|
|
18
|
+
CREATE TABLE c_policy_reach.docs (
|
|
19
|
+
id bigserial PRIMARY KEY,
|
|
20
|
+
owner_id bigint NOT NULL,
|
|
21
|
+
body text NOT NULL
|
|
22
|
+
);
|
|
23
|
+
ALTER TABLE c_policy_reach.docs OWNER TO c_policy_owner;
|
|
24
|
+
ALTER TABLE c_policy_reach.docs ENABLE ROW LEVEL SECURITY;
|
|
25
|
+
ALTER TABLE c_policy_reach.docs FORCE ROW LEVEL SECURITY;
|
|
26
|
+
|
|
27
|
+
-- The load-bearing check. It looks dead: corpus_anon never calls it, holds no
|
|
28
|
+
-- USAGE on its schema, and no ACL on `docs` names it. But it is the SELECT
|
|
29
|
+
-- policy's predicate, which evaluates as corpus_anon on every read of `docs`.
|
|
30
|
+
CREATE FUNCTION c_policy_priv.can_read(owner_id bigint)
|
|
31
|
+
RETURNS boolean
|
|
32
|
+
LANGUAGE sql
|
|
33
|
+
STABLE
|
|
34
|
+
AS $$
|
|
35
|
+
SELECT owner_id = NULLIF(current_setting('app.uid', true), '')::bigint;
|
|
36
|
+
$$;
|
|
37
|
+
ALTER FUNCTION c_policy_priv.can_read(bigint) OWNER TO c_policy_owner;
|
|
38
|
+
|
|
39
|
+
CREATE POLICY docs_visible ON c_policy_reach.docs
|
|
40
|
+
FOR SELECT TO corpus_anon
|
|
41
|
+
USING (c_policy_priv.can_read(owner_id));
|
|
42
|
+
|
|
43
|
+
GRANT SELECT ON c_policy_reach.docs TO corpus_anon;
|
|
44
|
+
|
|
45
|
+
-- The blanket grant again: corpus_anon holds EXECUTE on the private check
|
|
46
|
+
-- function. `granted − reachable` must NOT report it revocable — the policy
|
|
47
|
+
-- predicate is a reachable path, and losing EXECUTE would break authorization
|
|
48
|
+
-- silently, for every anonymous read. This is the case the rule exists for.
|
|
49
|
+
GRANT EXECUTE ON FUNCTION c_policy_priv.can_read(bigint) TO corpus_anon;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The evaluation corpus: small schemas with a deliberate flaw and a written-down
|
|
3
|
+
* answer.
|
|
4
|
+
*
|
|
5
|
+
* It exists because "the score went up" is not evidence of anything on its own.
|
|
6
|
+
* A corpus with known answers turns safegres into a measuring instrument you
|
|
7
|
+
* can calibrate: every case names the findings a correct audit must produce, so
|
|
8
|
+
* a run can be graded on recall (did it find the flaw?) rather than on a number
|
|
9
|
+
* whose provenance nobody can check. It is used three ways —
|
|
10
|
+
*
|
|
11
|
+
* 1. safegres's own regression suite (`__tests__/corpus.test.ts`);
|
|
12
|
+
* 2. an evaluation harness for an agent asked to *fix* the case: audit,
|
|
13
|
+
* hand the findings over, re-audit, and require the expected findings to
|
|
14
|
+
* be gone without new ones appearing;
|
|
15
|
+
* 3. worked examples — each case is short enough to read.
|
|
16
|
+
*
|
|
17
|
+
* Cases live in `corpus/cases/<id>/` as `schema.sql` plus `case.json`, i.e.
|
|
18
|
+
* data, not code: another tool can consume the corpus without running ours.
|
|
19
|
+
*/
|
|
20
|
+
import type { ExposureConfig } from '../config/types';
|
|
21
|
+
import type { Report, Severity } from '../types';
|
|
22
|
+
/** One expected finding: a rule code, optionally pinned to a relation. */
|
|
23
|
+
export interface ExpectedFinding {
|
|
24
|
+
code: string;
|
|
25
|
+
/** `schema.table`, when the case has more than one relation in play. */
|
|
26
|
+
relation?: string;
|
|
27
|
+
/** Why this is the right answer — the part a reader learns from. */
|
|
28
|
+
note?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CorpusCase {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
/** Which axis the flaw is on. A case may be graded on both. */
|
|
34
|
+
dimension: 'security' | 'perf';
|
|
35
|
+
/** The SQL that builds the case, as committed. */
|
|
36
|
+
sql: string;
|
|
37
|
+
/** The surface the case is graded against — its whole point, for most cases. */
|
|
38
|
+
exposure: ExposureConfig;
|
|
39
|
+
/** Findings a correct audit must produce. */
|
|
40
|
+
expect: ExpectedFinding[];
|
|
41
|
+
/** Rule codes that must NOT fire: the false positives this case guards. */
|
|
42
|
+
forbid?: string[];
|
|
43
|
+
/** Worst severity the case should produce, as a coarse sanity check. */
|
|
44
|
+
worstSeverity?: Severity;
|
|
45
|
+
/** How the flaw is fixed, in one sentence — the answer key. */
|
|
46
|
+
fix: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The corpus shipped with the package. Two candidates because the package
|
|
50
|
+
* publishes from `dist/`, where the corpus sits one directory closer.
|
|
51
|
+
*/
|
|
52
|
+
export declare function corpusDir(): string;
|
|
53
|
+
/**
|
|
54
|
+
* The SQL creating the roles every case is graded against (`corpus_anon`,
|
|
55
|
+
* `corpus_user`). Idempotent, so a harness can run it before every case.
|
|
56
|
+
*/
|
|
57
|
+
export declare function corpusBootstrap(dir?: string): string;
|
|
58
|
+
/** Load every case in `dir` (default: the shipped corpus), ordered by id. */
|
|
59
|
+
export declare function loadCorpus(dir?: string): CorpusCase[];
|
|
60
|
+
/** Load a single case directory (`case.json` + `schema.sql`). */
|
|
61
|
+
export declare function loadCase(dir: string): CorpusCase;
|
|
62
|
+
export interface CaseResult {
|
|
63
|
+
id: string;
|
|
64
|
+
/** Expected findings the audit produced. */
|
|
65
|
+
found: ExpectedFinding[];
|
|
66
|
+
/** Expected findings the audit missed — recall failures. */
|
|
67
|
+
missed: ExpectedFinding[];
|
|
68
|
+
/** Forbidden rule codes the audit produced — precision failures. */
|
|
69
|
+
falsePositives: string[];
|
|
70
|
+
/** True when nothing was missed and nothing forbidden fired. */
|
|
71
|
+
passed: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Grade a report against the case it was produced from. Deliberately not a
|
|
75
|
+
* strict equality check on the finding set: a case pins the findings that
|
|
76
|
+
* define it, and a later release adding an unrelated advisory should not
|
|
77
|
+
* invalidate the corpus. What a case *does* pin negatively goes in `forbid`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function gradeCase(report: Report, testCase: CorpusCase): CaseResult;
|
package/corpus/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The evaluation corpus: small schemas with a deliberate flaw and a written-down
|
|
4
|
+
* answer.
|
|
5
|
+
*
|
|
6
|
+
* It exists because "the score went up" is not evidence of anything on its own.
|
|
7
|
+
* A corpus with known answers turns safegres into a measuring instrument you
|
|
8
|
+
* can calibrate: every case names the findings a correct audit must produce, so
|
|
9
|
+
* a run can be graded on recall (did it find the flaw?) rather than on a number
|
|
10
|
+
* whose provenance nobody can check. It is used three ways —
|
|
11
|
+
*
|
|
12
|
+
* 1. safegres's own regression suite (`__tests__/corpus.test.ts`);
|
|
13
|
+
* 2. an evaluation harness for an agent asked to *fix* the case: audit,
|
|
14
|
+
* hand the findings over, re-audit, and require the expected findings to
|
|
15
|
+
* be gone without new ones appearing;
|
|
16
|
+
* 3. worked examples — each case is short enough to read.
|
|
17
|
+
*
|
|
18
|
+
* Cases live in `corpus/cases/<id>/` as `schema.sql` plus `case.json`, i.e.
|
|
19
|
+
* data, not code: another tool can consume the corpus without running ours.
|
|
20
|
+
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.corpusDir = corpusDir;
|
|
56
|
+
exports.corpusBootstrap = corpusBootstrap;
|
|
57
|
+
exports.loadCorpus = loadCorpus;
|
|
58
|
+
exports.loadCase = loadCase;
|
|
59
|
+
exports.gradeCase = gradeCase;
|
|
60
|
+
const fs = __importStar(require("fs"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
/**
|
|
63
|
+
* The corpus shipped with the package. Two candidates because the package
|
|
64
|
+
* publishes from `dist/`, where the corpus sits one directory closer.
|
|
65
|
+
*/
|
|
66
|
+
function corpusDir() {
|
|
67
|
+
const candidates = [
|
|
68
|
+
path.resolve(__dirname, '..', 'corpus', 'cases'),
|
|
69
|
+
path.resolve(__dirname, '..', '..', 'corpus', 'cases')
|
|
70
|
+
];
|
|
71
|
+
const found = candidates.find((dir) => fs.existsSync(dir));
|
|
72
|
+
if (!found)
|
|
73
|
+
throw new Error(`Corpus not found. Looked in: ${candidates.join(', ')}`);
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The SQL creating the roles every case is graded against (`corpus_anon`,
|
|
78
|
+
* `corpus_user`). Idempotent, so a harness can run it before every case.
|
|
79
|
+
*/
|
|
80
|
+
function corpusBootstrap(dir = corpusDir()) {
|
|
81
|
+
return fs.readFileSync(path.resolve(dir, '..', 'bootstrap.sql'), 'utf8');
|
|
82
|
+
}
|
|
83
|
+
/** Load every case in `dir` (default: the shipped corpus), ordered by id. */
|
|
84
|
+
function loadCorpus(dir = corpusDir()) {
|
|
85
|
+
return fs
|
|
86
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
87
|
+
.filter((e) => e.isDirectory())
|
|
88
|
+
.map((e) => loadCase(path.join(dir, e.name)))
|
|
89
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
90
|
+
}
|
|
91
|
+
/** Load a single case directory (`case.json` + `schema.sql`). */
|
|
92
|
+
function loadCase(dir) {
|
|
93
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dir, 'case.json'), 'utf8'));
|
|
94
|
+
return {
|
|
95
|
+
...meta,
|
|
96
|
+
id: meta.id ?? path.basename(dir),
|
|
97
|
+
sql: fs.readFileSync(path.join(dir, 'schema.sql'), 'utf8')
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function matches(finding, expected) {
|
|
101
|
+
if (finding.code !== expected.code)
|
|
102
|
+
return false;
|
|
103
|
+
if (!expected.relation)
|
|
104
|
+
return true;
|
|
105
|
+
return `${finding.schema}.${finding.table}` === expected.relation;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Grade a report against the case it was produced from. Deliberately not a
|
|
109
|
+
* strict equality check on the finding set: a case pins the findings that
|
|
110
|
+
* define it, and a later release adding an unrelated advisory should not
|
|
111
|
+
* invalidate the corpus. What a case *does* pin negatively goes in `forbid`.
|
|
112
|
+
*/
|
|
113
|
+
function gradeCase(report, testCase) {
|
|
114
|
+
const findings = report.findings;
|
|
115
|
+
const found = testCase.expect.filter((e) => findings.some((f) => matches(f, e)));
|
|
116
|
+
const missed = testCase.expect.filter((e) => !findings.some((f) => matches(f, e)));
|
|
117
|
+
const falsePositives = [...new Set(findings.map((f) => f.code))].filter((code) => (testCase.forbid ?? []).includes(code));
|
|
118
|
+
return {
|
|
119
|
+
id: testCase.id,
|
|
120
|
+
found,
|
|
121
|
+
missed,
|
|
122
|
+
falsePositives,
|
|
123
|
+
passed: missed.length === 0 && falsePositives.length === 0
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `safegres/adapters` — the built-in exposure adapters and the interface a
|
|
3
|
+
* custom one implements. Kept as its own entry point so a config file can
|
|
4
|
+
* import an adapter without pulling in the whole auditor.
|
|
5
|
+
*/
|
|
6
|
+
export type { ExposureAdapter, PlaneInput, ReachContext } from './exposure/adapters';
|
|
7
|
+
export { BUILTIN_ADAPTERS, constructiveAdapter, definePlanes, graphileAdapter, hasuraAdapter, postgraphileAdapter, postgrestAdapter, resolveAdapters, supabaseAdapter } from './exposure/adapters';
|
|
8
|
+
export type { ApiReach, UnreachableRelation } from './exposure/reach';
|
package/esm/adapters.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `safegres/adapters` — the built-in exposure adapters and the interface a
|
|
3
|
+
* custom one implements. Kept as its own entry point so a config file can
|
|
4
|
+
* import an adapter without pulling in the whole auditor.
|
|
5
|
+
*/
|
|
6
|
+
export { BUILTIN_ADAPTERS, constructiveAdapter, definePlanes, graphileAdapter, hasuraAdapter, postgraphileAdapter, postgrestAdapter, resolveAdapters, supabaseAdapter } from './exposure/adapters';
|