diff --git a/Cargo.lock b/Cargo.lock index 39ec92fb0..e9cace6a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2696,6 +2696,7 @@ dependencies = [ "pgls_env", "pgls_fs", "pgls_lsp", + "pgls_test_utils", "pgls_text_edit", "pgls_workspace", "quick-junit", @@ -2703,6 +2704,7 @@ dependencies = [ "rustc-hash 2.1.0", "serde", "serde_json", + "sqlx", "tikv-jemallocator", "tokio", "tracing", @@ -3175,6 +3177,7 @@ dependencies = [ "pgls_query", "pgls_query_ext", "pgls_schema_cache", + "pgls_splinter", "pgls_statement_splitter", "pgls_suppressions", "pgls_test_utils", diff --git a/crates/pgls_cli/Cargo.toml b/crates/pgls_cli/Cargo.toml index 1b4cace9d..fa7fd8c82 100644 --- a/crates/pgls_cli/Cargo.toml +++ b/crates/pgls_cli/Cargo.toml @@ -52,8 +52,10 @@ mimalloc = "0.1.43" tikv-jemallocator = "0.6.0" [dev-dependencies] -assert_cmd = "2.0.16" -insta = { workspace = true, features = ["yaml"] } +assert_cmd = "2.0.16" +insta = { workspace = true, features = ["yaml"] } +pgls_test_utils = { workspace = true } +sqlx = { workspace = true } [lib] doctest = false diff --git a/crates/pgls_cli/src/commands/dblint.rs b/crates/pgls_cli/src/commands/dblint.rs index 592d26a25..b9ce13044 100644 --- a/crates/pgls_cli/src/commands/dblint.rs +++ b/crates/pgls_cli/src/commands/dblint.rs @@ -3,6 +3,7 @@ use std::time::Instant; use crate::cli_options::CliOptions; use crate::reporter::Report; use crate::{CliDiagnostic, CliSession, VcsIntegration}; +use pgls_analyse::RuleCategoriesBuilder; use pgls_configuration::PartialConfiguration; use pgls_diagnostics::Error; use pgls_workspace::features::diagnostics::{PullDatabaseDiagnosticsParams, PullDiagnosticsResult}; @@ -24,10 +25,17 @@ pub fn dblint( let start = Instant::now(); + let params = PullDatabaseDiagnosticsParams { + categories: RuleCategoriesBuilder::default().all().build(), + max_diagnostics, + only: Vec::new(), // Uses configuration settings + skip: Vec::new(), // Uses configuration settings + }; + let PullDiagnosticsResult { diagnostics, skipped_diagnostics, - } = workspace.pull_db_diagnostics(PullDatabaseDiagnosticsParams { max_diagnostics })?; + } = workspace.pull_db_diagnostics(params)?; let report = Report::new( diagnostics.into_iter().map(Error::from).collect(), diff --git a/crates/pgls_cli/src/commands/mod.rs b/crates/pgls_cli/src/commands/mod.rs index 3dac093e5..790f228e6 100644 --- a/crates/pgls_cli/src/commands/mod.rs +++ b/crates/pgls_cli/src/commands/mod.rs @@ -24,7 +24,7 @@ pub enum PgLSCommand { #[bpaf(command)] Version(#[bpaf(external(cli_options), hide_usage)] CliOptions), - /// Runs everything to the requested files. + /// Lints your database schema. #[bpaf(command)] Dblint { #[bpaf(external(partial_configuration), hide_usage, optional)] diff --git a/crates/pgls_cli/tests/assert_dblint.rs b/crates/pgls_cli/tests/assert_dblint.rs new file mode 100644 index 000000000..558449dae --- /dev/null +++ b/crates/pgls_cli/tests/assert_dblint.rs @@ -0,0 +1,117 @@ +use assert_cmd::Command; +use insta::assert_snapshot; +use sqlx::PgPool; +use std::process::ExitStatus; + +const BIN: &str = "postgres-language-server"; + +/// Get database URL from the pool's connect options +/// Uses the known docker-compose credentials (postgres:postgres) +fn get_database_url(pool: &PgPool) -> String { + let opts = pool.connect_options(); + format!( + "postgres://postgres:postgres@{}:{}/{}", + opts.get_host(), + opts.get_port(), + opts.get_database().unwrap_or("postgres") + ) +} + +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +async fn dblint_empty_database_snapshot(test_db: PgPool) { + let url = get_database_url(&test_db); + let output = run_dblint(&url, &[]); + assert_snapshot!(output); +} + +#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +async fn dblint_detects_issues_snapshot(test_db: PgPool) { + // Setup: create table without primary key (triggers noPrimaryKey rule) + sqlx::raw_sql("CREATE TABLE test_no_pk (id int, name text)") + .execute(&test_db) + .await + .expect("Failed to create test table"); + + let url = get_database_url(&test_db); + let output = run_dblint(&url, &[]); + assert_snapshot!(output); +} + +#[test] +#[cfg_attr( + target_os = "windows", + ignore = "snapshot expectations only validated on unix-like platforms" +)] +fn dblint_no_database_snapshot() { + // Test that dblint completes gracefully when no database is configured + let mut cmd = Command::cargo_bin(BIN).expect("binary not built"); + let output = cmd + .args(["dblint", "--disable-db", "--log-level", "none"]) + .output() + .expect("failed to run CLI"); + + let normalized = normalize_output( + output.status, + &String::from_utf8_lossy(&output.stdout), + &String::from_utf8_lossy(&output.stderr), + ); + assert_snapshot!(normalized); +} + +fn run_dblint(url: &str, args: &[&str]) -> String { + let mut cmd = Command::cargo_bin(BIN).expect("binary not built"); + let mut full_args = vec!["dblint", "--connection-string", url, "--log-level", "none"]; + full_args.extend_from_slice(args); + + let output = cmd.args(full_args).output().expect("failed to run CLI"); + + normalize_output( + output.status, + &String::from_utf8_lossy(&output.stdout), + &String::from_utf8_lossy(&output.stderr), + ) +} + +fn normalize_output(status: ExitStatus, stdout: &str, stderr: &str) -> String { + let normalized_stdout = normalize_durations(stdout); + let status_label = if status.success() { + "success" + } else { + "failure" + }; + format!( + "status: {status_label}\nstdout:\n{}\nstderr:\n{}\n", + normalized_stdout.trim_end(), + stderr.trim_end() + ) +} + +fn normalize_durations(input: &str) -> String { + let mut content = input.to_owned(); + + let mut search_start = 0; + while let Some(relative) = content[search_start..].find(" in ") { + let start = search_start + relative + 4; + if let Some(end_rel) = content[start..].find('.') { + let end = start + end_rel; + if content[start..end].chars().any(|c| c.is_ascii_digit()) { + content.replace_range(start..end, ""); + search_start = start + "".len() + 1; + continue; + } + search_start = end + 1; + } else { + break; + } + } + + content +} diff --git a/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_detects_issues_snapshot.snap b/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_detects_issues_snapshot.snap new file mode 100644 index 000000000..db0348a93 --- /dev/null +++ b/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_detects_issues_snapshot.snap @@ -0,0 +1,20 @@ +--- +source: crates/pgls_cli/tests/assert_dblint.rs +expression: output +snapshot_kind: text +--- +status: success +stdout: +Warning: Deprecated config filename detected. Use 'postgres-language-server.jsonc'. + +Command completed in . +stderr: +splinter/performance/noPrimaryKey ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Table \`public.test_no_pk\` does not have a primary key + + Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. + + i table: public.test_no_pk + + i Remediation: https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key diff --git a/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_empty_database_snapshot.snap b/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_empty_database_snapshot.snap new file mode 100644 index 000000000..449797f39 --- /dev/null +++ b/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_empty_database_snapshot.snap @@ -0,0 +1,11 @@ +--- +source: crates/pgls_cli/tests/assert_dblint.rs +expression: output +snapshot_kind: text +--- +status: success +stdout: +Warning: Deprecated config filename detected. Use 'postgres-language-server.jsonc'. + +Command completed in . +stderr: diff --git a/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_no_database_snapshot.snap b/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_no_database_snapshot.snap new file mode 100644 index 000000000..0cbb89765 --- /dev/null +++ b/crates/pgls_cli/tests/snapshots/assert_dblint__dblint_no_database_snapshot.snap @@ -0,0 +1,11 @@ +--- +source: crates/pgls_cli/tests/assert_dblint.rs +expression: normalized +snapshot_kind: text +--- +status: success +stdout: +Warning: Deprecated config filename detected. Use 'postgres-language-server.jsonc'. + +Command completed in . +stderr: diff --git a/crates/pgls_configuration/src/lib.rs b/crates/pgls_configuration/src/lib.rs index 1e8872af0..8d14a331b 100644 --- a/crates/pgls_configuration/src/lib.rs +++ b/crates/pgls_configuration/src/lib.rs @@ -42,6 +42,9 @@ pub use rules::{ RuleWithFixOptions, RuleWithOptions, }; use serde::{Deserialize, Serialize}; +use splinter::{ + PartialSplinterConfiguration, SplinterConfiguration, partial_splinter_configuration, +}; pub use typecheck::{ PartialTypecheckConfiguration, TypecheckConfiguration, partial_typecheck_configuration, }; @@ -86,6 +89,10 @@ pub struct Configuration { #[partial(type, bpaf(external(partial_linter_configuration), optional))] pub linter: LinterConfiguration, + /// The configuration for splinter + #[partial(type, bpaf(external(partial_splinter_configuration), optional))] + pub splinter: SplinterConfiguration, + /// The configuration for type checking #[partial(type, bpaf(external(partial_typecheck_configuration), optional))] pub typecheck: TypecheckConfiguration, @@ -127,6 +134,10 @@ impl PartialConfiguration { }), ..Default::default() }), + splinter: Some(PartialSplinterConfiguration { + enabled: Some(true), + ..Default::default() + }), typecheck: Some(PartialTypecheckConfiguration { ..Default::default() }), diff --git a/crates/pgls_configuration/src/linter/rules.rs b/crates/pgls_configuration/src/linter/rules.rs index 1abb465f5..a78cf28c9 100644 --- a/crates/pgls_configuration/src/linter/rules.rs +++ b/crates/pgls_configuration/src/linter/rules.rs @@ -46,6 +46,7 @@ impl std::str::FromStr for RuleGroup { } #[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] +#[cfg_attr(feature = "schema", schemars(rename = "LinterRules"))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Rules { #[doc = r" It enables the lint rules recommended by Postgres Language Server. `true` by default."] diff --git a/crates/pgls_configuration/src/splinter/mod.rs b/crates/pgls_configuration/src/splinter/mod.rs index e237ebae5..d0d795204 100644 --- a/crates/pgls_configuration/src/splinter/mod.rs +++ b/crates/pgls_configuration/src/splinter/mod.rs @@ -4,7 +4,6 @@ mod options; pub use options::SplinterRuleOptions; mod rules; -use biome_deserialize::StringSet; use biome_deserialize_macros::{Merge, Partial}; use bpaf::Bpaf; pub use rules::*; @@ -20,12 +19,6 @@ pub struct SplinterConfiguration { #[doc = r" List of rules"] #[partial(bpaf(pure(Default::default()), optional, hide))] pub rules: Rules, - #[doc = r" A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns."] - #[partial(bpaf(hide))] - pub ignore: StringSet, - #[doc = r" A list of Unix shell style patterns. The linter will include files/folders that will match these patterns."] - #[partial(bpaf(hide))] - pub include: StringSet, } impl SplinterConfiguration { pub const fn is_disabled(&self) -> bool { @@ -37,8 +30,6 @@ impl Default for SplinterConfiguration { Self { enabled: true, rules: Default::default(), - ignore: Default::default(), - include: Default::default(), } } } diff --git a/crates/pgls_configuration/src/splinter/rules.rs b/crates/pgls_configuration/src/splinter/rules.rs index b10f7978d..74a16e1e3 100644 --- a/crates/pgls_configuration/src/splinter/rules.rs +++ b/crates/pgls_configuration/src/splinter/rules.rs @@ -49,6 +49,7 @@ impl std::str::FromStr for RuleGroup { } #[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] +#[cfg_attr(feature = "schema", schemars(rename = "SplinterRules"))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Rules { #[doc = r" It enables the lint rules recommended by Postgres Language Server. `true` by default."] @@ -191,26 +192,26 @@ pub struct Performance { #[doc = r" It enables ALL rules for this group."] #[serde(skip_serializing_if = "Option::is_none")] pub all: Option, - #[doc = "/// # Auth RLS Initialization Plan /// /// Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// with policies as ( /// select /// nsp.nspname as schema_name, /// pb.tablename as table_name, /// pc.relrowsecurity as is_rls_active, /// polname as policy_name, /// polpermissive as is_permissive, -- if not, then restrictive /// (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles, /// case polcmd /// when 'r' then 'SELECT' /// when 'a' then 'INSERT' /// when 'w' then 'UPDATE' /// when 'd' then 'DELETE' /// when '*' then 'ALL' /// end as command, /// qual, /// with_check /// from /// pg_catalog.pg_policy pa /// join pg_catalog.pg_class pc /// on pa.polrelid = pc.oid /// join pg_catalog.pg_namespace nsp /// on pc.relnamespace = nsp.oid /// join pg_catalog.pg_policies pb /// on pc.relname = pb.tablename /// and nsp.nspname = pb.schemaname /// and pa.polname = pb.policyname /// ) /// select /// 'auth_rls_initplan' as \"name!\", /// 'Auth RLS Initialization Plan' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['PERFORMANCE'] as \"categories!\", /// 'Detects if calls to \\`current_setting()\\` and \\`auth.\\()\\` in RLS policies are being unnecessarily re-evaluated for each row' as \"description!\", /// format( /// 'Table \\`%s.%s\\` has a row level security policy \\`%s\\` that re-evaluates current_setting() or auth.\\() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \\`auth.\\()\\` with \\`(select auth.\\())\\`. See \\[docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.', /// schema_name, /// table_name, /// policy_name /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan' as \"remediation!\", /// jsonb_build_object( /// 'schema', schema_name, /// 'name', table_name, /// 'type', 'table' /// ) as \"metadata!\", /// format('auth_rls_init_plan_%s_%s_%s', schema_name, table_name, policy_name) as \"cache_key!\" /// from /// policies /// where /// is_rls_active /// -- NOTE: does not include realtime in support of monitoring policies on realtime.messages /// and schema_name not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and ( /// -- Example: auth.uid() /// ( /// qual like '%auth.uid()%' /// and lower(qual) not like '%select auth.uid()%' /// ) /// or ( /// qual like '%auth.jwt()%' /// and lower(qual) not like '%select auth.jwt()%' /// ) /// or ( /// qual like '%auth.role()%' /// and lower(qual) not like '%select auth.role()%' /// ) /// or ( /// qual like '%auth.email()%' /// and lower(qual) not like '%select auth.email()%' /// ) /// or ( /// qual like '%current\\_setting(%)%' /// and lower(qual) not like '%select current\\_setting(%)%' /// ) /// or ( /// with_check like '%auth.uid()%' /// and lower(with_check) not like '%select auth.uid()%' /// ) /// or ( /// with_check like '%auth.jwt()%' /// and lower(with_check) not like '%select auth.jwt()%' /// ) /// or ( /// with_check like '%auth.role()%' /// and lower(with_check) not like '%select auth.role()%' /// ) /// or ( /// with_check like '%auth.email()%' /// and lower(with_check) not like '%select auth.email()%' /// ) /// or ( /// with_check like '%current\\_setting(%)%' /// and lower(with_check) not like '%select current\\_setting(%)%' /// ) /// )) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"performance\": { /// \"authRlsInitplan\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan"] + #[doc = "Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row"] #[serde(skip_serializing_if = "Option::is_none")] pub auth_rls_initplan: Option>, - #[doc = "/// # Duplicate Index /// /// Detects cases where two ore more identical indexes exist. /// /// ## SQL Query /// /// sql /// ( /// select /// 'duplicate_index' as \"name!\", /// 'Duplicate Index' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['PERFORMANCE'] as \"categories!\", /// 'Detects cases where two ore more identical indexes exist.' as \"description!\", /// format( /// 'Table \\`%s.%s\\` has identical indexes %s. Drop all except one of them', /// n.nspname, /// c.relname, /// array_agg(pi.indexname order by pi.indexname) /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', case /// when c.relkind = 'r' then 'table' /// when c.relkind = 'm' then 'materialized view' /// else 'ERROR' /// end, /// 'indexes', array_agg(pi.indexname order by pi.indexname) /// ) as \"metadata!\", /// format( /// 'duplicate_index_%s_%s_%s', /// n.nspname, /// c.relname, /// array_agg(pi.indexname order by pi.indexname) /// ) as \"cache_key!\" /// from /// pg_catalog.pg_indexes pi /// join pg_catalog.pg_namespace n /// on n.nspname = pi.schemaname /// join pg_catalog.pg_class c /// on pi.tablename = c.relname /// and n.oid = c.relnamespace /// left join pg_catalog.pg_depend dep /// on c.oid = dep.objid /// and dep.deptype = 'e' /// where /// c.relkind in ('r', 'm') -- tables and materialized views /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and dep.objid is null -- exclude tables owned by extensions /// group by /// n.nspname, /// c.relkind, /// c.relname, /// replace(pi.indexdef, pi.indexname, '') /// having /// count(*) > 1) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"performance\": { /// \"duplicateIndex\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index"] + #[doc = "Duplicate Index: Detects cases where two ore more identical indexes exist."] #[serde(skip_serializing_if = "Option::is_none")] pub duplicate_index: Option>, - #[doc = "/// # Multiple Permissive Policies /// /// Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. /// /// ## SQL Query /// /// sql /// ( /// select /// 'multiple_permissive_policies' as \"name!\", /// 'Multiple Permissive Policies' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['PERFORMANCE'] as \"categories!\", /// 'Detects if multiple permissive row level security policies are present on a table for the same \\`role\\` and \\`action\\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.' as \"description!\", /// format( /// 'Table \\`%s.%s\\` has multiple permissive policies for role \\`%s\\` for action \\`%s\\`. Policies include \\`%s\\`', /// n.nspname, /// c.relname, /// r.rolname, /// act.cmd, /// array_agg(p.polname order by p.polname) /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'multiple_permissive_policies_%s_%s_%s_%s', /// n.nspname, /// c.relname, /// r.rolname, /// act.cmd /// ) as \"cache_key!\" /// from /// pg_catalog.pg_policy p /// join pg_catalog.pg_class c /// on p.polrelid = c.oid /// join pg_catalog.pg_namespace n /// on c.relnamespace = n.oid /// join pg_catalog.pg_roles r /// on p.polroles @> array\\[r.oid] /// or p.polroles = array\\[0::oid] /// left join pg_catalog.pg_depend dep /// on c.oid = dep.objid /// and dep.deptype = 'e', /// lateral ( /// select x.cmd /// from unnest(( /// select /// case p.polcmd /// when 'r' then array\\['SELECT'] /// when 'a' then array\\['INSERT'] /// when 'w' then array\\['UPDATE'] /// when 'd' then array\\['DELETE'] /// when '*' then array\\['SELECT', 'INSERT', 'UPDATE', 'DELETE'] /// else array\\['ERROR'] /// end as actions /// )) x(cmd) /// ) act(cmd) /// where /// c.relkind = 'r' -- regular tables /// and p.polpermissive -- policy is permissive /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and r.rolname not like 'pg_%' /// and r.rolname not like 'supabase%admin' /// and not r.rolbypassrls /// and dep.objid is null -- exclude tables owned by extensions /// group by /// n.nspname, /// c.relname, /// r.rolname, /// act.cmd /// having /// count(1) > 1) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"performance\": { /// \"multiplePermissivePolicies\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies"] + #[doc = "Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query."] #[serde(skip_serializing_if = "Option::is_none")] pub multiple_permissive_policies: Option>, - #[doc = "/// # No Primary Key /// /// Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. /// /// ## SQL Query /// /// sql /// ( /// select /// 'no_primary_key' as \"name!\", /// 'No Primary Key' as \"title!\", /// 'INFO' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['PERFORMANCE'] as \"categories!\", /// 'Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.' as \"description!\", /// format( /// 'Table \\`%s.%s\\` does not have a primary key', /// pgns.nspname, /// pgc.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key' as \"remediation!\", /// jsonb_build_object( /// 'schema', pgns.nspname, /// 'name', pgc.relname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'no_primary_key_%s_%s', /// pgns.nspname, /// pgc.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_class pgc /// join pg_catalog.pg_namespace pgns /// on pgns.oid = pgc.relnamespace /// left join pg_catalog.pg_index pgi /// on pgi.indrelid = pgc.oid /// left join pg_catalog.pg_depend dep /// on pgc.oid = dep.objid /// and dep.deptype = 'e' /// where /// pgc.relkind = 'r' -- regular tables /// and pgns.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and dep.objid is null -- exclude tables owned by extensions /// group by /// pgc.oid, /// pgns.nspname, /// pgc.relname /// having /// max(coalesce(pgi.indisprimary, false)::int) = 0) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"performance\": { /// \"noPrimaryKey\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key"] + #[doc = "No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale."] #[serde(skip_serializing_if = "Option::is_none")] pub no_primary_key: Option>, - #[doc = "/// # Table Bloat /// /// Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. /// /// ## SQL Query /// /// sql /// ( /// with constants as ( /// select current_setting('block_size')::numeric as bs, 23 as hdr, 4 as ma /// ), /// /// bloat_info as ( /// select /// ma, /// bs, /// schemaname, /// tablename, /// (datawidth + (hdr + ma - (case when hdr % ma = 0 then ma else hdr % ma end)))::numeric as datahdr, /// (maxfracsum * (nullhdr + ma - (case when nullhdr % ma = 0 then ma else nullhdr % ma end))) as nullhdr2 /// from ( /// select /// schemaname, /// tablename, /// hdr, /// ma, /// bs, /// sum((1 - null_frac) * avg_width) as datawidth, /// max(null_frac) as maxfracsum, /// hdr + ( /// select 1 + count(*) / 8 /// from pg_stats s2 /// where /// null_frac \\<> 0 /// and s2.schemaname = s.schemaname /// and s2.tablename = s.tablename /// ) as nullhdr /// from pg_stats s, constants /// group by 1, 2, 3, 4, 5 /// ) as foo /// ), /// /// table_bloat as ( /// select /// schemaname, /// tablename, /// cc.relpages, /// bs, /// ceil((cc.reltuples * ((datahdr + ma - /// (case when datahdr % ma = 0 then ma else datahdr % ma end)) + nullhdr2 + 4)) / (bs - 20::float)) as otta /// from /// bloat_info /// join pg_class cc /// on cc.relname = bloat_info.tablename /// join pg_namespace nn /// on cc.relnamespace = nn.oid /// and nn.nspname = bloat_info.schemaname /// and nn.nspname \\<> 'information_schema' /// where /// cc.relkind = 'r' /// and cc.relam = (select oid from pg_am where amname = 'heap') /// ), /// /// bloat_data as ( /// select /// 'table' as type, /// schemaname, /// tablename as object_name, /// round(case when otta = 0 then 0.0 else table_bloat.relpages / otta::numeric end, 1) as bloat, /// case when relpages \\< otta then 0 else (bs * (table_bloat.relpages - otta)::bigint)::bigint end as raw_waste /// from /// table_bloat /// ) /// /// select /// 'table_bloat' as \"name!\", /// 'Table Bloat' as \"title!\", /// 'INFO' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['PERFORMANCE'] as \"categories!\", /// 'Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.' as \"description!\", /// format( /// 'Table `%s`.`%s` has excessive bloat', /// bloat_data.schemaname, /// bloat_data.object_name /// ) as \"detail!\", /// 'Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.' as \"remediation!\", /// jsonb_build_object( /// 'schema', bloat_data.schemaname, /// 'name', bloat_data.object_name, /// 'type', bloat_data.type /// ) as \"metadata!\", /// format( /// 'table_bloat_%s_%s', /// bloat_data.schemaname, /// bloat_data.object_name /// ) as \"cache_key!\" /// from /// bloat_data /// where /// bloat > 70.0 /// and raw_waste > (20 * 1024 * 1024) -- filter for waste > 200 MB /// order by /// schemaname, /// object_name) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"performance\": { /// \"tableBloat\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: "] + #[doc = "Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster."] #[serde(skip_serializing_if = "Option::is_none")] pub table_bloat: Option>, - #[doc = "/// # Unindexed foreign keys /// /// Identifies foreign key constraints without a covering index, which can impact database performance. /// /// ## SQL Query /// /// sql /// with foreign_keys as ( /// select /// cl.relnamespace::regnamespace::text as schema_name, /// cl.relname as table_name, /// cl.oid as table_oid, /// ct.conname as fkey_name, /// ct.conkey as col_attnums /// from /// pg_catalog.pg_constraint ct /// join pg_catalog.pg_class cl -- fkey owning table /// on ct.conrelid = cl.oid /// left join pg_catalog.pg_depend d /// on d.objid = cl.oid /// and d.deptype = 'e' /// where /// ct.contype = 'f' -- foreign key constraints /// and d.objid is null -- exclude tables that are dependencies of extensions /// and cl.relnamespace::regnamespace::text not in ( /// 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions' /// ) /// ), /// index_ as ( /// select /// pi.indrelid as table_oid, /// indexrelid::regclass as index_, /// string_to_array(indkey::text, ' ')::smallint\\[] as col_attnums /// from /// pg_catalog.pg_index pi /// where /// indisvalid /// ) /// select /// 'unindexed_foreign_keys' as \"name!\", /// 'Unindexed foreign keys' as \"title!\", /// 'INFO' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['PERFORMANCE'] as \"categories!\", /// 'Identifies foreign key constraints without a covering index, which can impact database performance.' as \"description!\", /// format( /// 'Table `%s.%s` has a foreign key `%s` without a covering index. This can lead to suboptimal query performance.', /// fk.schema_name, /// fk.table_name, /// fk.fkey_name /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as \"remediation!\", /// jsonb_build_object( /// 'schema', fk.schema_name, /// 'name', fk.table_name, /// 'type', 'table', /// 'fkey_name', fk.fkey_name, /// 'fkey_columns', fk.col_attnums /// ) as \"metadata!\", /// format('unindexed_foreign_keys_%s_%s_%s', fk.schema_name, fk.table_name, fk.fkey_name) as \"cache_key!\" /// from /// foreign_keys fk /// left join index_ idx /// on fk.table_oid = idx.table_oid /// and fk.col_attnums = idx.col_attnums\\[1:array_length(fk.col_attnums, 1)] /// left join pg_catalog.pg_depend dep /// on idx.table_oid = dep.objid /// and dep.deptype = 'e' /// where /// idx.index_ is null /// and fk.schema_name not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and dep.objid is null -- exclude tables owned by extensions /// order by /// fk.schema_name, /// fk.table_name, /// fk.fkey_name /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"performance\": { /// \"unindexedForeignKeys\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys"] + #[doc = "Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance."] #[serde(skip_serializing_if = "Option::is_none")] pub unindexed_foreign_keys: Option>, - #[doc = "/// # Unused Index /// /// Detects if an index has never been used and may be a candidate for removal. /// /// ## SQL Query /// /// sql /// ( /// select /// 'unused_index' as \"name!\", /// 'Unused Index' as \"title!\", /// 'INFO' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['PERFORMANCE'] as \"categories!\", /// 'Detects if an index has never been used and may be a candidate for removal.' as \"description!\", /// format( /// 'Index \\`%s\\` on table \\`%s.%s\\` has not been used', /// psui.indexrelname, /// psui.schemaname, /// psui.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as \"remediation!\", /// jsonb_build_object( /// 'schema', psui.schemaname, /// 'name', psui.relname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'unused_index_%s_%s_%s', /// psui.schemaname, /// psui.relname, /// psui.indexrelname /// ) as \"cache_key!\" /// /// from /// pg_catalog.pg_stat_user_indexes psui /// join pg_catalog.pg_index pi /// on psui.indexrelid = pi.indexrelid /// left join pg_catalog.pg_depend dep /// on psui.relid = dep.objid /// and dep.deptype = 'e' /// where /// psui.idx_scan = 0 /// and not pi.indisunique /// and not pi.indisprimary /// and dep.objid is null -- exclude tables owned by extensions /// and psui.schemaname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// )) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"performance\": { /// \"unusedIndex\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index"] + #[doc = "Unused Index: Detects if an index has never been used and may be a candidate for removal."] #[serde(skip_serializing_if = "Option::is_none")] pub unused_index: Option>, } @@ -225,7 +226,15 @@ impl Performance { "unindexedForeignKeys", "unusedIndex", ]; - const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[]; + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), + ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), @@ -495,50 +504,50 @@ pub struct Security { #[doc = r" It enables ALL rules for this group."] #[serde(skip_serializing_if = "Option::is_none")] pub all: Option, - #[doc = "/// # Exposed Auth Users /// /// Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// select /// 'auth_users_exposed' as \"name!\", /// 'Exposed Auth Users' as \"title!\", /// 'ERROR' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.' as \"description!\", /// format( /// 'View/Materialized View \"%s\" in the public schema may expose \\`auth.users\\` data to anon or authenticated roles.', /// c.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'view', /// 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null) /// ) as \"metadata!\", /// format('auth_users_exposed_%s_%s', n.nspname, c.relname) as \"cache_key!\" /// from /// -- Identify the oid for auth.users /// pg_catalog.pg_class auth_users_pg_class /// join pg_catalog.pg_namespace auth_users_pg_namespace /// on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid /// and auth_users_pg_class.relname = 'users' /// and auth_users_pg_namespace.nspname = 'auth' /// -- Depends on auth.users /// join pg_catalog.pg_depend d /// on d.refobjid = auth_users_pg_class.oid /// join pg_catalog.pg_rewrite r /// on r.oid = d.objid /// join pg_catalog.pg_class c /// on c.oid = r.ev_class /// join pg_catalog.pg_namespace n /// on n.oid = c.relnamespace /// join pg_catalog.pg_class pg_class_auth_users /// on d.refobjid = pg_class_auth_users.oid /// where /// d.deptype = 'n' /// and ( /// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') /// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') /// ) /// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) /// -- Exclude self /// and c.relname \\<> '0002_auth_users_exposed' /// -- There are 3 insecure configurations /// and /// ( /// -- Materialized views don't support RLS so this is insecure by default /// (c.relkind in ('m')) -- m for materialized view /// or /// -- Standard View, accessible to anon or authenticated that is security_definer /// ( /// c.relkind = 'v' -- v for view /// -- Exclude security invoker views /// and not ( /// lower(coalesce(c.reloptions::text,'{}'))::text\\[] /// && array\\[ /// 'security_invoker=1', /// 'security_invoker=true', /// 'security_invoker=yes', /// 'security_invoker=on' /// ] /// ) /// ) /// or /// -- Standard View, security invoker, but no RLS enabled on auth.users /// ( /// c.relkind in ('v') -- v for view /// -- is security invoker /// and ( /// lower(coalesce(c.reloptions::text,'{}'))::text\\[] /// && array\\[ /// 'security_invoker=1', /// 'security_invoker=true', /// 'security_invoker=yes', /// 'security_invoker=on' /// ] /// ) /// and not pg_class_auth_users.relrowsecurity /// ) /// ) /// group by /// n.nspname, /// c.relname, /// c.oid) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"authUsersExposed\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed"] + #[doc = "Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security."] #[serde(skip_serializing_if = "Option::is_none")] pub auth_users_exposed: Option>, - #[doc = "/// # Extension in Public /// /// Detects extensions installed in the `public` schema. /// /// ## SQL Query /// /// sql /// ( /// select /// 'extension_in_public' as \"name!\", /// 'Extension in Public' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects extensions installed in the \\`public\\` schema.' as \"description!\", /// format( /// 'Extension \\`%s\\` is installed in the public schema. Move it to another schema.', /// pe.extname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public' as \"remediation!\", /// jsonb_build_object( /// 'schema', pe.extnamespace::regnamespace, /// 'name', pe.extname, /// 'type', 'extension' /// ) as \"metadata!\", /// format( /// 'extension_in_public_%s', /// pe.extname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_extension pe /// where /// -- plpgsql is installed by default in public and outside user control /// -- confirmed safe /// pe.extname not in ('plpgsql') /// -- Scoping this to public is not optimal. Ideally we would use the postgres /// -- search path. That currently isn't available via SQL. In other lints /// -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that /// -- is not appropriate here as it would evaluate true for the extensions schema /// and pe.extnamespace::regnamespace::text = 'public') /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"extensionInPublic\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public"] + #[doc = "Extension in Public: Detects extensions installed in the `public` schema."] #[serde(skip_serializing_if = "Option::is_none")] pub extension_in_public: Option>, - #[doc = "/// # Extension Versions Outdated /// /// Detects extensions that are not using the default (recommended) version. /// /// ## SQL Query /// /// sql /// ( /// select /// 'extension_versions_outdated' as \"name!\", /// 'Extension Versions Outdated' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects extensions that are not using the default (recommended) version.' as \"description!\", /// format( /// 'Extension `%s` is using version `%s` but version `%s` is available. Using outdated extension versions may expose the database to security vulnerabilities.', /// ext.name, /// ext.installed_version, /// ext.default_version /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated' as \"remediation!\", /// jsonb_build_object( /// 'extension_name', ext.name, /// 'installed_version', ext.installed_version, /// 'default_version', ext.default_version /// ) as \"metadata!\", /// format( /// 'extension_versions_outdated_%s_%s', /// ext.name, /// ext.installed_version /// ) as \"cache_key!\" /// from /// pg_catalog.pg_available_extensions ext /// join /// -- ignore versions not in pg_available_extension_versions /// -- e.g. residue of pg_upgrade /// pg_catalog.pg_available_extension_versions extv /// on extv.name = ext.name and extv.installed /// where /// ext.installed_version is not null /// and ext.default_version is not null /// and ext.installed_version != ext.default_version /// order by /// ext.name) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"extensionVersionsOutdated\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated"] + #[doc = "Extension Versions Outdated: Detects extensions that are not using the default (recommended) version."] #[serde(skip_serializing_if = "Option::is_none")] pub extension_versions_outdated: Option>, - #[doc = "/// # Foreign Key to Auth Unique Constraint /// /// Detects user defined foreign keys to unique constraints in the auth schema. /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// select /// 'fkey_to_auth_unique' as \"name!\", /// 'Foreign Key to Auth Unique Constraint' as \"title!\", /// 'ERROR' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects user defined foreign keys to unique constraints in the auth schema.' as \"description!\", /// format( /// 'Table `%s`.`%s` has a foreign key `%s` referencing an auth unique constraint', /// n.nspname, -- referencing schema /// c_rel.relname, -- referencing table /// c.conname -- fkey name /// ) as \"detail!\", /// 'Drop the foreign key constraint that references the auth schema.' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c_rel.relname, /// 'foreign_key', c.conname /// ) as \"metadata!\", /// format( /// 'fkey_to_auth_unique_%s_%s_%s', /// n.nspname, -- referencing schema /// c_rel.relname, -- referencing table /// c.conname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_constraint c /// join pg_catalog.pg_class c_rel /// on c.conrelid = c_rel.oid /// join pg_catalog.pg_namespace n /// on c_rel.relnamespace = n.oid /// join pg_catalog.pg_class ref_rel /// on c.confrelid = ref_rel.oid /// join pg_catalog.pg_namespace cn /// on ref_rel.relnamespace = cn.oid /// join pg_catalog.pg_index i /// on c.conindid = i.indexrelid /// where c.contype = 'f' /// and cn.nspname = 'auth' /// and i.indisunique /// and not i.indisprimary) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"fkeyToAuthUnique\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: "] + #[doc = "Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema."] #[serde(skip_serializing_if = "Option::is_none")] pub fkey_to_auth_unique: Option>, - #[doc = "/// # Foreign Table in API /// /// Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// select /// 'foreign_table_in_api' as \"name!\", /// 'Foreign Table in API' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.' as \"description!\", /// format( /// 'Foreign table \\`%s.%s\\` is accessible over APIs', /// n.nspname, /// c.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'foreign table' /// ) as \"metadata!\", /// format( /// 'foreign_table_in_api_%s_%s', /// n.nspname, /// c.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_class c /// join pg_catalog.pg_namespace n /// on n.oid = c.relnamespace /// left join pg_catalog.pg_depend dep /// on c.oid = dep.objid /// and dep.deptype = 'e' /// where /// c.relkind = 'f' /// and ( /// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') /// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') /// ) /// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and dep.objid is null) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"foreignTableInApi\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api"] + #[doc = "Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies."] #[serde(skip_serializing_if = "Option::is_none")] pub foreign_table_in_api: Option>, - #[doc = "/// # Function Search Path Mutable /// /// Detects functions where the search_path parameter is not set. /// /// ## SQL Query /// /// sql /// ( /// select /// 'function_search_path_mutable' as \"name!\", /// 'Function Search Path Mutable' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects functions where the search_path parameter is not set.' as \"description!\", /// format( /// 'Function \\`%s.%s\\` has a role mutable search_path', /// n.nspname, /// p.proname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', p.proname, /// 'type', 'function' /// ) as \"metadata!\", /// format( /// 'function_search_path_mutable_%s_%s_%s', /// n.nspname, /// p.proname, /// md5(p.prosrc) -- required when function is polymorphic /// ) as \"cache_key!\" /// from /// pg_catalog.pg_proc p /// join pg_catalog.pg_namespace n /// on p.pronamespace = n.oid /// left join pg_catalog.pg_depend dep /// on p.oid = dep.objid /// and dep.deptype = 'e' /// where /// n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and dep.objid is null -- exclude functions owned by extensions /// -- Search path not set /// and not exists ( /// select 1 /// from unnest(coalesce(p.proconfig, '{}')) as config /// where config like 'search_path=%' /// )) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"functionSearchPathMutable\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable"] + #[doc = "Function Search Path Mutable: Detects functions where the search_path parameter is not set."] #[serde(skip_serializing_if = "Option::is_none")] pub function_search_path_mutable: Option>, - #[doc = "/// # Insecure Queue Exposed in API /// /// Detects cases where an insecure Queue is exposed over Data APIs /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// select /// 'insecure_queue_exposed_in_api' as \"name!\", /// 'Insecure Queue Exposed in API' as \"title!\", /// 'ERROR' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects cases where an insecure Queue is exposed over Data APIs' as \"description!\", /// format( /// 'Table \\`%s.%s\\` is public, but RLS has not been enabled.', /// n.nspname, /// c.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'rls_disabled_in_public_%s_%s', /// n.nspname, /// c.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_class c /// join pg_catalog.pg_namespace n /// on c.relnamespace = n.oid /// where /// c.relkind in ('r', 'I') -- regular or partitioned tables /// and not c.relrowsecurity -- RLS is disabled /// and ( /// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') /// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') /// ) /// and n.nspname = 'pgmq' -- tables in the pgmq schema /// and c.relname like 'q_%' -- only queue tables /// -- Constant requirements /// and 'pgmq_public' = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"insecureQueueExposedInApi\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api"] + #[doc = "Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs"] #[serde(skip_serializing_if = "Option::is_none")] pub insecure_queue_exposed_in_api: Option>, - #[doc = "/// # Materialized View in API /// /// Detects materialized views that are accessible over the Data APIs. /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// select /// 'materialized_view_in_api' as \"name!\", /// 'Materialized View in API' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects materialized views that are accessible over the Data APIs.' as \"description!\", /// format( /// 'Materialized view \\`%s.%s\\` is selectable by anon or authenticated roles', /// n.nspname, /// c.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'materialized view' /// ) as \"metadata!\", /// format( /// 'materialized_view_in_api_%s_%s', /// n.nspname, /// c.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_class c /// join pg_catalog.pg_namespace n /// on n.oid = c.relnamespace /// left join pg_catalog.pg_depend dep /// on c.oid = dep.objid /// and dep.deptype = 'e' /// where /// c.relkind = 'm' /// and ( /// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') /// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') /// ) /// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and dep.objid is null) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"materializedViewInApi\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api"] + #[doc = "Materialized View in API: Detects materialized views that are accessible over the Data APIs."] #[serde(skip_serializing_if = "Option::is_none")] pub materialized_view_in_api: Option>, - #[doc = "/// # Policy Exists RLS Disabled /// /// Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. /// /// ## SQL Query /// /// sql /// ( /// select /// 'policy_exists_rls_disabled' as \"name!\", /// 'Policy Exists RLS Disabled' as \"title!\", /// 'ERROR' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.' as \"description!\", /// format( /// 'Table \\`%s.%s\\` has RLS policies but RLS is not enabled on the table. Policies include %s.', /// n.nspname, /// c.relname, /// array_agg(p.polname order by p.polname) /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'policy_exists_rls_disabled_%s_%s', /// n.nspname, /// c.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_policy p /// join pg_catalog.pg_class c /// on p.polrelid = c.oid /// join pg_catalog.pg_namespace n /// on c.relnamespace = n.oid /// left join pg_catalog.pg_depend dep /// on c.oid = dep.objid /// and dep.deptype = 'e' /// where /// c.relkind = 'r' -- regular tables /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// -- RLS is disabled /// and not c.relrowsecurity /// and dep.objid is null -- exclude tables owned by extensions /// group by /// n.nspname, /// c.relname) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"policyExistsRlsDisabled\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled"] + #[doc = "Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table."] #[serde(skip_serializing_if = "Option::is_none")] pub policy_exists_rls_disabled: Option>, - #[doc = "/// # RLS Disabled in Public /// /// Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// select /// 'rls_disabled_in_public' as \"name!\", /// 'RLS Disabled in Public' as \"title!\", /// 'ERROR' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST' as \"description!\", /// format( /// 'Table \\`%s.%s\\` is public, but RLS has not been enabled.', /// n.nspname, /// c.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'rls_disabled_in_public_%s_%s', /// n.nspname, /// c.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_class c /// join pg_catalog.pg_namespace n /// on c.relnamespace = n.oid /// where /// c.relkind = 'r' -- regular tables /// -- RLS is disabled /// and not c.relrowsecurity /// and ( /// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') /// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') /// ) /// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// )) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"rlsDisabledInPublic\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public"] + #[doc = "RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST"] #[serde(skip_serializing_if = "Option::is_none")] pub rls_disabled_in_public: Option>, - #[doc = "/// # RLS Enabled No Policy /// /// Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. /// /// ## SQL Query /// /// sql /// ( /// select /// 'rls_enabled_no_policy' as \"name!\", /// 'RLS Enabled No Policy' as \"title!\", /// 'INFO' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.' as \"description!\", /// format( /// 'Table \\`%s.%s\\` has RLS enabled, but no policies exist', /// n.nspname, /// c.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'rls_enabled_no_policy_%s_%s', /// n.nspname, /// c.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_class c /// left join pg_catalog.pg_policy p /// on p.polrelid = c.oid /// join pg_catalog.pg_namespace n /// on c.relnamespace = n.oid /// left join pg_catalog.pg_depend dep /// on c.oid = dep.objid /// and dep.deptype = 'e' /// where /// c.relkind = 'r' -- regular tables /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// -- RLS is enabled /// and c.relrowsecurity /// and p.polname is null /// and dep.objid is null -- exclude tables owned by extensions /// group by /// n.nspname, /// c.relname) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"rlsEnabledNoPolicy\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy"] + #[doc = "RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created."] #[serde(skip_serializing_if = "Option::is_none")] pub rls_enabled_no_policy: Option>, - #[doc = "/// # RLS references user metadata /// /// Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// with policies as ( /// select /// nsp.nspname as schema_name, /// pb.tablename as table_name, /// polname as policy_name, /// qual, /// with_check /// from /// pg_catalog.pg_policy pa /// join pg_catalog.pg_class pc /// on pa.polrelid = pc.oid /// join pg_catalog.pg_namespace nsp /// on pc.relnamespace = nsp.oid /// join pg_catalog.pg_policies pb /// on pc.relname = pb.tablename /// and nsp.nspname = pb.schemaname /// and pa.polname = pb.policyname /// ) /// select /// 'rls_references_user_metadata' as \"name!\", /// 'RLS references user metadata' as \"title!\", /// 'ERROR' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.' as \"description!\", /// format( /// 'Table \\`%s.%s\\` has a row level security policy \\`%s\\` that references Supabase Auth \\`user_metadata\\`. \\`user_metadata\\` is editable by end users and should never be used in a security context.', /// schema_name, /// table_name, /// policy_name /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata' as \"remediation!\", /// jsonb_build_object( /// 'schema', schema_name, /// 'name', table_name, /// 'type', 'table' /// ) as \"metadata!\", /// format('rls_references_user_metadata_%s_%s_%s', schema_name, table_name, policy_name) as \"cache_key!\" /// from /// policies /// where /// schema_name not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and ( /// -- Example: auth.jwt() -> 'user_metadata' /// -- False positives are possible, but it isn't practical to string match /// -- If false positive rate is too high, this expression can iterate /// qual like '%auth.jwt()%user_metadata%' /// or qual like '%current_setting(%request.jwt.claims%)%user_metadata%' /// or with_check like '%auth.jwt()%user_metadata%' /// or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%' /// )) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"rlsReferencesUserMetadata\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata"] + #[doc = "RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy."] #[serde(skip_serializing_if = "Option::is_none")] pub rls_references_user_metadata: Option>, - #[doc = "/// # Security Definer View /// /// Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user /// /// Note: This rule requires Supabase roles (anon, authenticated, service_role). /// It will be automatically skipped if these roles don't exist in your database. /// /// ## SQL Query /// /// sql /// ( /// select /// 'security_definer_view' as \"name!\", /// 'Security Definer View' as \"title!\", /// 'ERROR' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user' as \"description!\", /// format( /// 'View \\`%s.%s\\` is defined with the SECURITY DEFINER property', /// n.nspname, /// c.relname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'type', 'view' /// ) as \"metadata!\", /// format( /// 'security_definer_view_%s_%s', /// n.nspname, /// c.relname /// ) as \"cache_key!\" /// from /// pg_catalog.pg_class c /// join pg_catalog.pg_namespace n /// on n.oid = c.relnamespace /// left join pg_catalog.pg_depend dep /// on c.oid = dep.objid /// and dep.deptype = 'e' /// where /// c.relkind = 'v' /// and ( /// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') /// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') /// ) /// and substring(pg_catalog.version() from 'PostgreSQL (\\[0-9]+)') >= '15' -- security invoker was added in pg15 /// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))) /// and n.nspname not in ( /// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' /// ) /// and dep.objid is null -- exclude views owned by extensions /// and not ( /// lower(coalesce(c.reloptions::text,'{}'))::text\\[] /// && array\\[ /// 'security_invoker=1', /// 'security_invoker=true', /// 'security_invoker=yes', /// 'security_invoker=on' /// ] /// )) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"securityDefinerView\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view"] + #[doc = "Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user"] #[serde(skip_serializing_if = "Option::is_none")] pub security_definer_view: Option>, - #[doc = "/// # Unsupported reg types /// /// Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. /// /// ## SQL Query /// /// sql /// ( /// select /// 'unsupported_reg_types' as \"name!\", /// 'Unsupported reg types' as \"title!\", /// 'WARN' as \"level!\", /// 'EXTERNAL' as \"facing!\", /// array\\['SECURITY'] as \"categories!\", /// 'Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.' as \"description!\", /// format( /// 'Table \\`%s.%s\\` has a column \\`%s\\` with unsupported reg* type \\`%s\\`.', /// n.nspname, /// c.relname, /// a.attname, /// t.typname /// ) as \"detail!\", /// 'https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types' as \"remediation!\", /// jsonb_build_object( /// 'schema', n.nspname, /// 'name', c.relname, /// 'column', a.attname, /// 'type', 'table' /// ) as \"metadata!\", /// format( /// 'unsupported_reg_types_%s_%s_%s', /// n.nspname, /// c.relname, /// a.attname /// ) AS cache_key /// from /// pg_catalog.pg_attribute a /// join pg_catalog.pg_class c /// on a.attrelid = c.oid /// join pg_catalog.pg_namespace n /// on c.relnamespace = n.oid /// join pg_catalog.pg_type t /// on a.atttypid = t.oid /// join pg_catalog.pg_namespace tn /// on t.typnamespace = tn.oid /// where /// tn.nspname = 'pg_catalog' /// and t.typname in ('regcollation', 'regconfig', 'regdictionary', 'regnamespace', 'regoper', 'regoperator', 'regproc', 'regprocedure') /// and n.nspname not in ('pg_catalog', 'information_schema', 'pgsodium')) /// /// /// ## Configuration /// /// Enable or disable this rule in your configuration: /// /// json /// { /// \"splinter\": { /// \"rules\": { /// \"security\": { /// \"unsupportedRegTypes\": \"warn\" /// } /// } /// } /// } /// /// /// ## Remediation /// /// See: https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types"] + #[doc = "Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade."] #[serde(skip_serializing_if = "Option::is_none")] pub unsupported_reg_types: Option>, } @@ -560,7 +569,22 @@ impl Security { "securityDefinerView", "unsupportedRegTypes", ]; - const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[]; + const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), + ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), diff --git a/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs b/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs index bd5c4f34c..47cf62e78 100644 --- a/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs +++ b/crates/pgls_splinter/src/rules/performance/auth_rls_initplan.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Auth RLS Initialization Plan\n///\n/// Detects if calls to \\`current_setting()\\` and \\`auth.()\\` in RLS policies are being unnecessarily re-evaluated for each row\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// with policies as (\n/// select\n/// nsp.nspname as schema_name,\n/// pb.tablename as table_name,\n/// pc.relrowsecurity as is_rls_active,\n/// polname as policy_name,\n/// polpermissive as is_permissive, -- if not, then restrictive\n/// (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles,\n/// case polcmd\n/// when 'r' then 'SELECT'\n/// when 'a' then 'INSERT'\n/// when 'w' then 'UPDATE'\n/// when 'd' then 'DELETE'\n/// when '*' then 'ALL'\n/// end as command,\n/// qual,\n/// with_check\n/// from\n/// pg_catalog.pg_policy pa\n/// join pg_catalog.pg_class pc\n/// on pa.polrelid = pc.oid\n/// join pg_catalog.pg_namespace nsp\n/// on pc.relnamespace = nsp.oid\n/// join pg_catalog.pg_policies pb\n/// on pc.relname = pb.tablename\n/// and nsp.nspname = pb.schemaname\n/// and pa.polname = pb.policyname\n/// )\n/// select\n/// 'auth_rls_initplan' as \"name!\",\n/// 'Auth RLS Initialization Plan' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects if calls to \\`current_setting()\\` and \\`auth.()\\` in RLS policies are being unnecessarily re-evaluated for each row' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has a row level security policy \\`%s\\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \\`auth.()\\` with \\`(select auth.())\\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.',\n/// schema_name,\n/// table_name,\n/// policy_name\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', schema_name,\n/// 'name', table_name,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format('auth_rls_init_plan_%s_%s_%s', schema_name, table_name, policy_name) as \"cache_key!\"\n/// from\n/// policies\n/// where\n/// is_rls_active\n/// -- NOTE: does not include realtime in support of monitoring policies on realtime.messages\n/// and schema_name not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and (\n/// -- Example: auth.uid()\n/// (\n/// qual like '%auth.uid()%'\n/// and lower(qual) not like '%select auth.uid()%'\n/// )\n/// or (\n/// qual like '%auth.jwt()%'\n/// and lower(qual) not like '%select auth.jwt()%'\n/// )\n/// or (\n/// qual like '%auth.role()%'\n/// and lower(qual) not like '%select auth.role()%'\n/// )\n/// or (\n/// qual like '%auth.email()%'\n/// and lower(qual) not like '%select auth.email()%'\n/// )\n/// or (\n/// qual like '%current\\_setting(%)%'\n/// and lower(qual) not like '%select current\\_setting(%)%'\n/// )\n/// or (\n/// with_check like '%auth.uid()%'\n/// and lower(with_check) not like '%select auth.uid()%'\n/// )\n/// or (\n/// with_check like '%auth.jwt()%'\n/// and lower(with_check) not like '%select auth.jwt()%'\n/// )\n/// or (\n/// with_check like '%auth.role()%'\n/// and lower(with_check) not like '%select auth.role()%'\n/// )\n/// or (\n/// with_check like '%auth.email()%'\n/// and lower(with_check) not like '%select auth.email()%'\n/// )\n/// or (\n/// with_check like '%current\\_setting(%)%'\n/// and lower(with_check) not like '%select current\\_setting(%)%'\n/// )\n/// ))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"authRlsInitplan\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub AuthRlsInitplan { version : "1.0.0" , name : "authRlsInitplan" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Auth RLS Initialization Plan\n\nDetects if calls to \\`current_setting()\\` and \\`auth.()\\` in RLS policies are being unnecessarily re-evaluated for each row\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n pc.relrowsecurity as is_rls_active,\n polname as policy_name,\n polpermissive as is_permissive, -- if not, then restrictive\n (select array_agg(r::regrole) from unnest(polroles) as x(r)) as roles,\n case polcmd\n when 'r' then 'SELECT'\n when 'a' then 'INSERT'\n when 'w' then 'UPDATE'\n when 'd' then 'DELETE'\n when '*' then 'ALL'\n end as command,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n 'auth_rls_initplan' as \"name!\",\n 'Auth RLS Initialization Plan' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['PERFORMANCE'] as \"categories!\",\n 'Detects if calls to \\`current_setting()\\` and \\`auth.()\\` in RLS policies are being unnecessarily re-evaluated for each row' as \"description!\",\n format(\n 'Table \\`%s.%s\\` has a row level security policy \\`%s\\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \\`auth.()\\` with \\`(select auth.())\\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.',\n schema_name,\n table_name,\n policy_name\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan' as \"remediation!\",\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table'\n ) as \"metadata!\",\n format('auth_rls_init_plan_%s_%s_%s', schema_name, table_name, policy_name) as \"cache_key!\"\nfrom\n policies\nwhere\n is_rls_active\n -- NOTE: does not include realtime in support of monitoring policies on realtime.messages\n and schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and (\n -- Example: auth.uid()\n (\n qual like '%auth.uid()%'\n and lower(qual) not like '%select auth.uid()%'\n )\n or (\n qual like '%auth.jwt()%'\n and lower(qual) not like '%select auth.jwt()%'\n )\n or (\n qual like '%auth.role()%'\n and lower(qual) not like '%select auth.role()%'\n )\n or (\n qual like '%auth.email()%'\n and lower(qual) not like '%select auth.email()%'\n )\n or (\n qual like '%current\\_setting(%)%'\n and lower(qual) not like '%select current\\_setting(%)%'\n )\n or (\n with_check like '%auth.uid()%'\n and lower(with_check) not like '%select auth.uid()%'\n )\n or (\n with_check like '%auth.jwt()%'\n and lower(with_check) not like '%select auth.jwt()%'\n )\n or (\n with_check like '%auth.role()%'\n and lower(with_check) not like '%select auth.role()%'\n )\n or (\n with_check like '%auth.email()%'\n and lower(with_check) not like '%select auth.email()%'\n )\n or (\n with_check like '%current\\_setting(%)%'\n and lower(with_check) not like '%select current\\_setting(%)%'\n )\n ))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"performance\": {\n \"authRlsInitplan\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub AuthRlsInitplan { version : "1.0.0" , name : "authRlsInitplan" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for AuthRlsInitplan { const SQL_FILE_PATH: &'static str = "performance/auth_rls_initplan.sql"; const DESCRIPTION: &'static str = "Detects if calls to \\`current_setting()\\` and \\`auth.()\\` in RLS policies are being unnecessarily re-evaluated for each row"; diff --git a/crates/pgls_splinter/src/rules/performance/duplicate_index.rs b/crates/pgls_splinter/src/rules/performance/duplicate_index.rs index 841b6a5cb..c0db964ce 100644 --- a/crates/pgls_splinter/src/rules/performance/duplicate_index.rs +++ b/crates/pgls_splinter/src/rules/performance/duplicate_index.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Duplicate Index\n///\n/// Detects cases where two ore more identical indexes exist.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'duplicate_index' as \"name!\",\n/// 'Duplicate Index' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects cases where two ore more identical indexes exist.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has identical indexes %s. Drop all except one of them',\n/// n.nspname,\n/// c.relname,\n/// array_agg(pi.indexname order by pi.indexname)\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', case\n/// when c.relkind = 'r' then 'table'\n/// when c.relkind = 'm' then 'materialized view'\n/// else 'ERROR'\n/// end,\n/// 'indexes', array_agg(pi.indexname order by pi.indexname)\n/// ) as \"metadata!\",\n/// format(\n/// 'duplicate_index_%s_%s_%s',\n/// n.nspname,\n/// c.relname,\n/// array_agg(pi.indexname order by pi.indexname)\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_indexes pi\n/// join pg_catalog.pg_namespace n\n/// on n.nspname = pi.schemaname\n/// join pg_catalog.pg_class c\n/// on pi.tablename = c.relname\n/// and n.oid = c.relnamespace\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// c.relkind in ('r', 'm') -- tables and materialized views\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// n.nspname,\n/// c.relkind,\n/// c.relname,\n/// replace(pi.indexdef, pi.indexname, '')\n/// having\n/// count(*) > 1)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"duplicateIndex\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub DuplicateIndex { version : "1.0.0" , name : "duplicateIndex" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Duplicate Index\n\nDetects cases where two ore more identical indexes exist.\n\n## SQL Query\n\n```sql\n(\nselect\n 'duplicate_index' as \"name!\",\n 'Duplicate Index' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['PERFORMANCE'] as \"categories!\",\n 'Detects cases where two ore more identical indexes exist.' as \"description!\",\n format(\n 'Table \\`%s.%s\\` has identical indexes %s. Drop all except one of them',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0009_duplicate_index' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', case\n when c.relkind = 'r' then 'table'\n when c.relkind = 'm' then 'materialized view'\n else 'ERROR'\n end,\n 'indexes', array_agg(pi.indexname order by pi.indexname)\n ) as \"metadata!\",\n format(\n 'duplicate_index_%s_%s_%s',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_indexes pi\n join pg_catalog.pg_namespace n\n on n.nspname = pi.schemaname\n join pg_catalog.pg_class c\n on pi.tablename = c.relname\n and n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind in ('r', 'm') -- tables and materialized views\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relkind,\n c.relname,\n replace(pi.indexdef, pi.indexname, '')\nhaving\n count(*) > 1)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"performance\": {\n \"duplicateIndex\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub DuplicateIndex { version : "1.0.0" , name : "duplicateIndex" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for DuplicateIndex { const SQL_FILE_PATH: &'static str = "performance/duplicate_index.sql"; const DESCRIPTION: &'static str = "Detects cases where two ore more identical indexes exist."; diff --git a/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs b/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs index 227551a04..995ef8810 100644 --- a/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs +++ b/crates/pgls_splinter/src/rules/performance/multiple_permissive_policies.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Multiple Permissive Policies\n///\n/// Detects if multiple permissive row level security policies are present on a table for the same \\`role\\` and \\`action\\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'multiple_permissive_policies' as \"name!\",\n/// 'Multiple Permissive Policies' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects if multiple permissive row level security policies are present on a table for the same \\`role\\` and \\`action\\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has multiple permissive policies for role \\`%s\\` for action \\`%s\\`. Policies include \\`%s\\`',\n/// n.nspname,\n/// c.relname,\n/// r.rolname,\n/// act.cmd,\n/// array_agg(p.polname order by p.polname)\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'multiple_permissive_policies_%s_%s_%s_%s',\n/// n.nspname,\n/// c.relname,\n/// r.rolname,\n/// act.cmd\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_policy p\n/// join pg_catalog.pg_class c\n/// on p.polrelid = c.oid\n/// join pg_catalog.pg_namespace n\n/// on c.relnamespace = n.oid\n/// join pg_catalog.pg_roles r\n/// on p.polroles @> array[r.oid]\n/// or p.polroles = array[0::oid]\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e',\n/// lateral (\n/// select x.cmd\n/// from unnest((\n/// select\n/// case p.polcmd\n/// when 'r' then array['SELECT']\n/// when 'a' then array['INSERT']\n/// when 'w' then array['UPDATE']\n/// when 'd' then array['DELETE']\n/// when '*' then array['SELECT', 'INSERT', 'UPDATE', 'DELETE']\n/// else array['ERROR']\n/// end as actions\n/// )) x(cmd)\n/// ) act(cmd)\n/// where\n/// c.relkind = 'r' -- regular tables\n/// and p.polpermissive -- policy is permissive\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and r.rolname not like 'pg_%'\n/// and r.rolname not like 'supabase%admin'\n/// and not r.rolbypassrls\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// n.nspname,\n/// c.relname,\n/// r.rolname,\n/// act.cmd\n/// having\n/// count(1) > 1)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"multiplePermissivePolicies\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub MultiplePermissivePolicies { version : "1.0.0" , name : "multiplePermissivePolicies" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Multiple Permissive Policies\n\nDetects if multiple permissive row level security policies are present on a table for the same \\`role\\` and \\`action\\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.\n\n## SQL Query\n\n```sql\n(\nselect\n 'multiple_permissive_policies' as \"name!\",\n 'Multiple Permissive Policies' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['PERFORMANCE'] as \"categories!\",\n 'Detects if multiple permissive row level security policies are present on a table for the same \\`role\\` and \\`action\\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.' as \"description!\",\n format(\n 'Table \\`%s.%s\\` has multiple permissive policies for role \\`%s\\` for action \\`%s\\`. Policies include \\`%s\\`',\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd,\n array_agg(p.polname order by p.polname)\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'multiple_permissive_policies_%s_%s_%s_%s',\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n join pg_catalog.pg_roles r\n on p.polroles @> array[r.oid]\n or p.polroles = array[0::oid]\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e',\n lateral (\n select x.cmd\n from unnest((\n select\n case p.polcmd\n when 'r' then array['SELECT']\n when 'a' then array['INSERT']\n when 'w' then array['UPDATE']\n when 'd' then array['DELETE']\n when '*' then array['SELECT', 'INSERT', 'UPDATE', 'DELETE']\n else array['ERROR']\n end as actions\n )) x(cmd)\n ) act(cmd)\nwhere\n c.relkind = 'r' -- regular tables\n and p.polpermissive -- policy is permissive\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and r.rolname not like 'pg_%'\n and r.rolname not like 'supabase%admin'\n and not r.rolbypassrls\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname,\n r.rolname,\n act.cmd\nhaving\n count(1) > 1)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"performance\": {\n \"multiplePermissivePolicies\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub MultiplePermissivePolicies { version : "1.0.0" , name : "multiplePermissivePolicies" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for MultiplePermissivePolicies { const SQL_FILE_PATH: &'static str = "performance/multiple_permissive_policies.sql"; const DESCRIPTION: &'static str = "Detects if multiple permissive row level security policies are present on a table for the same \\`role\\` and \\`action\\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query."; diff --git a/crates/pgls_splinter/src/rules/performance/no_primary_key.rs b/crates/pgls_splinter/src/rules/performance/no_primary_key.rs index d65f9fc80..e31d50562 100644 --- a/crates/pgls_splinter/src/rules/performance/no_primary_key.rs +++ b/crates/pgls_splinter/src/rules/performance/no_primary_key.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # No Primary Key\n///\n/// Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'no_primary_key' as \"name!\",\n/// 'No Primary Key' as \"title!\",\n/// 'INFO' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` does not have a primary key',\n/// pgns.nspname,\n/// pgc.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', pgns.nspname,\n/// 'name', pgc.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'no_primary_key_%s_%s',\n/// pgns.nspname,\n/// pgc.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class pgc\n/// join pg_catalog.pg_namespace pgns\n/// on pgns.oid = pgc.relnamespace\n/// left join pg_catalog.pg_index pgi\n/// on pgi.indrelid = pgc.oid\n/// left join pg_catalog.pg_depend dep\n/// on pgc.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// pgc.relkind = 'r' -- regular tables\n/// and pgns.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// pgc.oid,\n/// pgns.nspname,\n/// pgc.relname\n/// having\n/// max(coalesce(pgi.indisprimary, false)::int) = 0)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"noPrimaryKey\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub NoPrimaryKey { version : "1.0.0" , name : "noPrimaryKey" , severity : pgls_diagnostics :: Severity :: Information , } } +::pgls_analyse::declare_rule! { # [doc = "# No Primary Key\n\nDetects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.\n\n## SQL Query\n\n```sql\n(\nselect\n 'no_primary_key' as \"name!\",\n 'No Primary Key' as \"title!\",\n 'INFO' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['PERFORMANCE'] as \"categories!\",\n 'Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.' as \"description!\",\n format(\n 'Table \\`%s.%s\\` does not have a primary key',\n pgns.nspname,\n pgc.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0004_no_primary_key' as \"remediation!\",\n jsonb_build_object(\n 'schema', pgns.nspname,\n 'name', pgc.relname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'no_primary_key_%s_%s',\n pgns.nspname,\n pgc.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_class pgc\n join pg_catalog.pg_namespace pgns\n on pgns.oid = pgc.relnamespace\n left join pg_catalog.pg_index pgi\n on pgi.indrelid = pgc.oid\n left join pg_catalog.pg_depend dep\n on pgc.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n pgc.relkind = 'r' -- regular tables\n and pgns.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n pgc.oid,\n pgns.nspname,\n pgc.relname\nhaving\n max(coalesce(pgi.indisprimary, false)::int) = 0)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"performance\": {\n \"noPrimaryKey\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub NoPrimaryKey { version : "1.0.0" , name : "noPrimaryKey" , severity : pgls_diagnostics :: Severity :: Information , recommended : true , } } impl SplinterRule for NoPrimaryKey { const SQL_FILE_PATH: &'static str = "performance/no_primary_key.sql"; const DESCRIPTION: &'static str = "Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale."; diff --git a/crates/pgls_splinter/src/rules/performance/table_bloat.rs b/crates/pgls_splinter/src/rules/performance/table_bloat.rs index 58e34d3b2..d286b1b9c 100644 --- a/crates/pgls_splinter/src/rules/performance/table_bloat.rs +++ b/crates/pgls_splinter/src/rules/performance/table_bloat.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Table Bloat\n///\n/// Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// with constants as (\n/// select current_setting('block_size')::numeric as bs, 23 as hdr, 4 as ma\n/// ),\n/// \n/// bloat_info as (\n/// select\n/// ma,\n/// bs,\n/// schemaname,\n/// tablename,\n/// (datawidth + (hdr + ma - (case when hdr % ma = 0 then ma else hdr % ma end)))::numeric as datahdr,\n/// (maxfracsum * (nullhdr + ma - (case when nullhdr % ma = 0 then ma else nullhdr % ma end))) as nullhdr2\n/// from (\n/// select\n/// schemaname,\n/// tablename,\n/// hdr,\n/// ma,\n/// bs,\n/// sum((1 - null_frac) * avg_width) as datawidth,\n/// max(null_frac) as maxfracsum,\n/// hdr + (\n/// select 1 + count(*) / 8\n/// from pg_stats s2\n/// where\n/// null_frac <> 0\n/// and s2.schemaname = s.schemaname\n/// and s2.tablename = s.tablename\n/// ) as nullhdr\n/// from pg_stats s, constants\n/// group by 1, 2, 3, 4, 5\n/// ) as foo\n/// ),\n/// \n/// table_bloat as (\n/// select\n/// schemaname,\n/// tablename,\n/// cc.relpages,\n/// bs,\n/// ceil((cc.reltuples * ((datahdr + ma -\n/// (case when datahdr % ma = 0 then ma else datahdr % ma end)) + nullhdr2 + 4)) / (bs - 20::float)) as otta\n/// from\n/// bloat_info\n/// join pg_class cc\n/// on cc.relname = bloat_info.tablename\n/// join pg_namespace nn\n/// on cc.relnamespace = nn.oid\n/// and nn.nspname = bloat_info.schemaname\n/// and nn.nspname <> 'information_schema'\n/// where\n/// cc.relkind = 'r'\n/// and cc.relam = (select oid from pg_am where amname = 'heap')\n/// ),\n/// \n/// bloat_data as (\n/// select\n/// 'table' as type,\n/// schemaname,\n/// tablename as object_name,\n/// round(case when otta = 0 then 0.0 else table_bloat.relpages / otta::numeric end, 1) as bloat,\n/// case when relpages < otta then 0 else (bs * (table_bloat.relpages - otta)::bigint)::bigint end as raw_waste\n/// from\n/// table_bloat\n/// )\n/// \n/// select\n/// 'table_bloat' as \"name!\",\n/// 'Table Bloat' as \"title!\",\n/// 'INFO' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.' as \"description!\",\n/// format(\n/// 'Table `%s`.`%s` has excessive bloat',\n/// bloat_data.schemaname,\n/// bloat_data.object_name\n/// ) as \"detail!\",\n/// 'Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', bloat_data.schemaname,\n/// 'name', bloat_data.object_name,\n/// 'type', bloat_data.type\n/// ) as \"metadata!\",\n/// format(\n/// 'table_bloat_%s_%s',\n/// bloat_data.schemaname,\n/// bloat_data.object_name\n/// ) as \"cache_key!\"\n/// from\n/// bloat_data\n/// where\n/// bloat > 70.0\n/// and raw_waste > (20 * 1024 * 1024) -- filter for waste > 200 MB\n/// order by\n/// schemaname,\n/// object_name)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"tableBloat\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub TableBloat { version : "1.0.0" , name : "tableBloat" , severity : pgls_diagnostics :: Severity :: Information , } } +::pgls_analyse::declare_rule! { # [doc = "# Table Bloat\n\nDetects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.\n\n## SQL Query\n\n```sql\n(\nwith constants as (\n select current_setting('block_size')::numeric as bs, 23 as hdr, 4 as ma\n),\n\nbloat_info as (\n select\n ma,\n bs,\n schemaname,\n tablename,\n (datawidth + (hdr + ma - (case when hdr % ma = 0 then ma else hdr % ma end)))::numeric as datahdr,\n (maxfracsum * (nullhdr + ma - (case when nullhdr % ma = 0 then ma else nullhdr % ma end))) as nullhdr2\n from (\n select\n schemaname,\n tablename,\n hdr,\n ma,\n bs,\n sum((1 - null_frac) * avg_width) as datawidth,\n max(null_frac) as maxfracsum,\n hdr + (\n select 1 + count(*) / 8\n from pg_stats s2\n where\n null_frac <> 0\n and s2.schemaname = s.schemaname\n and s2.tablename = s.tablename\n ) as nullhdr\n from pg_stats s, constants\n group by 1, 2, 3, 4, 5\n ) as foo\n),\n\ntable_bloat as (\n select\n schemaname,\n tablename,\n cc.relpages,\n bs,\n ceil((cc.reltuples * ((datahdr + ma -\n (case when datahdr % ma = 0 then ma else datahdr % ma end)) + nullhdr2 + 4)) / (bs - 20::float)) as otta\n from\n bloat_info\n join pg_class cc\n on cc.relname = bloat_info.tablename\n join pg_namespace nn\n on cc.relnamespace = nn.oid\n and nn.nspname = bloat_info.schemaname\n and nn.nspname <> 'information_schema'\n where\n cc.relkind = 'r'\n and cc.relam = (select oid from pg_am where amname = 'heap')\n),\n\nbloat_data as (\n select\n 'table' as type,\n schemaname,\n tablename as object_name,\n round(case when otta = 0 then 0.0 else table_bloat.relpages / otta::numeric end, 1) as bloat,\n case when relpages < otta then 0 else (bs * (table_bloat.relpages - otta)::bigint)::bigint end as raw_waste\n from\n table_bloat\n)\n\nselect\n 'table_bloat' as \"name!\",\n 'Table Bloat' as \"title!\",\n 'INFO' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['PERFORMANCE'] as \"categories!\",\n 'Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.' as \"description!\",\n format(\n 'Table `%s`.`%s` has excessive bloat',\n bloat_data.schemaname,\n bloat_data.object_name\n ) as \"detail!\",\n 'Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.' as \"remediation!\",\n jsonb_build_object(\n 'schema', bloat_data.schemaname,\n 'name', bloat_data.object_name,\n 'type', bloat_data.type\n ) as \"metadata!\",\n format(\n 'table_bloat_%s_%s',\n bloat_data.schemaname,\n bloat_data.object_name\n ) as \"cache_key!\"\nfrom\n bloat_data\nwhere\n bloat > 70.0\n and raw_waste > (20 * 1024 * 1024) -- filter for waste > 200 MB\norder by\n schemaname,\n object_name)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"performance\": {\n \"tableBloat\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub TableBloat { version : "1.0.0" , name : "tableBloat" , severity : pgls_diagnostics :: Severity :: Information , recommended : true , } } impl SplinterRule for TableBloat { const SQL_FILE_PATH: &'static str = "performance/table_bloat.sql"; const DESCRIPTION: &'static str = "Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster."; diff --git a/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs b/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs index c4542cb0f..832757562 100644 --- a/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs +++ b/crates/pgls_splinter/src/rules/performance/unindexed_foreign_keys.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Unindexed foreign keys\n///\n/// Identifies foreign key constraints without a covering index, which can impact database performance.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// with foreign_keys as (\n/// select\n/// cl.relnamespace::regnamespace::text as schema_name,\n/// cl.relname as table_name,\n/// cl.oid as table_oid,\n/// ct.conname as fkey_name,\n/// ct.conkey as col_attnums\n/// from\n/// pg_catalog.pg_constraint ct\n/// join pg_catalog.pg_class cl -- fkey owning table\n/// on ct.conrelid = cl.oid\n/// left join pg_catalog.pg_depend d\n/// on d.objid = cl.oid\n/// and d.deptype = 'e'\n/// where\n/// ct.contype = 'f' -- foreign key constraints\n/// and d.objid is null -- exclude tables that are dependencies of extensions\n/// and cl.relnamespace::regnamespace::text not in (\n/// 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions'\n/// )\n/// ),\n/// index_ as (\n/// select\n/// pi.indrelid as table_oid,\n/// indexrelid::regclass as index_,\n/// string_to_array(indkey::text, ' ')::smallint[] as col_attnums\n/// from\n/// pg_catalog.pg_index pi\n/// where\n/// indisvalid\n/// )\n/// select\n/// 'unindexed_foreign_keys' as \"name!\",\n/// 'Unindexed foreign keys' as \"title!\",\n/// 'INFO' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Identifies foreign key constraints without a covering index, which can impact database performance.' as \"description!\",\n/// format(\n/// 'Table `%s.%s` has a foreign key `%s` without a covering index. This can lead to suboptimal query performance.',\n/// fk.schema_name,\n/// fk.table_name,\n/// fk.fkey_name\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', fk.schema_name,\n/// 'name', fk.table_name,\n/// 'type', 'table',\n/// 'fkey_name', fk.fkey_name,\n/// 'fkey_columns', fk.col_attnums\n/// ) as \"metadata!\",\n/// format('unindexed_foreign_keys_%s_%s_%s', fk.schema_name, fk.table_name, fk.fkey_name) as \"cache_key!\"\n/// from\n/// foreign_keys fk\n/// left join index_ idx\n/// on fk.table_oid = idx.table_oid\n/// and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)]\n/// left join pg_catalog.pg_depend dep\n/// on idx.table_oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// idx.index_ is null\n/// and fk.schema_name not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null -- exclude tables owned by extensions\n/// order by\n/// fk.schema_name,\n/// fk.table_name,\n/// fk.fkey_name\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"unindexedForeignKeys\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub UnindexedForeignKeys { version : "1.0.0" , name : "unindexedForeignKeys" , severity : pgls_diagnostics :: Severity :: Information , } } +::pgls_analyse::declare_rule! { # [doc = "# Unindexed foreign keys\n\nIdentifies foreign key constraints without a covering index, which can impact database performance.\n\n## SQL Query\n\n```sql\nwith foreign_keys as (\n select\n cl.relnamespace::regnamespace::text as schema_name,\n cl.relname as table_name,\n cl.oid as table_oid,\n ct.conname as fkey_name,\n ct.conkey as col_attnums\n from\n pg_catalog.pg_constraint ct\n join pg_catalog.pg_class cl -- fkey owning table\n on ct.conrelid = cl.oid\n left join pg_catalog.pg_depend d\n on d.objid = cl.oid\n and d.deptype = 'e'\n where\n ct.contype = 'f' -- foreign key constraints\n and d.objid is null -- exclude tables that are dependencies of extensions\n and cl.relnamespace::regnamespace::text not in (\n 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'extensions'\n )\n),\nindex_ as (\n select\n pi.indrelid as table_oid,\n indexrelid::regclass as index_,\n string_to_array(indkey::text, ' ')::smallint[] as col_attnums\n from\n pg_catalog.pg_index pi\n where\n indisvalid\n)\nselect\n 'unindexed_foreign_keys' as \"name!\",\n 'Unindexed foreign keys' as \"title!\",\n 'INFO' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['PERFORMANCE'] as \"categories!\",\n 'Identifies foreign key constraints without a covering index, which can impact database performance.' as \"description!\",\n format(\n 'Table `%s.%s` has a foreign key `%s` without a covering index. This can lead to suboptimal query performance.',\n fk.schema_name,\n fk.table_name,\n fk.fkey_name\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as \"remediation!\",\n jsonb_build_object(\n 'schema', fk.schema_name,\n 'name', fk.table_name,\n 'type', 'table',\n 'fkey_name', fk.fkey_name,\n 'fkey_columns', fk.col_attnums\n ) as \"metadata!\",\n format('unindexed_foreign_keys_%s_%s_%s', fk.schema_name, fk.table_name, fk.fkey_name) as \"cache_key!\"\nfrom\n foreign_keys fk\n left join index_ idx\n on fk.table_oid = idx.table_oid\n and fk.col_attnums = idx.col_attnums[1:array_length(fk.col_attnums, 1)]\n left join pg_catalog.pg_depend dep\n on idx.table_oid = dep.objid\n and dep.deptype = 'e'\nwhere\n idx.index_ is null\n and fk.schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude tables owned by extensions\norder by\n fk.schema_name,\n fk.table_name,\n fk.fkey_name\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"performance\": {\n \"unindexedForeignKeys\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub UnindexedForeignKeys { version : "1.0.0" , name : "unindexedForeignKeys" , severity : pgls_diagnostics :: Severity :: Information , recommended : true , } } impl SplinterRule for UnindexedForeignKeys { const SQL_FILE_PATH: &'static str = "performance/unindexed_foreign_keys.sql"; const DESCRIPTION: &'static str = "Identifies foreign key constraints without a covering index, which can impact database performance."; diff --git a/crates/pgls_splinter/src/rules/performance/unused_index.rs b/crates/pgls_splinter/src/rules/performance/unused_index.rs index 3f0c4db67..bc9eed6b1 100644 --- a/crates/pgls_splinter/src/rules/performance/unused_index.rs +++ b/crates/pgls_splinter/src/rules/performance/unused_index.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Unused Index\n///\n/// Detects if an index has never been used and may be a candidate for removal.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'unused_index' as \"name!\",\n/// 'Unused Index' as \"title!\",\n/// 'INFO' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['PERFORMANCE'] as \"categories!\",\n/// 'Detects if an index has never been used and may be a candidate for removal.' as \"description!\",\n/// format(\n/// 'Index \\`%s\\` on table \\`%s.%s\\` has not been used',\n/// psui.indexrelname,\n/// psui.schemaname,\n/// psui.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', psui.schemaname,\n/// 'name', psui.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'unused_index_%s_%s_%s',\n/// psui.schemaname,\n/// psui.relname,\n/// psui.indexrelname\n/// ) as \"cache_key!\"\n/// \n/// from\n/// pg_catalog.pg_stat_user_indexes psui\n/// join pg_catalog.pg_index pi\n/// on psui.indexrelid = pi.indexrelid\n/// left join pg_catalog.pg_depend dep\n/// on psui.relid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// psui.idx_scan = 0\n/// and not pi.indisunique\n/// and not pi.indisprimary\n/// and dep.objid is null -- exclude tables owned by extensions\n/// and psui.schemaname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// ))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"performance\": {\n/// \"unusedIndex\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub UnusedIndex { version : "1.0.0" , name : "unusedIndex" , severity : pgls_diagnostics :: Severity :: Information , } } +::pgls_analyse::declare_rule! { # [doc = "# Unused Index\n\nDetects if an index has never been used and may be a candidate for removal.\n\n## SQL Query\n\n```sql\n(\nselect\n 'unused_index' as \"name!\",\n 'Unused Index' as \"title!\",\n 'INFO' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['PERFORMANCE'] as \"categories!\",\n 'Detects if an index has never been used and may be a candidate for removal.' as \"description!\",\n format(\n 'Index \\`%s\\` on table \\`%s.%s\\` has not been used',\n psui.indexrelname,\n psui.schemaname,\n psui.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0005_unused_index' as \"remediation!\",\n jsonb_build_object(\n 'schema', psui.schemaname,\n 'name', psui.relname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'unused_index_%s_%s_%s',\n psui.schemaname,\n psui.relname,\n psui.indexrelname\n ) as \"cache_key!\"\n\nfrom\n pg_catalog.pg_stat_user_indexes psui\n join pg_catalog.pg_index pi\n on psui.indexrelid = pi.indexrelid\n left join pg_catalog.pg_depend dep\n on psui.relid = dep.objid\n and dep.deptype = 'e'\nwhere\n psui.idx_scan = 0\n and not pi.indisunique\n and not pi.indisprimary\n and dep.objid is null -- exclude tables owned by extensions\n and psui.schemaname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n ))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"performance\": {\n \"unusedIndex\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub UnusedIndex { version : "1.0.0" , name : "unusedIndex" , severity : pgls_diagnostics :: Severity :: Information , recommended : true , } } impl SplinterRule for UnusedIndex { const SQL_FILE_PATH: &'static str = "performance/unused_index.sql"; const DESCRIPTION: &'static str = diff --git a/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs b/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs index 9f0372fdf..eb0eb7692 100644 --- a/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs +++ b/crates/pgls_splinter/src/rules/security/auth_users_exposed.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Exposed Auth Users\n///\n/// Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'auth_users_exposed' as \"name!\",\n/// 'Exposed Auth Users' as \"title!\",\n/// 'ERROR' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.' as \"description!\",\n/// format(\n/// 'View/Materialized View \"%s\" in the public schema may expose \\`auth.users\\` data to anon or authenticated roles.',\n/// c.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'view',\n/// 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null)\n/// ) as \"metadata!\",\n/// format('auth_users_exposed_%s_%s', n.nspname, c.relname) as \"cache_key!\"\n/// from\n/// -- Identify the oid for auth.users\n/// pg_catalog.pg_class auth_users_pg_class\n/// join pg_catalog.pg_namespace auth_users_pg_namespace\n/// on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid\n/// and auth_users_pg_class.relname = 'users'\n/// and auth_users_pg_namespace.nspname = 'auth'\n/// -- Depends on auth.users\n/// join pg_catalog.pg_depend d\n/// on d.refobjid = auth_users_pg_class.oid\n/// join pg_catalog.pg_rewrite r\n/// on r.oid = d.objid\n/// join pg_catalog.pg_class c\n/// on c.oid = r.ev_class\n/// join pg_catalog.pg_namespace n\n/// on n.oid = c.relnamespace\n/// join pg_catalog.pg_class pg_class_auth_users\n/// on d.refobjid = pg_class_auth_users.oid\n/// where\n/// d.deptype = 'n'\n/// and (\n/// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n/// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n/// )\n/// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n/// -- Exclude self\n/// and c.relname <> '0002_auth_users_exposed'\n/// -- There are 3 insecure configurations\n/// and\n/// (\n/// -- Materialized views don't support RLS so this is insecure by default\n/// (c.relkind in ('m')) -- m for materialized view\n/// or\n/// -- Standard View, accessible to anon or authenticated that is security_definer\n/// (\n/// c.relkind = 'v' -- v for view\n/// -- Exclude security invoker views\n/// and not (\n/// lower(coalesce(c.reloptions::text,'{}'))::text[]\n/// && array[\n/// 'security_invoker=1',\n/// 'security_invoker=true',\n/// 'security_invoker=yes',\n/// 'security_invoker=on'\n/// ]\n/// )\n/// )\n/// or\n/// -- Standard View, security invoker, but no RLS enabled on auth.users\n/// (\n/// c.relkind in ('v') -- v for view\n/// -- is security invoker\n/// and (\n/// lower(coalesce(c.reloptions::text,'{}'))::text[]\n/// && array[\n/// 'security_invoker=1',\n/// 'security_invoker=true',\n/// 'security_invoker=yes',\n/// 'security_invoker=on'\n/// ]\n/// )\n/// and not pg_class_auth_users.relrowsecurity\n/// )\n/// )\n/// group by\n/// n.nspname,\n/// c.relname,\n/// c.oid)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"authUsersExposed\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub AuthUsersExposed { version : "1.0.0" , name : "authUsersExposed" , severity : pgls_diagnostics :: Severity :: Error , } } +::pgls_analyse::declare_rule! { # [doc = "# Exposed Auth Users\n\nDetects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nselect\n 'auth_users_exposed' as \"name!\",\n 'Exposed Auth Users' as \"title!\",\n 'ERROR' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.' as \"description!\",\n format(\n 'View/Materialized View \"%s\" in the public schema may expose \\`auth.users\\` data to anon or authenticated roles.',\n c.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'view',\n 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null)\n ) as \"metadata!\",\n format('auth_users_exposed_%s_%s', n.nspname, c.relname) as \"cache_key!\"\nfrom\n -- Identify the oid for auth.users\n pg_catalog.pg_class auth_users_pg_class\n join pg_catalog.pg_namespace auth_users_pg_namespace\n on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid\n and auth_users_pg_class.relname = 'users'\n and auth_users_pg_namespace.nspname = 'auth'\n -- Depends on auth.users\n join pg_catalog.pg_depend d\n on d.refobjid = auth_users_pg_class.oid\n join pg_catalog.pg_rewrite r\n on r.oid = d.objid\n join pg_catalog.pg_class c\n on c.oid = r.ev_class\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n join pg_catalog.pg_class pg_class_auth_users\n on d.refobjid = pg_class_auth_users.oid\nwhere\n d.deptype = 'n'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n -- Exclude self\n and c.relname <> '0002_auth_users_exposed'\n -- There are 3 insecure configurations\n and\n (\n -- Materialized views don't support RLS so this is insecure by default\n (c.relkind in ('m')) -- m for materialized view\n or\n -- Standard View, accessible to anon or authenticated that is security_definer\n (\n c.relkind = 'v' -- v for view\n -- Exclude security invoker views\n and not (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n )\n )\n or\n -- Standard View, security invoker, but no RLS enabled on auth.users\n (\n c.relkind in ('v') -- v for view\n -- is security invoker\n and (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n )\n and not pg_class_auth_users.relrowsecurity\n )\n )\ngroup by\n n.nspname,\n c.relname,\n c.oid)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"authUsersExposed\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub AuthUsersExposed { version : "1.0.0" , name : "authUsersExposed" , severity : pgls_diagnostics :: Severity :: Error , recommended : true , } } impl SplinterRule for AuthUsersExposed { const SQL_FILE_PATH: &'static str = "security/auth_users_exposed.sql"; const DESCRIPTION: &'static str = "Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security."; diff --git a/crates/pgls_splinter/src/rules/security/extension_in_public.rs b/crates/pgls_splinter/src/rules/security/extension_in_public.rs index 02aa1adec..d40fc4c1a 100644 --- a/crates/pgls_splinter/src/rules/security/extension_in_public.rs +++ b/crates/pgls_splinter/src/rules/security/extension_in_public.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Extension in Public\n///\n/// Detects extensions installed in the \\`public\\` schema.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'extension_in_public' as \"name!\",\n/// 'Extension in Public' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects extensions installed in the \\`public\\` schema.' as \"description!\",\n/// format(\n/// 'Extension \\`%s\\` is installed in the public schema. Move it to another schema.',\n/// pe.extname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', pe.extnamespace::regnamespace,\n/// 'name', pe.extname,\n/// 'type', 'extension'\n/// ) as \"metadata!\",\n/// format(\n/// 'extension_in_public_%s',\n/// pe.extname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_extension pe\n/// where\n/// -- plpgsql is installed by default in public and outside user control\n/// -- confirmed safe\n/// pe.extname not in ('plpgsql')\n/// -- Scoping this to public is not optimal. Ideally we would use the postgres\n/// -- search path. That currently isn't available via SQL. In other lints\n/// -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that\n/// -- is not appropriate here as it would evaluate true for the extensions schema\n/// and pe.extnamespace::regnamespace::text = 'public')\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"extensionInPublic\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub ExtensionInPublic { version : "1.0.0" , name : "extensionInPublic" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Extension in Public\n\nDetects extensions installed in the \\`public\\` schema.\n\n## SQL Query\n\n```sql\n(\nselect\n 'extension_in_public' as \"name!\",\n 'Extension in Public' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects extensions installed in the \\`public\\` schema.' as \"description!\",\n format(\n 'Extension \\`%s\\` is installed in the public schema. Move it to another schema.',\n pe.extname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0014_extension_in_public' as \"remediation!\",\n jsonb_build_object(\n 'schema', pe.extnamespace::regnamespace,\n 'name', pe.extname,\n 'type', 'extension'\n ) as \"metadata!\",\n format(\n 'extension_in_public_%s',\n pe.extname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_extension pe\nwhere\n -- plpgsql is installed by default in public and outside user control\n -- confirmed safe\n pe.extname not in ('plpgsql')\n -- Scoping this to public is not optimal. Ideally we would use the postgres\n -- search path. That currently isn't available via SQL. In other lints\n -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that\n -- is not appropriate here as it would evaluate true for the extensions schema\n and pe.extnamespace::regnamespace::text = 'public')\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"extensionInPublic\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub ExtensionInPublic { version : "1.0.0" , name : "extensionInPublic" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for ExtensionInPublic { const SQL_FILE_PATH: &'static str = "security/extension_in_public.sql"; const DESCRIPTION: &'static str = "Detects extensions installed in the \\`public\\` schema."; diff --git a/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs b/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs index 066ba0b9c..7b6d31aac 100644 --- a/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs +++ b/crates/pgls_splinter/src/rules/security/extension_versions_outdated.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Extension Versions Outdated\n///\n/// Detects extensions that are not using the default (recommended) version.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'extension_versions_outdated' as \"name!\",\n/// 'Extension Versions Outdated' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects extensions that are not using the default (recommended) version.' as \"description!\",\n/// format(\n/// 'Extension `%s` is using version `%s` but version `%s` is available. Using outdated extension versions may expose the database to security vulnerabilities.',\n/// ext.name,\n/// ext.installed_version,\n/// ext.default_version\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated' as \"remediation!\",\n/// jsonb_build_object(\n/// 'extension_name', ext.name,\n/// 'installed_version', ext.installed_version,\n/// 'default_version', ext.default_version\n/// ) as \"metadata!\",\n/// format(\n/// 'extension_versions_outdated_%s_%s',\n/// ext.name,\n/// ext.installed_version\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_available_extensions ext\n/// join\n/// -- ignore versions not in pg_available_extension_versions\n/// -- e.g. residue of pg_upgrade\n/// pg_catalog.pg_available_extension_versions extv\n/// on extv.name = ext.name and extv.installed\n/// where\n/// ext.installed_version is not null\n/// and ext.default_version is not null\n/// and ext.installed_version != ext.default_version\n/// order by\n/// ext.name)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"extensionVersionsOutdated\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub ExtensionVersionsOutdated { version : "1.0.0" , name : "extensionVersionsOutdated" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Extension Versions Outdated\n\nDetects extensions that are not using the default (recommended) version.\n\n## SQL Query\n\n```sql\n(\nselect\n 'extension_versions_outdated' as \"name!\",\n 'Extension Versions Outdated' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects extensions that are not using the default (recommended) version.' as \"description!\",\n format(\n 'Extension `%s` is using version `%s` but version `%s` is available. Using outdated extension versions may expose the database to security vulnerabilities.',\n ext.name,\n ext.installed_version,\n ext.default_version\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0022_extension_versions_outdated' as \"remediation!\",\n jsonb_build_object(\n 'extension_name', ext.name,\n 'installed_version', ext.installed_version,\n 'default_version', ext.default_version\n ) as \"metadata!\",\n format(\n 'extension_versions_outdated_%s_%s',\n ext.name,\n ext.installed_version\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_available_extensions ext\njoin\n -- ignore versions not in pg_available_extension_versions\n -- e.g. residue of pg_upgrade\n pg_catalog.pg_available_extension_versions extv\n on extv.name = ext.name and extv.installed\nwhere\n ext.installed_version is not null\n and ext.default_version is not null\n and ext.installed_version != ext.default_version\norder by\n ext.name)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"extensionVersionsOutdated\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub ExtensionVersionsOutdated { version : "1.0.0" , name : "extensionVersionsOutdated" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for ExtensionVersionsOutdated { const SQL_FILE_PATH: &'static str = "security/extension_versions_outdated.sql"; const DESCRIPTION: &'static str = diff --git a/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs b/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs index 53dd84031..80cfac9d2 100644 --- a/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs +++ b/crates/pgls_splinter/src/rules/security/fkey_to_auth_unique.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Foreign Key to Auth Unique Constraint\n///\n/// Detects user defined foreign keys to unique constraints in the auth schema.\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'fkey_to_auth_unique' as \"name!\",\n/// 'Foreign Key to Auth Unique Constraint' as \"title!\",\n/// 'ERROR' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects user defined foreign keys to unique constraints in the auth schema.' as \"description!\",\n/// format(\n/// 'Table `%s`.`%s` has a foreign key `%s` referencing an auth unique constraint',\n/// n.nspname, -- referencing schema\n/// c_rel.relname, -- referencing table\n/// c.conname -- fkey name\n/// ) as \"detail!\",\n/// 'Drop the foreign key constraint that references the auth schema.' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c_rel.relname,\n/// 'foreign_key', c.conname\n/// ) as \"metadata!\",\n/// format(\n/// 'fkey_to_auth_unique_%s_%s_%s',\n/// n.nspname, -- referencing schema\n/// c_rel.relname, -- referencing table\n/// c.conname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_constraint c\n/// join pg_catalog.pg_class c_rel\n/// on c.conrelid = c_rel.oid\n/// join pg_catalog.pg_namespace n\n/// on c_rel.relnamespace = n.oid\n/// join pg_catalog.pg_class ref_rel\n/// on c.confrelid = ref_rel.oid\n/// join pg_catalog.pg_namespace cn\n/// on ref_rel.relnamespace = cn.oid\n/// join pg_catalog.pg_index i\n/// on c.conindid = i.indexrelid\n/// where c.contype = 'f'\n/// and cn.nspname = 'auth'\n/// and i.indisunique\n/// and not i.indisprimary)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"fkeyToAuthUnique\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub FkeyToAuthUnique { version : "1.0.0" , name : "fkeyToAuthUnique" , severity : pgls_diagnostics :: Severity :: Error , } } +::pgls_analyse::declare_rule! { # [doc = "# Foreign Key to Auth Unique Constraint\n\nDetects user defined foreign keys to unique constraints in the auth schema.\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nselect\n 'fkey_to_auth_unique' as \"name!\",\n 'Foreign Key to Auth Unique Constraint' as \"title!\",\n 'ERROR' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects user defined foreign keys to unique constraints in the auth schema.' as \"description!\",\n format(\n 'Table `%s`.`%s` has a foreign key `%s` referencing an auth unique constraint',\n n.nspname, -- referencing schema\n c_rel.relname, -- referencing table\n c.conname -- fkey name\n ) as \"detail!\",\n 'Drop the foreign key constraint that references the auth schema.' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c_rel.relname,\n 'foreign_key', c.conname\n ) as \"metadata!\",\n format(\n 'fkey_to_auth_unique_%s_%s_%s',\n n.nspname, -- referencing schema\n c_rel.relname, -- referencing table\n c.conname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_constraint c\n join pg_catalog.pg_class c_rel\n on c.conrelid = c_rel.oid\n join pg_catalog.pg_namespace n\n on c_rel.relnamespace = n.oid\n join pg_catalog.pg_class ref_rel\n on c.confrelid = ref_rel.oid\n join pg_catalog.pg_namespace cn\n on ref_rel.relnamespace = cn.oid\n join pg_catalog.pg_index i\n on c.conindid = i.indexrelid\nwhere c.contype = 'f'\n and cn.nspname = 'auth'\n and i.indisunique\n and not i.indisprimary)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"fkeyToAuthUnique\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub FkeyToAuthUnique { version : "1.0.0" , name : "fkeyToAuthUnique" , severity : pgls_diagnostics :: Severity :: Error , recommended : true , } } impl SplinterRule for FkeyToAuthUnique { const SQL_FILE_PATH: &'static str = "security/fkey_to_auth_unique.sql"; const DESCRIPTION: &'static str = diff --git a/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs b/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs index 808b07742..a9d5fd704 100644 --- a/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs +++ b/crates/pgls_splinter/src/rules/security/foreign_table_in_api.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Foreign Table in API\n///\n/// Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'foreign_table_in_api' as \"name!\",\n/// 'Foreign Table in API' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.' as \"description!\",\n/// format(\n/// 'Foreign table \\`%s.%s\\` is accessible over APIs',\n/// n.nspname,\n/// c.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'foreign table'\n/// ) as \"metadata!\",\n/// format(\n/// 'foreign_table_in_api_%s_%s',\n/// n.nspname,\n/// c.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class c\n/// join pg_catalog.pg_namespace n\n/// on n.oid = c.relnamespace\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// c.relkind = 'f'\n/// and (\n/// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n/// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n/// )\n/// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"foreignTableInApi\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub ForeignTableInApi { version : "1.0.0" , name : "foreignTableInApi" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Foreign Table in API\n\nDetects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nselect\n 'foreign_table_in_api' as \"name!\",\n 'Foreign Table in API' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.' as \"description!\",\n format(\n 'Foreign table \\`%s.%s\\` is accessible over APIs',\n n.nspname,\n c.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0017_foreign_table_in_api' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'foreign table'\n ) as \"metadata!\",\n format(\n 'foreign_table_in_api_%s_%s',\n n.nspname,\n c.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'f'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"foreignTableInApi\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub ForeignTableInApi { version : "1.0.0" , name : "foreignTableInApi" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for ForeignTableInApi { const SQL_FILE_PATH: &'static str = "security/foreign_table_in_api.sql"; const DESCRIPTION: &'static str = "Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies."; diff --git a/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs b/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs index 6ea976151..fab4776c2 100644 --- a/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs +++ b/crates/pgls_splinter/src/rules/security/function_search_path_mutable.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Function Search Path Mutable\n///\n/// Detects functions where the search_path parameter is not set.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'function_search_path_mutable' as \"name!\",\n/// 'Function Search Path Mutable' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects functions where the search_path parameter is not set.' as \"description!\",\n/// format(\n/// 'Function \\`%s.%s\\` has a role mutable search_path',\n/// n.nspname,\n/// p.proname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', p.proname,\n/// 'type', 'function'\n/// ) as \"metadata!\",\n/// format(\n/// 'function_search_path_mutable_%s_%s_%s',\n/// n.nspname,\n/// p.proname,\n/// md5(p.prosrc) -- required when function is polymorphic\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_proc p\n/// join pg_catalog.pg_namespace n\n/// on p.pronamespace = n.oid\n/// left join pg_catalog.pg_depend dep\n/// on p.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null -- exclude functions owned by extensions\n/// -- Search path not set\n/// and not exists (\n/// select 1\n/// from unnest(coalesce(p.proconfig, '{}')) as config\n/// where config like 'search_path=%'\n/// ))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"functionSearchPathMutable\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub FunctionSearchPathMutable { version : "1.0.0" , name : "functionSearchPathMutable" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Function Search Path Mutable\n\nDetects functions where the search_path parameter is not set.\n\n## SQL Query\n\n```sql\n(\nselect\n 'function_search_path_mutable' as \"name!\",\n 'Function Search Path Mutable' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects functions where the search_path parameter is not set.' as \"description!\",\n format(\n 'Function \\`%s.%s\\` has a role mutable search_path',\n n.nspname,\n p.proname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', p.proname,\n 'type', 'function'\n ) as \"metadata!\",\n format(\n 'function_search_path_mutable_%s_%s_%s',\n n.nspname,\n p.proname,\n md5(p.prosrc) -- required when function is polymorphic\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_proc p\n join pg_catalog.pg_namespace n\n on p.pronamespace = n.oid\n left join pg_catalog.pg_depend dep\n on p.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude functions owned by extensions\n -- Search path not set\n and not exists (\n select 1\n from unnest(coalesce(p.proconfig, '{}')) as config\n where config like 'search_path=%'\n ))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"functionSearchPathMutable\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub FunctionSearchPathMutable { version : "1.0.0" , name : "functionSearchPathMutable" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for FunctionSearchPathMutable { const SQL_FILE_PATH: &'static str = "security/function_search_path_mutable.sql"; const DESCRIPTION: &'static str = diff --git a/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs b/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs index a739cb138..d347abff6 100644 --- a/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs +++ b/crates/pgls_splinter/src/rules/security/insecure_queue_exposed_in_api.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Insecure Queue Exposed in API\n///\n/// Detects cases where an insecure Queue is exposed over Data APIs\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'insecure_queue_exposed_in_api' as \"name!\",\n/// 'Insecure Queue Exposed in API' as \"title!\",\n/// 'ERROR' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects cases where an insecure Queue is exposed over Data APIs' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` is public, but RLS has not been enabled.',\n/// n.nspname,\n/// c.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'rls_disabled_in_public_%s_%s',\n/// n.nspname,\n/// c.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class c\n/// join pg_catalog.pg_namespace n\n/// on c.relnamespace = n.oid\n/// where\n/// c.relkind in ('r', 'I') -- regular or partitioned tables\n/// and not c.relrowsecurity -- RLS is disabled\n/// and (\n/// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n/// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n/// )\n/// and n.nspname = 'pgmq' -- tables in the pgmq schema\n/// and c.relname like 'q_%' -- only queue tables\n/// -- Constant requirements\n/// and 'pgmq_public' = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"insecureQueueExposedInApi\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub InsecureQueueExposedInApi { version : "1.0.0" , name : "insecureQueueExposedInApi" , severity : pgls_diagnostics :: Severity :: Error , } } +::pgls_analyse::declare_rule! { # [doc = "# Insecure Queue Exposed in API\n\nDetects cases where an insecure Queue is exposed over Data APIs\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nselect\n 'insecure_queue_exposed_in_api' as \"name!\",\n 'Insecure Queue Exposed in API' as \"title!\",\n 'ERROR' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects cases where an insecure Queue is exposed over Data APIs' as \"description!\",\n format(\n 'Table \\`%s.%s\\` is public, but RLS has not been enabled.',\n n.nspname,\n c.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0019_insecure_queue_exposed_in_api' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'rls_disabled_in_public_%s_%s',\n n.nspname,\n c.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind in ('r', 'I') -- regular or partitioned tables\n and not c.relrowsecurity -- RLS is disabled\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = 'pgmq' -- tables in the pgmq schema\n and c.relname like 'q_%' -- only queue tables\n -- Constant requirements\n and 'pgmq_public' = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ','))))))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"insecureQueueExposedInApi\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub InsecureQueueExposedInApi { version : "1.0.0" , name : "insecureQueueExposedInApi" , severity : pgls_diagnostics :: Severity :: Error , recommended : true , } } impl SplinterRule for InsecureQueueExposedInApi { const SQL_FILE_PATH: &'static str = "security/insecure_queue_exposed_in_api.sql"; const DESCRIPTION: &'static str = diff --git a/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs b/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs index b659eafd5..d86bcda27 100644 --- a/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs +++ b/crates/pgls_splinter/src/rules/security/materialized_view_in_api.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Materialized View in API\n///\n/// Detects materialized views that are accessible over the Data APIs.\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'materialized_view_in_api' as \"name!\",\n/// 'Materialized View in API' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects materialized views that are accessible over the Data APIs.' as \"description!\",\n/// format(\n/// 'Materialized view \\`%s.%s\\` is selectable by anon or authenticated roles',\n/// n.nspname,\n/// c.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'materialized view'\n/// ) as \"metadata!\",\n/// format(\n/// 'materialized_view_in_api_%s_%s',\n/// n.nspname,\n/// c.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class c\n/// join pg_catalog.pg_namespace n\n/// on n.oid = c.relnamespace\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// c.relkind = 'm'\n/// and (\n/// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n/// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n/// )\n/// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"materializedViewInApi\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub MaterializedViewInApi { version : "1.0.0" , name : "materializedViewInApi" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Materialized View in API\n\nDetects materialized views that are accessible over the Data APIs.\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nselect\n 'materialized_view_in_api' as \"name!\",\n 'Materialized View in API' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects materialized views that are accessible over the Data APIs.' as \"description!\",\n format(\n 'Materialized view \\`%s.%s\\` is selectable by anon or authenticated roles',\n n.nspname,\n c.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0016_materialized_view_in_api' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'materialized view'\n ) as \"metadata!\",\n format(\n 'materialized_view_in_api_%s_%s',\n n.nspname,\n c.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'm'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"materializedViewInApi\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub MaterializedViewInApi { version : "1.0.0" , name : "materializedViewInApi" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for MaterializedViewInApi { const SQL_FILE_PATH: &'static str = "security/materialized_view_in_api.sql"; const DESCRIPTION: &'static str = diff --git a/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs b/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs index 60cfc98a2..88429c992 100644 --- a/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs +++ b/crates/pgls_splinter/src/rules/security/policy_exists_rls_disabled.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Policy Exists RLS Disabled\n///\n/// Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'policy_exists_rls_disabled' as \"name!\",\n/// 'Policy Exists RLS Disabled' as \"title!\",\n/// 'ERROR' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has RLS policies but RLS is not enabled on the table. Policies include %s.',\n/// n.nspname,\n/// c.relname,\n/// array_agg(p.polname order by p.polname)\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'policy_exists_rls_disabled_%s_%s',\n/// n.nspname,\n/// c.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_policy p\n/// join pg_catalog.pg_class c\n/// on p.polrelid = c.oid\n/// join pg_catalog.pg_namespace n\n/// on c.relnamespace = n.oid\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// c.relkind = 'r' -- regular tables\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// -- RLS is disabled\n/// and not c.relrowsecurity\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// n.nspname,\n/// c.relname)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"policyExistsRlsDisabled\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub PolicyExistsRlsDisabled { version : "1.0.0" , name : "policyExistsRlsDisabled" , severity : pgls_diagnostics :: Severity :: Error , } } +::pgls_analyse::declare_rule! { # [doc = "# Policy Exists RLS Disabled\n\nDetects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.\n\n## SQL Query\n\n```sql\n(\nselect\n 'policy_exists_rls_disabled' as \"name!\",\n 'Policy Exists RLS Disabled' as \"title!\",\n 'ERROR' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.' as \"description!\",\n format(\n 'Table \\`%s.%s\\` has RLS policies but RLS is not enabled on the table. Policies include %s.',\n n.nspname,\n c.relname,\n array_agg(p.polname order by p.polname)\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0007_policy_exists_rls_disabled' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'policy_exists_rls_disabled_%s_%s',\n n.nspname,\n c.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_policy p\n join pg_catalog.pg_class c\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'r' -- regular tables\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- RLS is disabled\n and not c.relrowsecurity\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"policyExistsRlsDisabled\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub PolicyExistsRlsDisabled { version : "1.0.0" , name : "policyExistsRlsDisabled" , severity : pgls_diagnostics :: Severity :: Error , recommended : true , } } impl SplinterRule for PolicyExistsRlsDisabled { const SQL_FILE_PATH: &'static str = "security/policy_exists_rls_disabled.sql"; const DESCRIPTION: &'static str = "Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table."; diff --git a/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs b/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs index b441f4619..8afdd604b 100644 --- a/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs +++ b/crates/pgls_splinter/src/rules/security/rls_disabled_in_public.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # RLS Disabled in Public\n///\n/// Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'rls_disabled_in_public' as \"name!\",\n/// 'RLS Disabled in Public' as \"title!\",\n/// 'ERROR' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` is public, but RLS has not been enabled.',\n/// n.nspname,\n/// c.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'rls_disabled_in_public_%s_%s',\n/// n.nspname,\n/// c.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class c\n/// join pg_catalog.pg_namespace n\n/// on c.relnamespace = n.oid\n/// where\n/// c.relkind = 'r' -- regular tables\n/// -- RLS is disabled\n/// and not c.relrowsecurity\n/// and (\n/// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n/// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n/// )\n/// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// ))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"rlsDisabledInPublic\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub RlsDisabledInPublic { version : "1.0.0" , name : "rlsDisabledInPublic" , severity : pgls_diagnostics :: Severity :: Error , } } +::pgls_analyse::declare_rule! { # [doc = "# RLS Disabled in Public\n\nDetects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nselect\n 'rls_disabled_in_public' as \"name!\",\n 'RLS Disabled in Public' as \"title!\",\n 'ERROR' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST' as \"description!\",\n format(\n 'Table \\`%s.%s\\` is public, but RLS has not been enabled.',\n n.nspname,\n c.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0013_rls_disabled_in_public' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'rls_disabled_in_public_%s_%s',\n n.nspname,\n c.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind = 'r' -- regular tables\n -- RLS is disabled\n and not c.relrowsecurity\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n ))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"rlsDisabledInPublic\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub RlsDisabledInPublic { version : "1.0.0" , name : "rlsDisabledInPublic" , severity : pgls_diagnostics :: Severity :: Error , recommended : true , } } impl SplinterRule for RlsDisabledInPublic { const SQL_FILE_PATH: &'static str = "security/rls_disabled_in_public.sql"; const DESCRIPTION: &'static str = "Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST"; diff --git a/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs b/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs index 1f58b7789..b2b4af427 100644 --- a/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs +++ b/crates/pgls_splinter/src/rules/security/rls_enabled_no_policy.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # RLS Enabled No Policy\n///\n/// Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'rls_enabled_no_policy' as \"name!\",\n/// 'RLS Enabled No Policy' as \"title!\",\n/// 'INFO' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has RLS enabled, but no policies exist',\n/// n.nspname,\n/// c.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'rls_enabled_no_policy_%s_%s',\n/// n.nspname,\n/// c.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class c\n/// left join pg_catalog.pg_policy p\n/// on p.polrelid = c.oid\n/// join pg_catalog.pg_namespace n\n/// on c.relnamespace = n.oid\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// c.relkind = 'r' -- regular tables\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// -- RLS is enabled\n/// and c.relrowsecurity\n/// and p.polname is null\n/// and dep.objid is null -- exclude tables owned by extensions\n/// group by\n/// n.nspname,\n/// c.relname)\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"rlsEnabledNoPolicy\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub RlsEnabledNoPolicy { version : "1.0.0" , name : "rlsEnabledNoPolicy" , severity : pgls_diagnostics :: Severity :: Information , } } +::pgls_analyse::declare_rule! { # [doc = "# RLS Enabled No Policy\n\nDetects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.\n\n## SQL Query\n\n```sql\n(\nselect\n 'rls_enabled_no_policy' as \"name!\",\n 'RLS Enabled No Policy' as \"title!\",\n 'INFO' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.' as \"description!\",\n format(\n 'Table \\`%s.%s\\` has RLS enabled, but no policies exist',\n n.nspname,\n c.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0008_rls_enabled_no_policy' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'rls_enabled_no_policy_%s_%s',\n n.nspname,\n c.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_class c\n left join pg_catalog.pg_policy p\n on p.polrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'r' -- regular tables\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n -- RLS is enabled\n and c.relrowsecurity\n and p.polname is null\n and dep.objid is null -- exclude tables owned by extensions\ngroup by\n n.nspname,\n c.relname)\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"rlsEnabledNoPolicy\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub RlsEnabledNoPolicy { version : "1.0.0" , name : "rlsEnabledNoPolicy" , severity : pgls_diagnostics :: Severity :: Information , recommended : true , } } impl SplinterRule for RlsEnabledNoPolicy { const SQL_FILE_PATH: &'static str = "security/rls_enabled_no_policy.sql"; const DESCRIPTION: &'static str = "Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created."; diff --git a/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs b/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs index a43c1dcf8..39b43f850 100644 --- a/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs +++ b/crates/pgls_splinter/src/rules/security/rls_references_user_metadata.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # RLS references user metadata\n///\n/// Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// with policies as (\n/// select\n/// nsp.nspname as schema_name,\n/// pb.tablename as table_name,\n/// polname as policy_name,\n/// qual,\n/// with_check\n/// from\n/// pg_catalog.pg_policy pa\n/// join pg_catalog.pg_class pc\n/// on pa.polrelid = pc.oid\n/// join pg_catalog.pg_namespace nsp\n/// on pc.relnamespace = nsp.oid\n/// join pg_catalog.pg_policies pb\n/// on pc.relname = pb.tablename\n/// and nsp.nspname = pb.schemaname\n/// and pa.polname = pb.policyname\n/// )\n/// select\n/// 'rls_references_user_metadata' as \"name!\",\n/// 'RLS references user metadata' as \"title!\",\n/// 'ERROR' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has a row level security policy \\`%s\\` that references Supabase Auth \\`user_metadata\\`. \\`user_metadata\\` is editable by end users and should never be used in a security context.',\n/// schema_name,\n/// table_name,\n/// policy_name\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', schema_name,\n/// 'name', table_name,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format('rls_references_user_metadata_%s_%s_%s', schema_name, table_name, policy_name) as \"cache_key!\"\n/// from\n/// policies\n/// where\n/// schema_name not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and (\n/// -- Example: auth.jwt() -> 'user_metadata'\n/// -- False positives are possible, but it isn't practical to string match\n/// -- If false positive rate is too high, this expression can iterate\n/// qual like '%auth.jwt()%user_metadata%'\n/// or qual like '%current_setting(%request.jwt.claims%)%user_metadata%'\n/// or with_check like '%auth.jwt()%user_metadata%'\n/// or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%'\n/// ))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"rlsReferencesUserMetadata\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub RlsReferencesUserMetadata { version : "1.0.0" , name : "rlsReferencesUserMetadata" , severity : pgls_diagnostics :: Severity :: Error , } } +::pgls_analyse::declare_rule! { # [doc = "# RLS references user metadata\n\nDetects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nwith policies as (\n select\n nsp.nspname as schema_name,\n pb.tablename as table_name,\n polname as policy_name,\n qual,\n with_check\n from\n pg_catalog.pg_policy pa\n join pg_catalog.pg_class pc\n on pa.polrelid = pc.oid\n join pg_catalog.pg_namespace nsp\n on pc.relnamespace = nsp.oid\n join pg_catalog.pg_policies pb\n on pc.relname = pb.tablename\n and nsp.nspname = pb.schemaname\n and pa.polname = pb.policyname\n)\nselect\n 'rls_references_user_metadata' as \"name!\",\n 'RLS references user metadata' as \"title!\",\n 'ERROR' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.' as \"description!\",\n format(\n 'Table \\`%s.%s\\` has a row level security policy \\`%s\\` that references Supabase Auth \\`user_metadata\\`. \\`user_metadata\\` is editable by end users and should never be used in a security context.',\n schema_name,\n table_name,\n policy_name\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata' as \"remediation!\",\n jsonb_build_object(\n 'schema', schema_name,\n 'name', table_name,\n 'type', 'table'\n ) as \"metadata!\",\n format('rls_references_user_metadata_%s_%s_%s', schema_name, table_name, policy_name) as \"cache_key!\"\nfrom\n policies\nwhere\n schema_name not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and (\n -- Example: auth.jwt() -> 'user_metadata'\n -- False positives are possible, but it isn't practical to string match\n -- If false positive rate is too high, this expression can iterate\n qual like '%auth.jwt()%user_metadata%'\n or qual like '%current_setting(%request.jwt.claims%)%user_metadata%'\n or with_check like '%auth.jwt()%user_metadata%'\n or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%'\n ))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"rlsReferencesUserMetadata\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub RlsReferencesUserMetadata { version : "1.0.0" , name : "rlsReferencesUserMetadata" , severity : pgls_diagnostics :: Severity :: Error , recommended : true , } } impl SplinterRule for RlsReferencesUserMetadata { const SQL_FILE_PATH: &'static str = "security/rls_references_user_metadata.sql"; const DESCRIPTION: &'static str = "Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy."; diff --git a/crates/pgls_splinter/src/rules/security/security_definer_view.rs b/crates/pgls_splinter/src/rules/security/security_definer_view.rs index f36b73670..e8f0f82ad 100644 --- a/crates/pgls_splinter/src/rules/security/security_definer_view.rs +++ b/crates/pgls_splinter/src/rules/security/security_definer_view.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Security Definer View\n///\n/// Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'security_definer_view' as \"name!\",\n/// 'Security Definer View' as \"title!\",\n/// 'ERROR' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user' as \"description!\",\n/// format(\n/// 'View \\`%s.%s\\` is defined with the SECURITY DEFINER property',\n/// n.nspname,\n/// c.relname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'type', 'view'\n/// ) as \"metadata!\",\n/// format(\n/// 'security_definer_view_%s_%s',\n/// n.nspname,\n/// c.relname\n/// ) as \"cache_key!\"\n/// from\n/// pg_catalog.pg_class c\n/// join pg_catalog.pg_namespace n\n/// on n.oid = c.relnamespace\n/// left join pg_catalog.pg_depend dep\n/// on c.oid = dep.objid\n/// and dep.deptype = 'e'\n/// where\n/// c.relkind = 'v'\n/// and (\n/// pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n/// or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n/// )\n/// and substring(pg_catalog.version() from 'PostgreSQL ([0-9]+)') >= '15' -- security invoker was added in pg15\n/// and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n/// and n.nspname not in (\n/// '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n/// )\n/// and dep.objid is null -- exclude views owned by extensions\n/// and not (\n/// lower(coalesce(c.reloptions::text,'{}'))::text[]\n/// && array[\n/// 'security_invoker=1',\n/// 'security_invoker=true',\n/// 'security_invoker=yes',\n/// 'security_invoker=on'\n/// ]\n/// ))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"securityDefinerView\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub SecurityDefinerView { version : "1.0.0" , name : "securityDefinerView" , severity : pgls_diagnostics :: Severity :: Error , } } +::pgls_analyse::declare_rule! { # [doc = "# Security Definer View\n\nDetects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.\n\n## SQL Query\n\n```sql\n(\nselect\n 'security_definer_view' as \"name!\",\n 'Security Definer View' as \"title!\",\n 'ERROR' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user' as \"description!\",\n format(\n 'View \\`%s.%s\\` is defined with the SECURITY DEFINER property',\n n.nspname,\n c.relname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=0010_security_definer_view' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'view'\n ) as \"metadata!\",\n format(\n 'security_definer_view_%s_%s',\n n.nspname,\n c.relname\n ) as \"cache_key!\"\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\n left join pg_catalog.pg_depend dep\n on c.oid = dep.objid\n and dep.deptype = 'e'\nwhere\n c.relkind = 'v'\n and (\n pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')\n or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')\n )\n and substring(pg_catalog.version() from 'PostgreSQL ([0-9]+)') >= '15' -- security invoker was added in pg15\n and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))\n and n.nspname not in (\n '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'\n )\n and dep.objid is null -- exclude views owned by extensions\n and not (\n lower(coalesce(c.reloptions::text,'{}'))::text[]\n && array[\n 'security_invoker=1',\n 'security_invoker=true',\n 'security_invoker=yes',\n 'security_invoker=on'\n ]\n ))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"securityDefinerView\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub SecurityDefinerView { version : "1.0.0" , name : "securityDefinerView" , severity : pgls_diagnostics :: Severity :: Error , recommended : true , } } impl SplinterRule for SecurityDefinerView { const SQL_FILE_PATH: &'static str = "security/security_definer_view.sql"; const DESCRIPTION: &'static str = "Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user"; diff --git a/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs b/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs index 0d1008df1..8068b4f50 100644 --- a/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs +++ b/crates/pgls_splinter/src/rules/security/unsupported_reg_types.rs @@ -2,7 +2,7 @@ #![doc = r" Generated file, do not edit by hand, see `xtask/codegen`"] use crate::rule::SplinterRule; -::pgls_analyse::declare_rule! { # [doc = "/// # Unsupported reg types\n///\n/// Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.\n///\n/// ## SQL Query\n///\n/// ```sql\n/// (\n/// select\n/// 'unsupported_reg_types' as \"name!\",\n/// 'Unsupported reg types' as \"title!\",\n/// 'WARN' as \"level!\",\n/// 'EXTERNAL' as \"facing!\",\n/// array['SECURITY'] as \"categories!\",\n/// 'Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.' as \"description!\",\n/// format(\n/// 'Table \\`%s.%s\\` has a column \\`%s\\` with unsupported reg* type \\`%s\\`.',\n/// n.nspname,\n/// c.relname,\n/// a.attname,\n/// t.typname\n/// ) as \"detail!\",\n/// 'https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types' as \"remediation!\",\n/// jsonb_build_object(\n/// 'schema', n.nspname,\n/// 'name', c.relname,\n/// 'column', a.attname,\n/// 'type', 'table'\n/// ) as \"metadata!\",\n/// format(\n/// 'unsupported_reg_types_%s_%s_%s',\n/// n.nspname,\n/// c.relname,\n/// a.attname\n/// ) AS cache_key\n/// from\n/// pg_catalog.pg_attribute a\n/// join pg_catalog.pg_class c\n/// on a.attrelid = c.oid\n/// join pg_catalog.pg_namespace n\n/// on c.relnamespace = n.oid\n/// join pg_catalog.pg_type t\n/// on a.atttypid = t.oid\n/// join pg_catalog.pg_namespace tn\n/// on t.typnamespace = tn.oid\n/// where\n/// tn.nspname = 'pg_catalog'\n/// and t.typname in ('regcollation', 'regconfig', 'regdictionary', 'regnamespace', 'regoper', 'regoperator', 'regproc', 'regprocedure')\n/// and n.nspname not in ('pg_catalog', 'information_schema', 'pgsodium'))\n/// ```\n///\n/// ## Configuration\n///\n/// Enable or disable this rule in your configuration:\n///\n/// ```json\n/// {\n/// \"splinter\": {\n/// \"rules\": {\n/// \"security\": {\n/// \"unsupportedRegTypes\": \"warn\"\n/// }\n/// }\n/// }\n/// }\n/// ```\n///\n/// ## Remediation\n///\n/// See: "] pub UnsupportedRegTypes { version : "1.0.0" , name : "unsupportedRegTypes" , severity : pgls_diagnostics :: Severity :: Warning , } } +::pgls_analyse::declare_rule! { # [doc = "# Unsupported reg types\n\nIdentifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.\n\n## SQL Query\n\n```sql\n(\nselect\n 'unsupported_reg_types' as \"name!\",\n 'Unsupported reg types' as \"title!\",\n 'WARN' as \"level!\",\n 'EXTERNAL' as \"facing!\",\n array['SECURITY'] as \"categories!\",\n 'Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.' as \"description!\",\n format(\n 'Table \\`%s.%s\\` has a column \\`%s\\` with unsupported reg* type \\`%s\\`.',\n n.nspname,\n c.relname,\n a.attname,\n t.typname\n ) as \"detail!\",\n 'https://supabase.com/docs/guides/database/database-linter?lint=unsupported_reg_types' as \"remediation!\",\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'column', a.attname,\n 'type', 'table'\n ) as \"metadata!\",\n format(\n 'unsupported_reg_types_%s_%s_%s',\n n.nspname,\n c.relname,\n a.attname\n ) AS cache_key\nfrom\n pg_catalog.pg_attribute a\n join pg_catalog.pg_class c\n on a.attrelid = c.oid\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\n join pg_catalog.pg_type t\n on a.atttypid = t.oid\n join pg_catalog.pg_namespace tn\n on t.typnamespace = tn.oid\nwhere\n tn.nspname = 'pg_catalog'\n and t.typname in ('regcollation', 'regconfig', 'regdictionary', 'regnamespace', 'regoper', 'regoperator', 'regproc', 'regprocedure')\n and n.nspname not in ('pg_catalog', 'information_schema', 'pgsodium'))\n```\n\n## Configuration\n\nEnable or disable this rule in your configuration:\n\n```json\n{\n \"splinter\": {\n \"rules\": {\n \"security\": {\n \"unsupportedRegTypes\": \"warn\"\n }\n }\n }\n}\n```\n\n## Remediation\n\nSee: "] pub UnsupportedRegTypes { version : "1.0.0" , name : "unsupportedRegTypes" , severity : pgls_diagnostics :: Severity :: Warning , recommended : true , } } impl SplinterRule for UnsupportedRegTypes { const SQL_FILE_PATH: &'static str = "security/unsupported_reg_types.sql"; const DESCRIPTION: &'static str = "Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade."; diff --git a/crates/pgls_workspace/Cargo.toml b/crates/pgls_workspace/Cargo.toml index 49d6ecb08..7c2753a3b 100644 --- a/crates/pgls_workspace/Cargo.toml +++ b/crates/pgls_workspace/Cargo.toml @@ -33,6 +33,7 @@ pgls_plpgsql_check = { workspace = true } pgls_query = { workspace = true } pgls_query_ext = { workspace = true } pgls_schema_cache = { workspace = true } +pgls_splinter = { workspace = true } pgls_statement_splitter = { workspace = true } pgls_suppressions = { workspace = true } pgls_text_size.workspace = true diff --git a/crates/pgls_workspace/src/features/diagnostics.rs b/crates/pgls_workspace/src/features/diagnostics.rs index 2ca3132a0..e09bea76a 100644 --- a/crates/pgls_workspace/src/features/diagnostics.rs +++ b/crates/pgls_workspace/src/features/diagnostics.rs @@ -22,5 +22,8 @@ pub struct PullDiagnosticsResult { #[derive(Debug, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct PullDatabaseDiagnosticsParams { + pub categories: RuleCategories, pub max_diagnostics: u32, + pub only: Vec, + pub skip: Vec, } diff --git a/crates/pgls_workspace/src/settings.rs b/crates/pgls_workspace/src/settings.rs index 99dbb6fbc..001361d0e 100644 --- a/crates/pgls_workspace/src/settings.rs +++ b/crates/pgls_workspace/src/settings.rs @@ -19,6 +19,7 @@ use pgls_configuration::{ files::FilesConfiguration, migrations::{MigrationsConfiguration, PartialMigrationsConfiguration}, plpgsql_check::PlPgSqlCheckConfiguration, + splinter::SplinterConfiguration, }; use pgls_fs::PgLSPath; use sqlx::postgres::PgConnectOptions; @@ -213,6 +214,9 @@ pub struct Settings { /// Linter settings applied to all files in the workspace pub linter: LinterSettings, + /// Splinter (database linter) settings for the workspace + pub splinter: SplinterSettings, + /// Type checking settings for the workspace pub typecheck: TypecheckSettings, @@ -254,6 +258,11 @@ impl Settings { to_linter_settings(working_directory.clone(), LinterConfiguration::from(linter))?; } + // splinter part + if let Some(splinter) = configuration.splinter { + self.splinter = to_splinter_settings(SplinterConfiguration::from(splinter)); + } + // typecheck part if let Some(typecheck) = configuration.typecheck { self.typecheck = to_typecheck_settings(TypecheckConfiguration::from(typecheck)); @@ -286,6 +295,11 @@ impl Settings { self.linter.rules.as_ref().map(Cow::Borrowed) } + /// Returns splinter rules. + pub fn as_splinter_rules(&self) -> Option> { + self.splinter.rules.as_ref().map(Cow::Borrowed) + } + /// It retrieves the severity based on the `code` of the rule and the current configuration. /// /// The code of the has the following pattern: `{group}/{rule_name}`. @@ -314,6 +328,13 @@ fn to_linter_settings( }) } +fn to_splinter_settings(conf: SplinterConfiguration) -> SplinterSettings { + SplinterSettings { + enabled: conf.enabled, + rules: Some(conf.rules), + } +} + fn to_typecheck_settings(conf: TypecheckConfiguration) -> TypecheckSettings { TypecheckSettings { search_path: conf.search_path.into_iter().collect(), @@ -434,6 +455,25 @@ impl Default for LinterSettings { } } +/// Splinter (database linter) settings for the entire workspace +#[derive(Debug)] +pub struct SplinterSettings { + /// Enabled by default + pub enabled: bool, + + /// List of rules + pub rules: Option, +} + +impl Default for SplinterSettings { + fn default() -> Self { + Self { + enabled: true, + rules: Some(pgls_configuration::splinter::Rules::default()), + } + } +} + /// Type checking settings for the entire workspace #[derive(Debug)] pub struct PlPgSqlCheckSettings { diff --git a/crates/pgls_workspace/src/workspace/server.rs b/crates/pgls_workspace/src/workspace/server.rs index e5feed085..cdd8964b7 100644 --- a/crates/pgls_workspace/src/workspace/server.rs +++ b/crates/pgls_workspace/src/workspace/server.rs @@ -706,9 +706,73 @@ impl Workspace for WorkspaceServer { fn pull_db_diagnostics( &self, - _params: crate::features::diagnostics::PullDatabaseDiagnosticsParams, + params: crate::features::diagnostics::PullDatabaseDiagnosticsParams, ) -> Result { - Ok(PullDiagnosticsResult::default()) + let settings = self.workspaces(); + let Some(settings) = settings.settings() else { + debug!("No settings available. Returning empty diagnostics."); + return Ok(PullDiagnosticsResult::default()); + }; + + if !settings.splinter.enabled { + debug!("Splinter is disabled. Skipping database linting."); + return Ok(PullDiagnosticsResult::default()); + } + + let Some(pool) = self.get_current_connection() else { + debug!("No database connection available. Skipping splinter checks."); + return Ok(PullDiagnosticsResult::default()); + }; + + let (enabled_rules, disabled_rules) = AnalyserVisitorBuilder::new(settings) + .with_splinter_rules(¶ms.only, ¶ms.skip) + .finish(); + + let schema_cache = self.schema_cache.load(pool.clone()).ok(); + + let pool_clone = pool.clone(); + let schema_cache_clone = schema_cache.clone(); + let categories = params.categories; + let rules_config = settings.splinter.rules.clone(); + let splinter_result = run_async(async move { + let filter = AnalysisFilter { + categories, + enabled_rules: Some(enabled_rules.as_slice()), + disabled_rules: &disabled_rules, + }; + let splinter_params = pgls_splinter::SplinterParams { + conn: &pool_clone, + schema_cache: schema_cache_clone.as_deref(), + rules_config: rules_config.as_ref(), + }; + pgls_splinter::run_splinter(splinter_params, &filter).await + }); + + let splinter_diagnostics = match splinter_result { + Ok(Ok(diags)) => diags, + Ok(Err(sql_err)) => { + debug!("Splinter SQL error: {:?}", sql_err); + return Err(sql_err.into()); + } + Err(join_err) => { + debug!("Splinter join error: {:?}", join_err); + return Err(join_err); + } + }; + + let total = splinter_diagnostics.len(); + let max = params.max_diagnostics as usize; + let diagnostics: Vec = splinter_diagnostics + .into_iter() + .take(max) + .map(SDiagnostic::new) + .collect(); + let skipped = total.saturating_sub(max) as u32; + + Ok(PullDiagnosticsResult { + diagnostics, + skipped_diagnostics: skipped, + }) } #[ignored_path(path=¶ms.path)] diff --git a/crates/pgls_workspace/src/workspace/server/analyser.rs b/crates/pgls_workspace/src/workspace/server/analyser.rs index d8de3bbb3..cace0563d 100644 --- a/crates/pgls_workspace/src/workspace/server/analyser.rs +++ b/crates/pgls_workspace/src/workspace/server/analyser.rs @@ -4,30 +4,42 @@ use rustc_hash::FxHashSet; use crate::settings::Settings; -pub(crate) struct AnalyserVisitorBuilder<'a, 'b> { - lint: Option>, - settings: &'b Settings, +pub(crate) struct AnalyserVisitorBuilder<'a> { + lint: Option>, + splinter: Option>, + settings: &'a Settings, } -impl<'a, 'b> AnalyserVisitorBuilder<'a, 'b> { - pub(crate) fn new(settings: &'b Settings) -> Self { +impl<'a> AnalyserVisitorBuilder<'a> { + pub(crate) fn new(settings: &'a Settings) -> Self { Self { settings, lint: None, + splinter: None, } } #[must_use] pub(crate) fn with_linter_rules( mut self, - only: &'b [RuleSelector], - skip: &'b [RuleSelector], + only: &'a [RuleSelector], + skip: &'a [RuleSelector], ) -> Self { self.lint = Some(LintVisitor::new(only, skip, self.settings)); self } #[must_use] - pub(crate) fn finish(self) -> (Vec>, Vec>) { + pub(crate) fn with_splinter_rules( + mut self, + only: &'a [RuleSelector], + skip: &'a [RuleSelector], + ) -> Self { + self.splinter = Some(SplinterVisitor::new(only, skip, self.settings)); + self + } + + #[must_use] + pub(crate) fn finish(self) -> (Vec>, Vec>) { let mut disabled_rules = vec![]; let mut enabled_rules = vec![]; if let Some(mut lint) = self.lint { @@ -36,6 +48,12 @@ impl<'a, 'b> AnalyserVisitorBuilder<'a, 'b> { enabled_rules.extend(linter_enabled_rules); disabled_rules.extend(linter_disabled_rules); } + if let Some(mut splinter) = self.splinter { + pgls_splinter::registry::visit_registry(&mut splinter); + let (splinter_enabled_rules, splinter_disabled_rules) = splinter.finish(); + enabled_rules.extend(splinter_enabled_rules); + disabled_rules.extend(splinter_disabled_rules); + } (enabled_rules, disabled_rules) } @@ -43,19 +61,19 @@ impl<'a, 'b> AnalyserVisitorBuilder<'a, 'b> { /// Type meant to register all the lint rules #[derive(Debug)] -struct LintVisitor<'a, 'b> { - pub(crate) enabled_rules: FxHashSet>, - pub(crate) disabled_rules: FxHashSet>, - only: &'b [RuleSelector], - skip: &'b [RuleSelector], - settings: &'b Settings, +struct LintVisitor<'a> { + pub(crate) enabled_rules: FxHashSet>, + pub(crate) disabled_rules: FxHashSet>, + only: &'a [RuleSelector], + skip: &'a [RuleSelector], + settings: &'a Settings, } -impl<'a, 'b> LintVisitor<'a, 'b> { +impl<'a> LintVisitor<'a> { pub(crate) fn new( - only: &'b [RuleSelector], - skip: &'b [RuleSelector], - settings: &'b Settings, + only: &'a [RuleSelector], + skip: &'a [RuleSelector], + settings: &'a Settings, ) -> Self { Self { enabled_rules: Default::default(), @@ -66,7 +84,12 @@ impl<'a, 'b> LintVisitor<'a, 'b> { } } - fn finish(mut self) -> (FxHashSet>, FxHashSet>) { + fn finish( + mut self, + ) -> ( + FxHashSet>, + FxHashSet>, + ) { let has_only_filter = !self.only.is_empty(); if !has_only_filter { @@ -109,7 +132,7 @@ impl<'a, 'b> LintVisitor<'a, 'b> { } } -impl RegistryVisitor for LintVisitor<'_, '_> { +impl RegistryVisitor for LintVisitor<'_> { fn record_category(&mut self) { if C::CATEGORY == RuleCategory::Lint { C::record_groups(self) @@ -138,18 +161,119 @@ impl RegistryVisitor for LintVisitor<'_, '_> { } } +/// Type meant to register all the splinter (database lint) rules +#[derive(Debug)] +struct SplinterVisitor<'a> { + pub(crate) enabled_rules: FxHashSet>, + pub(crate) disabled_rules: FxHashSet>, + only: &'a [RuleSelector], + skip: &'a [RuleSelector], + settings: &'a Settings, +} + +impl<'a> SplinterVisitor<'a> { + pub(crate) fn new( + only: &'a [RuleSelector], + skip: &'a [RuleSelector], + settings: &'a Settings, + ) -> Self { + Self { + enabled_rules: Default::default(), + disabled_rules: Default::default(), + only, + skip, + settings, + } + } + + fn finish( + mut self, + ) -> ( + FxHashSet>, + FxHashSet>, + ) { + let has_only_filter = !self.only.is_empty(); + + if !has_only_filter { + let enabled_rules = self + .settings + .as_splinter_rules() + .map(|rules| rules.as_enabled_rules()) + .unwrap_or_default(); + + self.enabled_rules.extend(enabled_rules); + + let disabled_rules = self + .settings + .as_splinter_rules() + .map(|rules| rules.as_disabled_rules()) + .unwrap_or_default(); + self.disabled_rules.extend(disabled_rules); + } + + (self.enabled_rules, self.disabled_rules) + } + + fn push_rule(&mut self) + where + R: RuleMeta + 'static, + { + for selector in self.only { + let filter = RuleFilter::from(selector); + if filter.match_rule::() { + self.enabled_rules.insert(filter); + } + } + for selector in self.skip { + let filter = RuleFilter::from(selector); + if filter.match_rule::() { + self.disabled_rules.insert(filter); + } + } + } +} + +impl RegistryVisitor for SplinterVisitor<'_> { + fn record_category(&mut self) { + // Splinter uses Lint as its kind in declare_category! macro + // We always record because we're visiting the splinter registry specifically + C::record_groups(self) + } + + fn record_group(&mut self) { + for selector in self.only { + if RuleFilter::from(selector).match_group::() { + G::record_rules(self) + } + } + + for selector in self.skip { + if RuleFilter::from(selector).match_group::() { + G::record_rules(self) + } + } + } + + fn record_rule(&mut self) + where + R: RuleMeta + 'static, + { + self.push_rule::() + } +} + #[cfg(test)] mod tests { use pgls_analyse::RuleFilter; use pgls_configuration::{RuleConfiguration, Rules, linter::Safety}; use crate::{ - settings::{LinterSettings, Settings}, + settings::{LinterSettings, Settings, SplinterSettings}, workspace::server::analyser::AnalyserVisitorBuilder, }; #[test] - fn recognizes_disabled_rules() { + fn recognizes_disabled_linter_rules() { let settings = Settings { linter: LinterSettings { rules: Some(Rules { @@ -175,4 +299,77 @@ mod tests { vec![RuleFilter::Rule("safety", "banDropColumn")] ) } + + #[test] + fn recognizes_disabled_splinter_rules() { + use pgls_configuration::splinter::{Performance, Rules as SplinterRules}; + + let settings = Settings { + splinter: SplinterSettings { + enabled: true, + rules: Some(SplinterRules { + performance: Some(Performance { + auth_rls_initplan: Some(RuleConfiguration::Plain( + pgls_configuration::RulePlainConfiguration::Off, + )), + ..Default::default() + }), + ..Default::default() + }), + }, + ..Default::default() + }; + + let (_, disabled_rules) = AnalyserVisitorBuilder::new(&settings) + .with_splinter_rules(&[], &[]) + .finish(); + + assert_eq!( + disabled_rules, + vec![RuleFilter::Rule("performance", "authRlsInitplan")] + ) + } + + #[test] + fn combines_linter_and_splinter_rules() { + use pgls_configuration::splinter::{Performance, Rules as SplinterRules}; + + let settings = Settings { + linter: LinterSettings { + rules: Some(Rules { + safety: Some(Safety { + ban_drop_column: Some(RuleConfiguration::Plain( + pgls_configuration::RulePlainConfiguration::Off, + )), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }, + splinter: SplinterSettings { + enabled: true, + rules: Some(SplinterRules { + performance: Some(Performance { + auth_rls_initplan: Some(RuleConfiguration::Plain( + pgls_configuration::RulePlainConfiguration::Off, + )), + ..Default::default() + }), + ..Default::default() + }), + }, + ..Default::default() + }; + + let (_, disabled_rules) = AnalyserVisitorBuilder::new(&settings) + .with_linter_rules(&[], &[]) + .with_splinter_rules(&[], &[]) + .finish(); + + // Should contain disabled rules from both linter and splinter + assert!(disabled_rules.contains(&RuleFilter::Rule("safety", "banDropColumn"))); + assert!(disabled_rules.contains(&RuleFilter::Rule("performance", "authRlsInitplan"))); + assert_eq!(disabled_rules.len(), 2); + } } diff --git a/docs/schema.json b/docs/schema.json index 93dc6ceae..71db27702 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -77,6 +77,17 @@ } ] }, + "splinter": { + "description": "The configuration for splinter", + "anyOf": [ + { + "$ref": "#/definitions/SplinterConfiguration" + }, + { + "type": "null" + } + ] + }, "typecheck": { "description": "The configuration for type checking", "anyOf": [ @@ -246,7 +257,37 @@ "description": "List of rules", "anyOf": [ { - "$ref": "#/definitions/Rules" + "$ref": "#/definitions/LinterRules" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "LinterRules": { + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules. The rules that belong to `nursery` won't be enabled.", + "type": [ + "boolean", + "null" + ] + }, + "recommended": { + "description": "It enables the lint rules recommended by Postgres Language Server. `true` by default.", + "type": [ + "boolean", + "null" + ] + }, + "safety": { + "anyOf": [ + { + "$ref": "#/definitions/Safety" }, { "type": "null" @@ -279,6 +320,104 @@ }, "additionalProperties": false }, + "Performance": { + "description": "A list of rules that belong to this group", + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules for this group.", + "type": [ + "boolean", + "null" + ] + }, + "authRlsInitplan": { + "description": "Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "duplicateIndex": { + "description": "Duplicate Index: Detects cases where two ore more identical indexes exist.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "multiplePermissivePolicies": { + "description": "Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "noPrimaryKey": { + "description": "No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "recommended": { + "description": "It enables the recommended rules for this group", + "type": [ + "boolean", + "null" + ] + }, + "tableBloat": { + "description": "Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "unindexedForeignKeys": { + "description": "Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "unusedIndex": { + "description": "Unused Index: Detects if an index has never been used and may be a candidate for removal.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, "PlPgSqlCheckConfiguration": { "description": "The configuration for type checking.", "type": "object", @@ -329,30 +468,26 @@ }, "additionalProperties": false }, - "Rules": { + "RuleWithSplinterRuleOptions": { "type": "object", + "required": [ + "level", + "options" + ], "properties": { - "all": { - "description": "It enables ALL rules. The rules that belong to `nursery` won't be enabled.", - "type": [ - "boolean", - "null" - ] - }, - "recommended": { - "description": "It enables the lint rules recommended by Postgres Language Server. `true` by default.", - "type": [ - "boolean", - "null" + "level": { + "description": "The severity of the emitted diagnostics by the rule", + "allOf": [ + { + "$ref": "#/definitions/RulePlainConfiguration" + } ] }, - "safety": { - "anyOf": [ - { - "$ref": "#/definitions/Safety" - }, + "options": { + "description": "Rule's options", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/SplinterRuleOptions" } ] } @@ -743,6 +878,270 @@ }, "additionalProperties": false }, + "Security": { + "description": "A list of rules that belong to this group", + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules for this group.", + "type": [ + "boolean", + "null" + ] + }, + "authUsersExposed": { + "description": "Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "extensionInPublic": { + "description": "Extension in Public: Detects extensions installed in the `public` schema.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "extensionVersionsOutdated": { + "description": "Extension Versions Outdated: Detects extensions that are not using the default (recommended) version.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "fkeyToAuthUnique": { + "description": "Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "foreignTableInApi": { + "description": "Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "functionSearchPathMutable": { + "description": "Function Search Path Mutable: Detects functions where the search_path parameter is not set.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "insecureQueueExposedInApi": { + "description": "Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "materializedViewInApi": { + "description": "Materialized View in API: Detects materialized views that are accessible over the Data APIs.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "policyExistsRlsDisabled": { + "description": "Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "recommended": { + "description": "It enables the recommended rules for this group", + "type": [ + "boolean", + "null" + ] + }, + "rlsDisabledInPublic": { + "description": "RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "rlsEnabledNoPolicy": { + "description": "RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "rlsReferencesUserMetadata": { + "description": "RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "securityDefinerView": { + "description": "Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + }, + "unsupportedRegTypes": { + "description": "Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade.", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRuleConfiguration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "SplinterConfiguration": { + "type": "object", + "properties": { + "enabled": { + "description": "if `false`, it disables the feature and the linter won't be executed. `true` by default", + "type": [ + "boolean", + "null" + ] + }, + "rules": { + "description": "List of rules", + "anyOf": [ + { + "$ref": "#/definitions/SplinterRules" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "SplinterRuleConfiguration": { + "anyOf": [ + { + "$ref": "#/definitions/RulePlainConfiguration" + }, + { + "$ref": "#/definitions/RuleWithSplinterRuleOptions" + } + ] + }, + "SplinterRuleOptions": { + "description": "Shared options for all splinter rules.\n\nThese options allow configuring per-rule filtering of database objects.", + "type": "object", + "properties": { + "ignore": { + "description": "A list of glob patterns for database objects to ignore.\n\nPatterns use Unix-style globs where: - `*` matches any sequence of characters - `?` matches any single character\n\nEach pattern should be in the format `schema.object_name`, for example: - `\"public.my_table\"` - ignores a specific table - `\"audit.*\"` - ignores all objects in the audit schema - `\"*.audit_*\"` - ignores objects with audit_ prefix in any schema", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "SplinterRules": { + "type": "object", + "properties": { + "all": { + "description": "It enables ALL rules. The rules that belong to `nursery` won't be enabled.", + "type": [ + "boolean", + "null" + ] + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Performance" + }, + { + "type": "null" + } + ] + }, + "recommended": { + "description": "It enables the lint rules recommended by Postgres Language Server. `true` by default.", + "type": [ + "boolean", + "null" + ] + }, + "security": { + "anyOf": [ + { + "$ref": "#/definitions/Security" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, "StringSet": { "type": "array", "items": { diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts index cf42e5145..c6b93982d 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts @@ -314,6 +314,10 @@ export interface PartialConfiguration { * The configuration for type checking */ plpgsqlCheck?: PartialPlPgSqlCheckConfiguration; + /** + * The configuration for splinter + */ + splinter?: PartialSplinterConfiguration; /** * The configuration for type checking */ @@ -391,7 +395,7 @@ export interface PartialLinterConfiguration { /** * List of rules */ - rules?: Rules; + rules?: LinterRules; } /** * The configuration of the filesystem @@ -415,6 +419,16 @@ export interface PartialPlPgSqlCheckConfiguration { */ enabled?: boolean; } +export interface PartialSplinterConfiguration { + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * List of rules + */ + rules?: SplinterRules; +} /** * The configuration for type checking. */ @@ -455,7 +469,7 @@ If we can't find the configuration, it will attempt to use the current working d */ useIgnoreFile?: boolean; } -export interface Rules { +export interface LinterRules { /** * It enables ALL rules. The rules that belong to `nursery` won't be enabled. */ @@ -466,6 +480,18 @@ export interface Rules { recommended?: boolean; safety?: Safety; } +export interface SplinterRules { + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + performance?: Performance; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + security?: Security; +} export type VcsClientKind = "git"; /** * A list of rules that belong to this group @@ -612,9 +638,122 @@ export interface Safety { */ transactionNesting?: RuleConfiguration_for_Null; } +/** + * A list of rules that belong to this group + */ +export interface Performance { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row + */ + authRlsInitplan?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Duplicate Index: Detects cases where two ore more identical indexes exist. + */ + duplicateIndex?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. + */ + multiplePermissivePolicies?: RuleConfiguration_for_SplinterRuleOptions; + /** + * No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. + */ + noPrimaryKey?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. + */ + tableBloat?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance. + */ + unindexedForeignKeys?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unused Index: Detects if an index has never been used and may be a candidate for removal. + */ + unusedIndex?: RuleConfiguration_for_SplinterRuleOptions; +} +/** + * A list of rules that belong to this group + */ +export interface Security { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. + */ + authUsersExposed?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension in Public: Detects extensions installed in the `public` schema. + */ + extensionInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension Versions Outdated: Detects extensions that are not using the default (recommended) version. + */ + extensionVersionsOutdated?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema. + */ + fkeyToAuthUnique?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. + */ + foreignTableInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Function Search Path Mutable: Detects functions where the search_path parameter is not set. + */ + functionSearchPathMutable?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs + */ + insecureQueueExposedInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Materialized View in API: Detects materialized views that are accessible over the Data APIs. + */ + materializedViewInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. + */ + policyExistsRlsDisabled?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST + */ + rlsDisabledInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. + */ + rlsEnabledNoPolicy?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. + */ + rlsReferencesUserMetadata?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user + */ + securityDefinerView?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. + */ + unsupportedRegTypes?: RuleConfiguration_for_SplinterRuleOptions; +} export type RuleConfiguration_for_Null = | RulePlainConfiguration | RuleWithOptions_for_Null; +export type RuleConfiguration_for_SplinterRuleOptions = + | RulePlainConfiguration + | RuleWithOptions_for_SplinterRuleOptions; export type RulePlainConfiguration = "warn" | "error" | "info" | "off"; export interface RuleWithOptions_for_Null { /** @@ -626,6 +765,31 @@ export interface RuleWithOptions_for_Null { */ options: null; } +export interface RuleWithOptions_for_SplinterRuleOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: SplinterRuleOptions; +} +/** + * Shared options for all splinter rules. + +These options allow configuring per-rule filtering of database objects. + */ +export interface SplinterRuleOptions { + /** + * A list of glob patterns for database objects to ignore. + +Patterns use Unix-style globs where: - `*` matches any sequence of characters - `?` matches any single character + +Each pattern should be in the format `schema.object_name`, for example: - `"public.my_table"` - ignores a specific table - `"audit.*"` - ignores all objects in the audit schema - `"*.audit_*"` - ignores objects with audit_ prefix in any schema + */ + ignore?: string[]; +} export interface OpenFileParams { content: string; path: PgLSPath; diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index cf42e5145..c6b93982d 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -314,6 +314,10 @@ export interface PartialConfiguration { * The configuration for type checking */ plpgsqlCheck?: PartialPlPgSqlCheckConfiguration; + /** + * The configuration for splinter + */ + splinter?: PartialSplinterConfiguration; /** * The configuration for type checking */ @@ -391,7 +395,7 @@ export interface PartialLinterConfiguration { /** * List of rules */ - rules?: Rules; + rules?: LinterRules; } /** * The configuration of the filesystem @@ -415,6 +419,16 @@ export interface PartialPlPgSqlCheckConfiguration { */ enabled?: boolean; } +export interface PartialSplinterConfiguration { + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * List of rules + */ + rules?: SplinterRules; +} /** * The configuration for type checking. */ @@ -455,7 +469,7 @@ If we can't find the configuration, it will attempt to use the current working d */ useIgnoreFile?: boolean; } -export interface Rules { +export interface LinterRules { /** * It enables ALL rules. The rules that belong to `nursery` won't be enabled. */ @@ -466,6 +480,18 @@ export interface Rules { recommended?: boolean; safety?: Safety; } +export interface SplinterRules { + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + performance?: Performance; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + security?: Security; +} export type VcsClientKind = "git"; /** * A list of rules that belong to this group @@ -612,9 +638,122 @@ export interface Safety { */ transactionNesting?: RuleConfiguration_for_Null; } +/** + * A list of rules that belong to this group + */ +export interface Performance { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row + */ + authRlsInitplan?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Duplicate Index: Detects cases where two ore more identical indexes exist. + */ + duplicateIndex?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. + */ + multiplePermissivePolicies?: RuleConfiguration_for_SplinterRuleOptions; + /** + * No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. + */ + noPrimaryKey?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. + */ + tableBloat?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance. + */ + unindexedForeignKeys?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unused Index: Detects if an index has never been used and may be a candidate for removal. + */ + unusedIndex?: RuleConfiguration_for_SplinterRuleOptions; +} +/** + * A list of rules that belong to this group + */ +export interface Security { + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. + */ + authUsersExposed?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension in Public: Detects extensions installed in the `public` schema. + */ + extensionInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension Versions Outdated: Detects extensions that are not using the default (recommended) version. + */ + extensionVersionsOutdated?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema. + */ + fkeyToAuthUnique?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. + */ + foreignTableInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Function Search Path Mutable: Detects functions where the search_path parameter is not set. + */ + functionSearchPathMutable?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs + */ + insecureQueueExposedInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Materialized View in API: Detects materialized views that are accessible over the Data APIs. + */ + materializedViewInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. + */ + policyExistsRlsDisabled?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST + */ + rlsDisabledInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. + */ + rlsEnabledNoPolicy?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. + */ + rlsReferencesUserMetadata?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user + */ + securityDefinerView?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. + */ + unsupportedRegTypes?: RuleConfiguration_for_SplinterRuleOptions; +} export type RuleConfiguration_for_Null = | RulePlainConfiguration | RuleWithOptions_for_Null; +export type RuleConfiguration_for_SplinterRuleOptions = + | RulePlainConfiguration + | RuleWithOptions_for_SplinterRuleOptions; export type RulePlainConfiguration = "warn" | "error" | "info" | "off"; export interface RuleWithOptions_for_Null { /** @@ -626,6 +765,31 @@ export interface RuleWithOptions_for_Null { */ options: null; } +export interface RuleWithOptions_for_SplinterRuleOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: SplinterRuleOptions; +} +/** + * Shared options for all splinter rules. + +These options allow configuring per-rule filtering of database objects. + */ +export interface SplinterRuleOptions { + /** + * A list of glob patterns for database objects to ignore. + +Patterns use Unix-style globs where: - `*` matches any sequence of characters - `?` matches any single character + +Each pattern should be in the format `schema.object_name`, for example: - `"public.my_table"` - ignores a specific table - `"audit.*"` - ignores all objects in the audit schema - `"*.audit_*"` - ignores objects with audit_ prefix in any schema + */ + ignore?: string[]; +} export interface OpenFileParams { content: string; path: PgLSPath; diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index e5361e9cf..0f243909e 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -14,11 +14,17 @@ use xtask::*; struct ToolConfig { name: &'static str, category: RuleCategory, + /// Whether this tool operates on files (vs database) + handles_files: bool, } impl ToolConfig { - const fn new(name: &'static str, category: RuleCategory) -> Self { - Self { name, category } + const fn new(name: &'static str, category: RuleCategory, handles_files: bool) -> Self { + Self { + name, + category, + handles_files, + } } /// Derived: Directory name under pgls_configuration/src/ @@ -72,10 +78,10 @@ impl ToolConfig { /// All supported tools const TOOLS: &[ToolConfig] = &[ - ToolConfig::new("linter", RuleCategory::Lint), - ToolConfig::new("assists", RuleCategory::Action), - ToolConfig::new("splinter", RuleCategory::Lint), - ToolConfig::new("pglinter", RuleCategory::Lint), + ToolConfig::new("linter", RuleCategory::Lint, true), + ToolConfig::new("assists", RuleCategory::Action, true), + ToolConfig::new("splinter", RuleCategory::Lint, false), // Database linter, doesn't handle files + ToolConfig::new("pglinter", RuleCategory::Lint, true), ]; /// Visitor that collects rules for a specific category @@ -182,6 +188,38 @@ fn generate_lint_mod_file(tool: &ToolConfig) -> String { quote! {} }; + // Only file-based tools need ignore/include fields + let handles_files = tool.handles_files; + + let file_fields = if handles_files { + quote! { + /// A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns. + #[partial(bpaf(hide))] + pub ignore: StringSet, + + /// A list of Unix shell style patterns. The linter will include files/folders that will match these patterns. + #[partial(bpaf(hide))] + pub include: StringSet, + } + } else { + quote! {} + }; + + let file_defaults = if handles_files { + quote! { + ignore: Default::default(), + include: Default::default(), + } + } else { + quote! {} + }; + + let string_set_import = if handles_files { + quote! { use biome_deserialize::StringSet; } + } else { + quote! {} + }; + let content = quote! { //! Generated file, do not edit by hand, see `xtask/codegen` @@ -189,7 +227,7 @@ fn generate_lint_mod_file(tool: &ToolConfig) -> String { mod #generated_file_ident; - use biome_deserialize::StringSet; + #string_set_import use biome_deserialize_macros::{Merge, Partial}; use bpaf::Bpaf; pub use #generated_file_ident::*; @@ -208,13 +246,7 @@ fn generate_lint_mod_file(tool: &ToolConfig) -> String { #[partial(bpaf(pure(Default::default()), optional, hide))] pub rules: Rules, - /// A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns. - #[partial(bpaf(hide))] - pub ignore: StringSet, - - /// A list of Unix shell style patterns. The linter will include files/folders that will match these patterns. - #[partial(bpaf(hide))] - pub include: StringSet, + #file_fields } impl #config_struct { @@ -228,8 +260,7 @@ fn generate_lint_mod_file(tool: &ToolConfig) -> String { Self { enabled: true, rules: Default::default(), - ignore: Default::default(), - include: Default::default(), + #file_defaults } } } @@ -328,6 +359,10 @@ fn generate_lint_rules_file( quote! {} }; + // Schema name for the Rules struct (e.g., "LinterRules", "SplinterRules") + let rules_schema_name = format!("{}Rules", to_capitalized(tool.name)); + let rules_schema_name_lit = Literal::string(&rules_schema_name); + let rules_struct_content = quote! { //! Generated file, do not edit by hand, see `xtask/codegen` @@ -368,6 +403,7 @@ fn generate_lint_rules_file( #[derive(Clone, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] + #[cfg_attr(feature = "schema", schemars(rename = #rules_schema_name_lit))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Rules { /// It enables the lint rules recommended by Postgres Language Server. `true` by default. @@ -736,6 +772,7 @@ fn generate_lint_group_struct( } /// Extract the first paragraph from markdown documentation as a summary +/// Stops at the first heading (## etc) or end of first paragraph fn extract_summary_from_docs(docs: &str) -> String { let mut summary = String::new(); let parser = Parser::new(docs); @@ -756,6 +793,16 @@ fn extract_summary_from_docs(docs: &str) -> String { Event::End(TagEnd::Paragraph) => { break; } + // Stop at H2+ headings (subsections) - H1 is the title + Event::Start(Tag::Heading { level, .. }) + if level != pulldown_cmark::HeadingLevel::H1 => + { + break; + } + // Add separator after H1 heading ends + Event::End(TagEnd::Heading(_)) => { + summary.push_str(": "); + } Event::Start(tag) => match tag { Tag::Strong | Tag::Paragraph => continue, _ => { diff --git a/xtask/codegen/src/generate_splinter.rs b/xtask/codegen/src/generate_splinter.rs index 79616821a..65cb0d0e1 100644 --- a/xtask/codegen/src/generate_splinter.rs +++ b/xtask/codegen/src/generate_splinter.rs @@ -267,52 +267,42 @@ fn generate_rule_file(category_dir: &Path, metadata: &SqlRuleMetadata) -> Result // Build comprehensive documentation let requires_supabase_note = if requires_supabase { - "\n/// \n/// **Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). \n/// It will be automatically skipped if these roles don't exist in your database.".to_string() + "\n\n**Note:** This rule requires Supabase roles (`anon`, `authenticated`, `service_role`). It will be automatically skipped if these roles don't exist in your database.".to_string() } else { String::new() }; + // Build doc string as proper markdown (no /// prefixes - those are for source code comments) let doc_string = format!( - r#"/// # {title} -/// -/// {description}{requires_supabase_note} -/// -/// ## SQL Query -/// -/// ```sql -{sql_query_commented} -/// ``` -/// -/// ## Configuration -/// -/// Enable or disable this rule in your configuration: -/// -/// ```json -/// {{ -/// "splinter": {{ -/// "rules": {{ -/// "{category_lower}": {{ -/// "{name}": "warn" -/// }} -/// }} -/// }} -/// }} -/// ``` -/// -/// ## Remediation -/// -/// See: <{remediation}>"#, - title = title, - description = description, - requires_supabase_note = requires_supabase_note, - sql_query_commented = sql_query - .lines() - .map(|line| format!("/// {line}")) - .collect::>() - .join("\n"), - category_lower = category_lower, - name = name, - remediation = remediation, + r#"# {title} + +{description}{requires_supabase_note} + +## SQL Query + +```sql +{sql_query} +``` + +## Configuration + +Enable or disable this rule in your configuration: + +```json +{{ + "splinter": {{ + "rules": {{ + "{category_lower}": {{ + "{name}": "warn" + }} + }} + }} +}} +``` + +## Remediation + +See: <{remediation}>"#, ); let content = quote! { @@ -326,6 +316,7 @@ fn generate_rule_file(category_dir: &Path, metadata: &SqlRuleMetadata) -> Result version: "1.0.0", name: #name, severity: #severity, + recommended: true, } }