From 9d0557948004ec7c34e78c28e83a6ae2a11eeb0d Mon Sep 17 00:00:00 2001 From: Vamsi-klu Date: Sun, 25 Oct 2026 00:16:07 +0000 Subject: [PATCH 1/2] docs: publish rule catalog for check --checks safety, lint and policy Add Safety, Lint and Policy rule tables to the check docs, linked from the Available Checks table. Every rule was observed from the real check CLI against altimate-core 0.9.0; none is invented. Fix the JSON/text examples, which showed `L003` (missing_where_delete) for a SELECT * finding. Fixes #1125 Co-Authored-By: Claude Opus 5.5 --- docs/docs/usage/check.md | 117 +++++++++++++++++++++++++++++++++++---- 1 file changed, 107 insertions(+), 10 deletions(-) diff --git a/docs/docs/usage/check.md b/docs/docs/usage/check.md index 21494e88b7..b6b8e8d573 100644 --- a/docs/docs/usage/check.md +++ b/docs/docs/usage/check.md @@ -42,17 +42,17 @@ altimate-code check --fail-on warning | Check | What It Does | Needs Schema? | Default | |------------|--------------------------------------------------------------|---------------|---------| -| `lint` | Anti-pattern detection (SELECT *, cartesian JOINs, etc.) | Optional | Yes | +| `lint` | Anti-pattern detection (SELECT *, cartesian JOINs, etc.) — [rules](#lint-rules) | Optional | Yes | | `validate` | DataFusion SQL validation (column exists, type matches) | Yes | No | -| `safety` | SQL injection and dangerous pattern detection | No | Yes | -| `policy` | Custom guardrails (block SELECT *, require LIMIT, etc.) | Optional | No | +| `safety` | SQL injection and dangerous pattern detection — [rules](#safety-rules) | No | Yes | +| `policy` | Custom guardrails (block SELECT *, require LIMIT, etc.) — [rules](#policy-rules) | Optional | No | | `pii` | PII column detection and query exposure | Optional | No | | `semantic` | Semantic validation (cartesian products, wrong JOINs) | Optional | No | | `grade` | SQL quality grading with recommendations | Optional | No | `policy` and `semantic` never report a statement the engine cannot plan as clean. Such statements include table-valued functions (`TABLE(fn())`, Snowflake `LATERAL FLATTEN`, DuckDB `read_csv(...)`). `policy` fails closed: it reports a `policy_not_evaluated` error and treats the statement as blocked. `semantic` reports the skip as a finding: an error when `--schema` is given, and a warning without it, which does not fail a run with `--fail-on error`. Earlier versions rejected these statements as invalid SQL. -Findings usually carry a machine-readable `rule` field (e.g. `unbalanced_quote`, `sql_injection`, `L003`) visible in `--format json` output — see the [Finding Object](#finding-object) schema below. `safety` and `policy` findings always include one; `lint` findings may omit both `rule` and `code` if the engine didn't attach them, so switch on presence rather than assuming. +Findings usually carry a machine-readable `rule` field (e.g. `unbalanced_quote`, `select_star`) visible in `--format json` output — see the [Finding Object](#finding-object) schema below and the [Rule Catalog](#rule-catalog). `safety` and `policy` findings always include one; `lint` findings also carry the engine's `code` (e.g. `L001`), but may omit both `rule` and `code` if the engine didn't attach them, so switch on presence rather than assuming. By default, `lint` and `safety` are enabled. Override with `--checks`: @@ -97,11 +97,11 @@ When using `--format json`, the command writes structured JSON to stdout (diagno { "file": "models/staging/stg_orders.sql", "line": 5, - "column": 1, - "rule": "L003", + "code": "L001", + "rule": "select_star", "severity": "warning", - "message": "SELECT * used -- enumerate columns explicitly", - "suggestion": "Replace SELECT * with explicit column list" + "message": "SELECT * is used — consider listing columns explicitly", + "suggestion": "Replace * with specific column names for clarity and performance" } ], "error_count": 0, @@ -161,8 +161,8 @@ The default `text` format is designed for human consumption: Checked 3 file(s) with [lint, safety] --- LINT --- - WARNING models/staging/stg_orders.sql:5:1 [L003]: SELECT * used -- enumerate columns explicitly - suggestion: Replace SELECT * with explicit column list + WARNING models/staging/stg_orders.sql:5 [select_star]: SELECT * is used — consider listing columns explicitly + suggestion: Replace * with specific column names for clarity and performance 1 finding(s): 0 error(s), 1 warning(s), 0 info PASS @@ -283,6 +283,103 @@ altimate-code check --severity info --fail-on warning --- +## Rule Catalog + +The engine does not publish a rule list. The rules below are the ones `check` actually reported when run against a probe corpus with the `@altimateai/altimate-core` version pinned in `packages/opencode/package.json` (0.9.0 when this list was made). Rules change when the engine is upgraded, and the engine may emit rules not listed here. To see what fires on your SQL, run `--format json` and read each finding's `rule` field (and `code` for lint). + +`check` itself adds a `-error` rule (for example `safety-error`) when an engine call throws. + +### Safety Rules + +`check --checks safety` maps engine severities to `check` severities: `critical` and `high` become `error`, `medium` becomes `warning`, `low` becomes `info`. Safety findings carry no `code`. + +| Rule | What it flags | Example | Severity | +|------|---------------|---------|----------| +| `multi_statement` | A semicolon followed by a dangerous statement, or a disallowed statement type (`DROP`, `BEGIN`, `COMMIT`, …) | `SELECT 1; DROP TABLE users` | error | +| `stacked_queries` | More than one statement in a single input | `SELECT 1; SELECT 2` | error | +| `union_injection` | `UNION SELECT`, including comment-obfuscated `UNION/**/SELECT` | `SELECT name FROM users WHERE id = 1 UNION SELECT password FROM admins` | error | +| `tautology_attack` | Always-true predicates: `OR 1=1`, `OR 'a'='a'`, `OR TRUE`, `OR NOT FALSE` | `SELECT * FROM users WHERE name = 'a' OR 1=1` | error | +| `unbalanced_quote` | An unclosed single-quoted string (possible injection breakout) | `SELECT * FROM users WHERE name = 'abc` | error | +| `comment_injection` | A string literal followed by `--`, `#` or `/* */`, or the `';--` truncation pattern | `SELECT * FROM users WHERE name = 'a' -- x` | error | +| `info_schema_probe` | Reads from `information_schema` or Snowflake system functions | `SELECT * FROM information_schema.tables` | error | +| `transaction_escape` | A transaction control statement | `COMMIT; SELECT 1` | error | +| `string_concat_predicate` | `CONCAT()`, `||` or `+` string concatenation in a `WHERE` clause | `SELECT * FROM users WHERE name = CONCAT('a', 'b')` | warning | +| `sleep_attack` | Time-based blind injection (`SLEEP(...)`) | `SELECT * FROM users WHERE id = 1 AND SLEEP(5)` | error | +| `encoding_bypass` | `CHAR()` encoding, URL-encoded payloads, Jinja statement tags in SQL, hex literals | `SELECT * FROM users WHERE name = CHAR(97,100,109,105,110)` | error (hex literal `0x…`: warning) | + +### Lint Rules + +Lint findings carry both a `code` and a `rule`. Almost every `SELECT` without a `LIMIT` also reports `L030`, so the examples below list only the rule they demonstrate. + +| Code | Rule | What it flags | Example | Severity | +|------|------|---------------|---------|----------| +| `L001` | `select_star` | `SELECT *` | `SELECT * FROM orders` | warning | +| `L002` | `missing_where_update` | `UPDATE` without `WHERE` | `UPDATE orders SET status = 'x'` | error | +| `L003` | `missing_where_delete` | `DELETE` without `WHERE` | `DELETE FROM orders` | error | +| `L004` | `cartesian_product` | Several tables in `FROM` with no `WHERE` relating them | `SELECT a.id, b.id FROM a, b` | warning | +| `L005` | `select_distinct_smell` | `SELECT DISTINCT` with joins (may hide a bad join) | `SELECT DISTINCT o.id FROM orders o JOIN customers c ON o.customer_id = c.id` | info | +| `L006` | `order_by_in_subquery` | `ORDER BY` inside a subquery | `SELECT id FROM (SELECT id FROM orders ORDER BY id) t` | warning | +| `L008` | `like_leading_wildcard` | `LIKE` pattern starting with `%` | `SELECT id FROM orders WHERE name LIKE '%abc'` | warning | +| `L009` | `not_null_comparison` | `= NULL` or `!= NULL` | `SELECT id FROM orders WHERE status = NULL` | error | +| `L010` | `count_star_vs_column` | `COUNT(column)`, which skips NULLs | `SELECT count(id) FROM orders` | info | +| `L011` | `redundant_distinct_with_group_by` | `DISTINCT` together with `GROUP BY` | `SELECT DISTINCT id FROM orders GROUP BY id` | info | +| `L012` | `missing_table_alias` | Multi-table query without table aliases | `SELECT a.id, b.id FROM a, b` | info | +| `L013` | `correlated_subquery` | Correlated subquery (N+1 pattern) | `SELECT id FROM orders WHERE NOT EXISTS (SELECT 1 FROM x WHERE x.id = orders.id)` | warning | +| `L014` | `non_sargable_predicate` | Column wrapped in a function in a predicate | `SELECT id FROM orders WHERE upper(name) = 'X'` | warning | +| `L015` | `not_in_nullable` | `NOT IN (subquery)`, which returns nothing if the subquery yields a NULL | `SELECT id FROM orders WHERE name NOT IN (SELECT name FROM x)` | error | +| `L016` | `unused_cte` | CTE defined but never referenced | `WITH a AS (SELECT 1 AS x) SELECT 2` | info | +| `L017` | `union_without_all` | `UNION` without `ALL` (forces a dedup sort) | `SELECT id FROM orders UNION SELECT id FROM other` | warning | +| `L018` | `implicit_cross_join` | Comma-separated tables in `FROM` | `SELECT a.id, b.id FROM a, b` | warning | +| `L019` | `select_star_in_subquery` | `SELECT *` inside a subquery | `SELECT id FROM orders o WHERE EXISTS (SELECT * FROM x)` | warning | +| `L020` | `or_in_join` | `OR` in a `JOIN ... ON` condition | `SELECT o.id FROM orders o JOIN customers c ON o.customer_id = c.id OR o.id = c.id` | warning | +| `L021` | `order_by_without_limit_subquery` | `ORDER BY` in a subquery that has no `LIMIT` | `SELECT id FROM (SELECT id FROM orders ORDER BY id) t` | warning | +| `L022` | `large_in_list` | `IN` list with more than 100 values | `SELECT id FROM orders WHERE id IN (1, 2, …, 101)` | warning | +| `L024` | `non_equi_join` | Join on a non-equality condition | `SELECT o.id FROM orders o JOIN customers c ON o.customer_id > c.id` | warning | +| `L025` | `window_without_partition` | `ROW_NUMBER()` and similar window functions without `PARTITION BY` | `SELECT row_number() OVER () FROM t` | info | +| `L027` | `function-on-filter-column` | Function applied to a column in `WHERE` | `SELECT id FROM orders WHERE upper(name) = 'X'` | warning | +| `L028` | `function-on-join-column` | Function applied to a column in a join condition | `SELECT o.id FROM orders o JOIN customers c ON upper(o.name) = c.name` | warning | +| `L029` | `aggregation-before-join` | Aggregation in a subquery that is then joined | `SELECT o.id FROM orders o JOIN (SELECT customer_id, sum(amount) s FROM orders GROUP BY customer_id) a ON a.customer_id = o.customer_id` | error | +| `L030` | `select-without-limit` | `SELECT` without `LIMIT` | `SELECT id FROM orders` | warning | +| `L031` | `create-or-replace-table` | `CREATE OR REPLACE TABLE` (drops and recreates) | `CREATE OR REPLACE TABLE x AS SELECT id FROM orders` | info | +| `L032` | `division_by_column_no_guard` | Division by a column with no zero guard | `SELECT amount / qty FROM orders` | warning | +| `L033` | `non_portable_function` | Dialect-specific function (`NVL`, `IFF`, …) | `SELECT nvl(a, 0) FROM t` | info | +| `L035` | `non_portable_type` | Dialect-specific type (`VARIANT`, …) | `SELECT id::VARIANT FROM orders` | info | +| `L038` | `timezone_in_hash_key` | Row-key hash over a timezone-dependent value | `SELECT md5(convert_timezone('UTC', created_at)::varchar) FROM orders` | warning | +| `L040` | `monetary_float_cast` | Money-like column cast to `FLOAT`/`DOUBLE`/`REAL` | `SELECT CAST(amount AS FLOAT) FROM orders` | warning | +| `L042` | `case_sensitive_like` | `LIKE`, which is case-sensitive on most warehouses | `SELECT id FROM orders WHERE name LIKE 'abc%'` | info | +| `L043` | `outer_join_filter_in_where` | `WHERE` predicate on the outer side of a `LEFT`/`RIGHT` join (turns it into an inner join) | `SELECT o.id FROM orders o LEFT JOIN customers c ON o.cid = c.id WHERE c.name = 'x'` | warning | +| `L044` | `null_propagating_concat` | `||` concatenation, which is NULL if any operand is NULL | `SELECT a || b FROM t` | warning | +| `L045` | `greatest_least_null` | `GREATEST`/`LEAST` over nullable columns | `SELECT greatest(qty, price) FROM orders` | warning | +| `L046` | `distinct_with_window` | `SELECT DISTINCT` together with a window function | `SELECT DISTINCT id, row_number() OVER (PARTITION BY id ORDER BY id) FROM orders` | warning | +| `L048` | `cast_division_to_int` | Division result cast to an integer (truncates) | `SELECT CAST(qty / price AS INTEGER) FROM orders` | warning | +| `L049` | `clock_in_filter` | `current_date`/`now()` in a `WHERE` or join predicate | `SELECT id FROM orders WHERE created_at > current_date` | warning | +| `L051` | `full_outer_join` | `FULL OUTER JOIN` | `SELECT id FROM orders FULL OUTER JOIN x ON orders.id = x.id` | info | +| `L052` | `join_without_condition` | Join with no real condition (`ON 1=1`, `ON true`, no `ON`) | `SELECT id FROM orders o JOIN customers c ON 1=1` | warning | +| `L053` | `null_in_in_list` | `NULL` inside an `IN (...)` list | `SELECT id FROM orders WHERE id IN (1, NULL)` | warning | +| `L054` | `sum_of_ratio` | `SUM()` of a per-row ratio | `SELECT sum(amount / qty) FROM orders` | warning | + +### Policy Rules + +Policy findings are named after the built-in check that fired. Each check is switched on by a key in the `--policy` JSON file. In this engine version the `rules` / `pattern` entries shown under [Policy File Format](#policy-file-format) produced no findings in testing; use the keys below. + +Except `allowed_statements` and `blocked_tables`, these checks run only when `--schema` resolves the query's tables. Otherwise `check` reports `validation_failed`, and `blocked_columns` reports nothing. The examples were checked with a JSON schema file such as `{"tables": {"customers": {"columns": [{"name": "email", "type": "VARCHAR"}]}}}`. + +| Rule | Enabled by | Example | Severity | +|------|------------|---------|----------| +| `max_joins` | `{"cost_control": {"max_joins": 1}}` | A query with two joins | error | +| `max_tables` | `{"cost_control": {"max_tables": 1}}` | `SELECT o.id FROM orders o JOIN customers c ON o.customer_id = c.id` | error | +| `block_select_star` | `{"cost_control": {"block_select_star": true}}` | `SELECT * FROM orders` | error | +| `require_limit` | `{"cost_control": {"require_limit": true}}` | `SELECT id FROM orders` | error | +| `max_complexity_score` | `{"cost_control": {"max_complexity_score": 5}}` | A join plus an `IN` subquery and `GROUP BY` (score 9) | error | +| `blocked_columns` | `{"data_protection": {"blocked_columns": [{"table": "customers", "columns": ["email"]}]}}` | `SELECT email FROM customers` | error | +| `blocked_tables` | `{"data_protection": {"blocked_tables": [{"table": "customers"}]}}` | `SELECT id FROM customers` | error | +| `require_where_clause` | `{"data_protection": {"require_where_clause": true}}` | `DELETE FROM orders` | error | +| `allowed_statements` | `{"query_patterns": {"allowed_statements": ["SELECT"]}}` | `DROP TABLE orders` | error | +| `validation_failed` | Any policy (even `{}`), when the query does not validate against `--schema` or no `--schema` is given | `SELECT * FROM orders` with no schema | error | +| `policy_not_evaluated` | Any policy, on statements the engine cannot plan (see [Available Checks](#available-checks)) | `SELECT * FROM TABLE(my_fn())` | error | + +--- + ## CI/CD Integration ### GitHub Actions From a4bc1c064c760031b8f6bcf3fff5947d4bc6470e Mon Sep 17 00:00:00 2001 From: Vamsi-klu Date: Sun, 25 Oct 2026 01:17:04 +0000 Subject: [PATCH 2/2] docs: correct rule catalog examples --- docs/docs/usage/check.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/usage/check.md b/docs/docs/usage/check.md index b6b8e8d573..9715782bb1 100644 --- a/docs/docs/usage/check.md +++ b/docs/docs/usage/check.md @@ -303,7 +303,7 @@ The engine does not publish a rule list. The rules below are the ones `check` ac | `comment_injection` | A string literal followed by `--`, `#` or `/* */`, or the `';--` truncation pattern | `SELECT * FROM users WHERE name = 'a' -- x` | error | | `info_schema_probe` | Reads from `information_schema` or Snowflake system functions | `SELECT * FROM information_schema.tables` | error | | `transaction_escape` | A transaction control statement | `COMMIT; SELECT 1` | error | -| `string_concat_predicate` | `CONCAT()`, `||` or `+` string concatenation in a `WHERE` clause | `SELECT * FROM users WHERE name = CONCAT('a', 'b')` | warning | +| `string_concat_predicate` | `CONCAT()`, `\|\|` or `+` string concatenation in a `WHERE` clause | `SELECT * FROM users WHERE name = CONCAT('a', 'b')` | warning | | `sleep_attack` | Time-based blind injection (`SLEEP(...)`) | `SELECT * FROM users WHERE id = 1 AND SLEEP(5)` | error | | `encoding_bypass` | `CHAR()` encoding, URL-encoded payloads, Jinja statement tags in SQL, hex literals | `SELECT * FROM users WHERE name = CHAR(97,100,109,105,110)` | error (hex literal `0x…`: warning) | @@ -348,7 +348,7 @@ Lint findings carry both a `code` and a `rule`. Almost every `SELECT` without a | `L040` | `monetary_float_cast` | Money-like column cast to `FLOAT`/`DOUBLE`/`REAL` | `SELECT CAST(amount AS FLOAT) FROM orders` | warning | | `L042` | `case_sensitive_like` | `LIKE`, which is case-sensitive on most warehouses | `SELECT id FROM orders WHERE name LIKE 'abc%'` | info | | `L043` | `outer_join_filter_in_where` | `WHERE` predicate on the outer side of a `LEFT`/`RIGHT` join (turns it into an inner join) | `SELECT o.id FROM orders o LEFT JOIN customers c ON o.cid = c.id WHERE c.name = 'x'` | warning | -| `L044` | `null_propagating_concat` | `||` concatenation, which is NULL if any operand is NULL | `SELECT a || b FROM t` | warning | +| `L044` | `null_propagating_concat` | `\|\|` concatenation, which is NULL if any operand is NULL | `SELECT a \|\| b FROM t` | warning | | `L045` | `greatest_least_null` | `GREATEST`/`LEAST` over nullable columns | `SELECT greatest(qty, price) FROM orders` | warning | | `L046` | `distinct_with_window` | `SELECT DISTINCT` together with a window function | `SELECT DISTINCT id, row_number() OVER (PARTITION BY id ORDER BY id) FROM orders` | warning | | `L048` | `cast_division_to_int` | Division result cast to an integer (truncates) | `SELECT CAST(qty / price AS INTEGER) FROM orders` | warning | @@ -370,7 +370,7 @@ Except `allowed_statements` and `blocked_tables`, these checks run only when `-- | `max_tables` | `{"cost_control": {"max_tables": 1}}` | `SELECT o.id FROM orders o JOIN customers c ON o.customer_id = c.id` | error | | `block_select_star` | `{"cost_control": {"block_select_star": true}}` | `SELECT * FROM orders` | error | | `require_limit` | `{"cost_control": {"require_limit": true}}` | `SELECT id FROM orders` | error | -| `max_complexity_score` | `{"cost_control": {"max_complexity_score": 5}}` | A join plus an `IN` subquery and `GROUP BY` (score 9) | error | +| `max_complexity_score` | `{"cost_control": {"max_complexity_score": 5}}` | A join plus an `IN` subquery and `GROUP BY` | error | | `blocked_columns` | `{"data_protection": {"blocked_columns": [{"table": "customers", "columns": ["email"]}]}}` | `SELECT email FROM customers` | error | | `blocked_tables` | `{"data_protection": {"blocked_tables": [{"table": "customers"}]}}` | `SELECT id FROM customers` | error | | `require_where_clause` | `{"data_protection": {"require_where_clause": true}}` | `DELETE FROM orders` | error |