@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,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateConfigShape = validateConfigShape;
|
|
4
|
+
const resolve_1 = require("./resolve");
|
|
5
|
+
const schema_1 = require("./schema");
|
|
6
|
+
/**
|
|
7
|
+
* Check a config against the described shape: unknown keys, and values of the
|
|
8
|
+
* wrong kind.
|
|
9
|
+
*
|
|
10
|
+
* A typo'd key used to be *silently ignored*, which for a file that decides
|
|
11
|
+
* whether CI fails is the worst possible outcome — `failon: { grade: 'B' }`
|
|
12
|
+
* reads as a passing build rather than as a mistake. So this rejects rather
|
|
13
|
+
* than warns, and names the key it thinks you meant.
|
|
14
|
+
*
|
|
15
|
+
* Value checks stay shallow on purpose: rule settings, severities and grades
|
|
16
|
+
* are validated by `resolveRules` and by the gate code, which know the rule
|
|
17
|
+
* registry. This is the shape, not the semantics.
|
|
18
|
+
*/
|
|
19
|
+
function validateConfigShape(config) {
|
|
20
|
+
const problems = [];
|
|
21
|
+
checkObject(config, schema_1.CONFIG_SHAPE, '', problems, ['$schema', 'extends']);
|
|
22
|
+
if (problems.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
throw new resolve_1.ConfigValidationError(`Invalid safegres configuration:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
25
|
+
}
|
|
26
|
+
function checkObject(value, properties, path, problems, extraKeys = []) {
|
|
27
|
+
if (!isPlainObject(value))
|
|
28
|
+
return;
|
|
29
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
30
|
+
if (extraKeys.includes(key))
|
|
31
|
+
continue;
|
|
32
|
+
const node = properties[key];
|
|
33
|
+
if (!node) {
|
|
34
|
+
problems.push(`unknown key ${quote(path, key)}${suggestion(key, Object.keys(properties))}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
checkNode(entry, node, path ? `${path}.${key}` : key, problems);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function checkNode(value, node, path, problems) {
|
|
41
|
+
if (value === undefined || value === null)
|
|
42
|
+
return;
|
|
43
|
+
switch (node.type) {
|
|
44
|
+
case 'any':
|
|
45
|
+
return;
|
|
46
|
+
case 'string':
|
|
47
|
+
if (typeof value !== 'string')
|
|
48
|
+
return void problems.push(wrongType(path, 'a string', value));
|
|
49
|
+
if (node.enum && !node.enum.includes(value)) {
|
|
50
|
+
problems.push(`"${path}" is "${value}"; expected one of ${node.enum.map((v) => `"${v}"`).join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
case 'number':
|
|
54
|
+
if (typeof value !== 'number')
|
|
55
|
+
problems.push(wrongType(path, 'a number', value));
|
|
56
|
+
return;
|
|
57
|
+
case 'boolean':
|
|
58
|
+
if (typeof value !== 'boolean')
|
|
59
|
+
problems.push(wrongType(path, 'a boolean', value));
|
|
60
|
+
return;
|
|
61
|
+
case 'array':
|
|
62
|
+
if (!Array.isArray(value))
|
|
63
|
+
return void problems.push(wrongType(path, 'an array', value));
|
|
64
|
+
value.forEach((item, i) => checkNode(item, node.items, `${path}[${i}]`, problems));
|
|
65
|
+
return;
|
|
66
|
+
case 'object':
|
|
67
|
+
if (!isPlainObject(value))
|
|
68
|
+
return void problems.push(wrongType(path, 'an object', value));
|
|
69
|
+
checkObject(value, node.properties, path, problems);
|
|
70
|
+
return;
|
|
71
|
+
case 'record':
|
|
72
|
+
if (!isPlainObject(value))
|
|
73
|
+
return void problems.push(wrongType(path, 'an object', value));
|
|
74
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
75
|
+
checkNode(entry, node.values, `${path}.${key}`, problems);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
case 'union': {
|
|
79
|
+
// A union is satisfied by any branch, so only report when every branch
|
|
80
|
+
// rejects — and then report the branch that complained least.
|
|
81
|
+
const attempts = node.of.map((branch) => {
|
|
82
|
+
const branchProblems = [];
|
|
83
|
+
checkNode(value, branch, path, branchProblems);
|
|
84
|
+
return branchProblems;
|
|
85
|
+
});
|
|
86
|
+
if (attempts.some((a) => a.length === 0))
|
|
87
|
+
return;
|
|
88
|
+
const best = attempts.reduce((a, b) => (b.length < a.length ? b : a));
|
|
89
|
+
problems.push(...best);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function wrongType(path, expected, value) {
|
|
94
|
+
return `"${path}" is ${describe(value)}; expected ${expected}`;
|
|
95
|
+
}
|
|
96
|
+
function describe(value) {
|
|
97
|
+
if (Array.isArray(value))
|
|
98
|
+
return 'an array';
|
|
99
|
+
if (value === null)
|
|
100
|
+
return 'null';
|
|
101
|
+
return `a ${typeof value}`;
|
|
102
|
+
}
|
|
103
|
+
function quote(path, key) {
|
|
104
|
+
return `"${path ? `${path}.${key}` : key}"`;
|
|
105
|
+
}
|
|
106
|
+
/** "did you mean" for one edit's worth of typo, which is the common case. */
|
|
107
|
+
function suggestion(key, known) {
|
|
108
|
+
const lower = key.toLowerCase();
|
|
109
|
+
const near = known.find((k) => k.toLowerCase() === lower)
|
|
110
|
+
?? known.find((k) => distance(k.toLowerCase(), lower) <= Math.max(1, Math.floor(key.length / 4)));
|
|
111
|
+
return near ? ` — did you mean "${near}"?` : '';
|
|
112
|
+
}
|
|
113
|
+
function distance(a, b) {
|
|
114
|
+
const rows = [Array.from({ length: b.length + 1 }, (_, j) => j)];
|
|
115
|
+
for (let i = 1; i <= a.length; i++) {
|
|
116
|
+
rows[i] = [i];
|
|
117
|
+
for (let j = 1; j <= b.length; j++) {
|
|
118
|
+
rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return rows[a.length][b.length];
|
|
122
|
+
}
|
|
123
|
+
function isPlainObject(value) {
|
|
124
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
125
|
+
}
|
package/corpus/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# The safegres evaluation corpus
|
|
2
|
+
|
|
3
|
+
Small schemas with a deliberate flaw and a written-down answer.
|
|
4
|
+
|
|
5
|
+
"The score went up" is not evidence of anything on its own. A corpus with known answers turns
|
|
6
|
+
safegres into an instrument you can calibrate: every case names the findings a correct audit must
|
|
7
|
+
produce, so a run is graded on *recall* — did it find the flaw? — rather than on a number whose
|
|
8
|
+
provenance nobody can check.
|
|
9
|
+
|
|
10
|
+
## Layout
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
corpus/
|
|
14
|
+
bootstrap.sql # corpus_anon / corpus_user: the two roles every case is graded against
|
|
15
|
+
cases/<id>/
|
|
16
|
+
schema.sql # the case, as SQL
|
|
17
|
+
case.json # the answer key
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`case.json`:
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
{
|
|
24
|
+
"title": "The anonymous role holds a write grant",
|
|
25
|
+
"dimension": "security", // which axis the flaw is on
|
|
26
|
+
"exposure": { // the surface it is graded against
|
|
27
|
+
"schemas": ["c_anon_write_grant"],
|
|
28
|
+
"roles": ["corpus_anon", "corpus_user"],
|
|
29
|
+
"anonRoles": ["corpus_anon"] // reachable without credentials
|
|
30
|
+
},
|
|
31
|
+
"expect": [{ "code": "R1", "relation": "c_anon_write_grant.posts", "note": "…" }],
|
|
32
|
+
"forbid": ["R2"], // false positives this case guards against
|
|
33
|
+
"worstSeverity": "critical",
|
|
34
|
+
"fix": "REVOKE INSERT … — writes belong to the signed-in role."
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Cases are **data, not code**: another tool can consume the corpus without running ours.
|
|
39
|
+
|
|
40
|
+
## Grading
|
|
41
|
+
|
|
42
|
+
`expect` is a lower bound, not an equality check — a later release adding an unrelated advisory
|
|
43
|
+
must not invalidate the corpus. What a case pins *negatively* goes in `forbid`.
|
|
44
|
+
|
|
45
|
+
A **negative case** is one whose whole answer is negative: `expect` is empty and `forbid` names the
|
|
46
|
+
rules that must stay quiet. It is the fixed form of a positive case — `38-foreign-key-with-index`
|
|
47
|
+
is `17-foreign-key-without-index` with the index added — so the pair states both halves of what a
|
|
48
|
+
rule means, and a rule that fires on the fix is caught by the corpus rather than by a user. A
|
|
49
|
+
negative case must also score **100** on its dimension: precision is not just "the forbidden code
|
|
50
|
+
didn't appear", it is "a correct schema costs nothing".
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { audit, gradeCase, loadCorpus, loadConfig } from 'safegres';
|
|
54
|
+
|
|
55
|
+
const { config } = loadConfig({ sealed: true, preset: 'recommended' });
|
|
56
|
+
for (const c of loadCorpus()) {
|
|
57
|
+
await client.query(c.sql);
|
|
58
|
+
const report = await audit(client, { config, exposure: c.exposure, perf: true, sealed: true });
|
|
59
|
+
const { missed, falsePositives, passed } = gradeCase(report, c);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Runs sealed, so the corpus is graded by the shipped preset and never by a config file in the tree —
|
|
64
|
+
see `--sealed` and `report.provenance` in the main README.
|
|
65
|
+
|
|
66
|
+
## Using it to evaluate an agent
|
|
67
|
+
|
|
68
|
+
1. Deploy a case and audit it.
|
|
69
|
+
2. Hand the agent the findings (or just the schema) and ask for a fix.
|
|
70
|
+
3. Re-audit. The expected findings must be **gone**, no `forbid` code may appear, and the score on
|
|
71
|
+
the case's dimension must reach 100.
|
|
72
|
+
|
|
73
|
+
Step 3 is why every case carries a one-sentence `fix`: the corpus states what "solved" means
|
|
74
|
+
instead of leaving it to the number.
|
|
75
|
+
|
|
76
|
+
## Adding a case
|
|
77
|
+
|
|
78
|
+
Keep it to one flaw, and add the negative alongside it. Anything the case does *not* mean to
|
|
79
|
+
demonstrate — an unforced RLS table, an open read policy left in to make the SQL shorter — shows up
|
|
80
|
+
as an extra finding and muddies the answer, so the surrounding schema should be otherwise correct.
|
|
81
|
+
`__tests__/corpus.test.ts` runs every case and additionally checks that the flaw costs points
|
|
82
|
+
exactly when the rule carries weight (`info` severities and fail-closed rules are weightless by
|
|
83
|
+
construction).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Roles every corpus case is graded against.
|
|
2
|
+
--
|
|
3
|
+
-- Two, because the whole model turns on the difference: `corpus_anon` is the
|
|
4
|
+
-- role an unauthenticated request arrives as, `corpus_user` the role a
|
|
5
|
+
-- signed-in one does. A case that grants `corpus_user` a write is a working
|
|
6
|
+
-- application; the same grant to `corpus_anon` is a critical.
|
|
7
|
+
DO $$ BEGIN
|
|
8
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'corpus_anon') THEN
|
|
9
|
+
CREATE ROLE corpus_anon NOLOGIN;
|
|
10
|
+
END IF;
|
|
11
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'corpus_user') THEN
|
|
12
|
+
CREATE ROLE corpus_user NOLOGIN;
|
|
13
|
+
END IF;
|
|
14
|
+
END $$;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "The anonymous role holds a write grant",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_anon_write_grant"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "R1",
|
|
19
|
+
"relation": "c_anon_write_grant.posts",
|
|
20
|
+
"note": "an unauthenticated caller can INSERT; RLS narrows which rows, not whether it may write at all"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"worstSeverity": "critical",
|
|
24
|
+
"fix": "REVOKE INSERT ON c_anon_write_grant.posts FROM corpus_anon \u2014 writes belong to the signed-in role.",
|
|
25
|
+
"id": "01-anon-write-grant"
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_anon_write_grant CASCADE;
|
|
2
|
+
CREATE SCHEMA c_anon_write_grant;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_anon_write_grant TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_anon_write_grant.posts (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
author_id uuid NOT NULL,
|
|
8
|
+
body text NOT NULL
|
|
9
|
+
);
|
|
10
|
+
ALTER TABLE c_anon_write_grant.posts ENABLE ROW LEVEL SECURITY;
|
|
11
|
+
ALTER TABLE c_anon_write_grant.posts FORCE ROW LEVEL SECURITY;
|
|
12
|
+
|
|
13
|
+
CREATE POLICY posts_read ON c_anon_write_grant.posts FOR SELECT TO corpus_anon USING (true);
|
|
14
|
+
CREATE POLICY posts_write ON c_anon_write_grant.posts FOR INSERT TO corpus_user
|
|
15
|
+
WITH CHECK (author_id = nullif(current_setting('jwt.claims.user_id', true), '')::uuid);
|
|
16
|
+
|
|
17
|
+
GRANT SELECT ON c_anon_write_grant.posts TO corpus_anon;
|
|
18
|
+
-- The flaw: the anonymous role can write, not just read.
|
|
19
|
+
GRANT INSERT ON c_anon_write_grant.posts TO corpus_anon;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Grant to PUBLIC on an RLS table",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_public_grant"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "R3",
|
|
19
|
+
"relation": "c_public_grant.invoices",
|
|
20
|
+
"note": "PUBLIC includes roles created after this grant, so the surface grows on its own"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"fix": "REVOKE SELECT ON c_public_grant.invoices FROM PUBLIC and grant to the named API role instead.",
|
|
24
|
+
"id": "02-grant-to-public"
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_public_grant CASCADE;
|
|
2
|
+
CREATE SCHEMA c_public_grant;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_public_grant TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_public_grant.invoices (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
tenant_id uuid NOT NULL,
|
|
8
|
+
total numeric NOT NULL
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX invoices_tenant_idx ON c_public_grant.invoices (tenant_id);
|
|
11
|
+
ALTER TABLE c_public_grant.invoices ENABLE ROW LEVEL SECURITY;
|
|
12
|
+
ALTER TABLE c_public_grant.invoices FORCE ROW LEVEL SECURITY;
|
|
13
|
+
|
|
14
|
+
CREATE POLICY invoices_tenant ON c_public_grant.invoices
|
|
15
|
+
USING (tenant_id = (SELECT nullif(current_setting('jwt.claims.tenant_id', true), '')::uuid));
|
|
16
|
+
|
|
17
|
+
-- The flaw: PUBLIC is every role that exists and every role that ever will.
|
|
18
|
+
GRANT SELECT ON c_public_grant.invoices TO PUBLIC;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "RLS-off table reachable by the anonymous role via PUBLIC",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_rls_off_public"
|
|
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_rls_off_public.audit_log",
|
|
20
|
+
"note": "grants exist and RLS is off: every granted role sees every row"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"code": "L5",
|
|
24
|
+
"relation": "c_rls_off_public.audit_log",
|
|
25
|
+
"note": "the reach is indirect \u2014 nothing names corpus_anon"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"fix": "ENABLE ROW LEVEL SECURITY with a scoped policy, and revoke the PUBLIC grant.",
|
|
29
|
+
"id": "03-rls-off-reachable-by-anon"
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_rls_off_public CASCADE;
|
|
2
|
+
CREATE SCHEMA c_rls_off_public;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_rls_off_public TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_rls_off_public.audit_log (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
actor_id uuid,
|
|
8
|
+
detail text
|
|
9
|
+
);
|
|
10
|
+
-- The flaw: no RLS at all. The named grant is the one a reviewer sees; the
|
|
11
|
+
-- PUBLIC grant is the one that also hands the table to the anonymous role.
|
|
12
|
+
GRANT SELECT ON c_rls_off_public.audit_log TO corpus_user;
|
|
13
|
+
GRANT SELECT ON c_rls_off_public.audit_log TO PUBLIC;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Trivially-permissive write policy",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_permissive_write"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A7",
|
|
19
|
+
"relation": "c_permissive_write.documents",
|
|
20
|
+
"note": "USING (true) on a write policy: any signed-in tenant can rewrite another tenant's rows"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"forbid": [
|
|
24
|
+
"R1",
|
|
25
|
+
"R2"
|
|
26
|
+
],
|
|
27
|
+
"worstSeverity": "critical",
|
|
28
|
+
"fix": "Scope the write policy to the caller's tenant, or make it RESTRICTIVE.",
|
|
29
|
+
"id": "04-permissive-write-policy"
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_permissive_write CASCADE;
|
|
2
|
+
CREATE SCHEMA c_permissive_write;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_permissive_write TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_permissive_write.documents (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
tenant_id uuid NOT NULL,
|
|
8
|
+
body text
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX documents_tenant_idx ON c_permissive_write.documents (tenant_id);
|
|
11
|
+
ALTER TABLE c_permissive_write.documents ENABLE ROW LEVEL SECURITY;
|
|
12
|
+
ALTER TABLE c_permissive_write.documents FORCE ROW LEVEL SECURITY;
|
|
13
|
+
|
|
14
|
+
-- The flaw: a permissive FOR ALL policy whose body is the literal `true`.
|
|
15
|
+
-- Permissive policies OR together, so this defeats every scoped policy below.
|
|
16
|
+
CREATE POLICY documents_all ON c_permissive_write.documents FOR ALL TO corpus_user
|
|
17
|
+
USING (true) WITH CHECK (true);
|
|
18
|
+
CREATE POLICY documents_tenant ON c_permissive_write.documents FOR SELECT TO corpus_user
|
|
19
|
+
USING (tenant_id = (SELECT nullif(current_setting('jwt.claims.tenant_id', true), '')::uuid));
|
|
20
|
+
|
|
21
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON c_permissive_write.documents TO corpus_user;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "USING (true) SELECT policy on a table with private columns",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_public_read_policy"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A8",
|
|
19
|
+
"relation": "c_public_read_policy.profiles",
|
|
20
|
+
"note": "public-read is sometimes intended, which is why this is a low, not a critical"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"fix": "Expose a view with the public columns, or scope the policy to the owner.",
|
|
24
|
+
"id": "05-public-read-policy"
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_public_read_policy CASCADE;
|
|
2
|
+
CREATE SCHEMA c_public_read_policy;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_public_read_policy TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_public_read_policy.profiles (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
email text NOT NULL,
|
|
8
|
+
display_name text
|
|
9
|
+
);
|
|
10
|
+
ALTER TABLE c_public_read_policy.profiles ENABLE ROW LEVEL SECURITY;
|
|
11
|
+
ALTER TABLE c_public_read_policy.profiles FORCE ROW LEVEL SECURITY;
|
|
12
|
+
|
|
13
|
+
-- The flaw: every row is world-readable, including the email column.
|
|
14
|
+
CREATE POLICY profiles_read ON c_public_read_policy.profiles FOR SELECT TO corpus_anon USING (true);
|
|
15
|
+
|
|
16
|
+
GRANT SELECT ON c_public_read_policy.profiles TO corpus_anon;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "UPDATE policy with USING but no WITH CHECK",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_update_no_check"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A6",
|
|
19
|
+
"relation": "c_update_no_check.tasks",
|
|
20
|
+
"note": "row smuggling: the row can be updated out of the caller's own scope"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"fix": "Add WITH CHECK with the same predicate as USING.",
|
|
24
|
+
"id": "06-update-without-with-check"
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_update_no_check CASCADE;
|
|
2
|
+
CREATE SCHEMA c_update_no_check;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_update_no_check TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_update_no_check.tasks (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
owner_id uuid NOT NULL,
|
|
8
|
+
title text
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX tasks_owner_idx ON c_update_no_check.tasks (owner_id);
|
|
11
|
+
ALTER TABLE c_update_no_check.tasks ENABLE ROW LEVEL SECURITY;
|
|
12
|
+
ALTER TABLE c_update_no_check.tasks FORCE ROW LEVEL SECURITY;
|
|
13
|
+
|
|
14
|
+
-- The flaw: USING controls which rows may be updated; without WITH CHECK
|
|
15
|
+
-- the new row is unchecked, so a caller can reassign owner_id to someone else.
|
|
16
|
+
CREATE POLICY tasks_update ON c_update_no_check.tasks FOR UPDATE TO corpus_user
|
|
17
|
+
USING (owner_id = (SELECT nullif(current_setting('jwt.claims.user_id', true), '')::uuid));
|
|
18
|
+
|
|
19
|
+
GRANT UPDATE ON c_update_no_check.tasks TO corpus_user;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "RLS enabled with no policies \u2014 a silent deny-all",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_rls_no_policies"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A1",
|
|
19
|
+
"relation": "c_rls_no_policies.settings"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"code": "A5",
|
|
23
|
+
"relation": "c_rls_no_policies.settings",
|
|
24
|
+
"note": "the SELECT grant is dead: no policy can admit it"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"fix": "Add the policy that was meant to exist, or drop the grant and say the lock is deliberate.",
|
|
28
|
+
"id": "07-rls-enabled-no-policies"
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_rls_no_policies CASCADE;
|
|
2
|
+
CREATE SCHEMA c_rls_no_policies;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_rls_no_policies TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_rls_no_policies.settings (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
key text NOT NULL,
|
|
8
|
+
value text
|
|
9
|
+
);
|
|
10
|
+
-- The flaw (or the intent — the finding is fail-closed): RLS is on and no
|
|
11
|
+
-- policy exists, so every query returns zero rows for every non-owner.
|
|
12
|
+
ALTER TABLE c_rls_no_policies.settings ENABLE ROW LEVEL SECURITY;
|
|
13
|
+
ALTER TABLE c_rls_no_policies.settings FORCE ROW LEVEL SECURITY;
|
|
14
|
+
GRANT SELECT ON c_rls_no_policies.settings TO corpus_user;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "SELECT grant with no covering policy",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_select_no_policy"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A5",
|
|
19
|
+
"relation": "c_select_no_policy.orders",
|
|
20
|
+
"note": "fail-closed: nothing leaks, but the feature silently does not work"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"fix": "Add a FOR SELECT policy scoped to the caller.",
|
|
24
|
+
"id": "08-select-grant-no-policy"
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_select_no_policy CASCADE;
|
|
2
|
+
CREATE SCHEMA c_select_no_policy;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_select_no_policy TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_select_no_policy.orders (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
customer_id uuid NOT NULL,
|
|
8
|
+
total numeric
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX orders_customer_idx ON c_select_no_policy.orders (customer_id);
|
|
11
|
+
ALTER TABLE c_select_no_policy.orders ENABLE ROW LEVEL SECURITY;
|
|
12
|
+
ALTER TABLE c_select_no_policy.orders FORCE ROW LEVEL SECURITY;
|
|
13
|
+
|
|
14
|
+
-- Only the write path has a policy.
|
|
15
|
+
CREATE POLICY orders_insert ON c_select_no_policy.orders FOR INSERT TO corpus_user
|
|
16
|
+
WITH CHECK (customer_id = (SELECT nullif(current_setting('jwt.claims.user_id', true), '')::uuid));
|
|
17
|
+
|
|
18
|
+
-- The flaw: reads are granted but no policy covers SELECT, so the API
|
|
19
|
+
-- returns an empty list and looks like a data bug.
|
|
20
|
+
GRANT SELECT, INSERT ON c_select_no_policy.orders TO corpus_user;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Write grant with no covering policy",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_write_no_policy"
|
|
7
|
+
],
|
|
8
|
+
"roles": [
|
|
9
|
+
"corpus_anon",
|
|
10
|
+
"corpus_user"
|
|
11
|
+
],
|
|
12
|
+
"anonRoles": [
|
|
13
|
+
"corpus_anon"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"expect": [
|
|
17
|
+
{
|
|
18
|
+
"code": "A4",
|
|
19
|
+
"relation": "c_write_no_policy.comments"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"fix": "Add FOR INSERT / FOR UPDATE policies, or revoke the grants.",
|
|
23
|
+
"id": "09-write-grant-no-policy"
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
DROP SCHEMA IF EXISTS c_write_no_policy CASCADE;
|
|
2
|
+
CREATE SCHEMA c_write_no_policy;
|
|
3
|
+
GRANT USAGE ON SCHEMA c_write_no_policy TO corpus_user, corpus_anon;
|
|
4
|
+
|
|
5
|
+
CREATE TABLE c_write_no_policy.comments (
|
|
6
|
+
id bigserial PRIMARY KEY,
|
|
7
|
+
author_id uuid NOT NULL,
|
|
8
|
+
body text
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX comments_author_idx ON c_write_no_policy.comments (author_id);
|
|
11
|
+
ALTER TABLE c_write_no_policy.comments ENABLE ROW LEVEL SECURITY;
|
|
12
|
+
ALTER TABLE c_write_no_policy.comments FORCE ROW LEVEL SECURITY;
|
|
13
|
+
|
|
14
|
+
CREATE POLICY comments_read ON c_write_no_policy.comments FOR SELECT TO corpus_user
|
|
15
|
+
USING (author_id = (SELECT nullif(current_setting('jwt.claims.user_id', true), '')::uuid));
|
|
16
|
+
|
|
17
|
+
-- The flaw: INSERT/UPDATE are granted with no policy covering them.
|
|
18
|
+
GRANT SELECT, INSERT, UPDATE ON c_write_no_policy.comments TO corpus_user;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "RLS enabled without FORCE \u2014 the owner bypasses every policy",
|
|
3
|
+
"dimension": "security",
|
|
4
|
+
"exposure": {
|
|
5
|
+
"schemas": [
|
|
6
|
+
"c_owner_bypass"
|
|
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_owner_bypass.secrets"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"fix": "ALTER TABLE c_owner_bypass.secrets FORCE ROW LEVEL SECURITY.",
|
|
23
|
+
"id": "10-rls-not-forced"
|
|
24
|
+
}
|