Skip to content

Audit (security)

scythe audit runs a curated set of security rules over your SQL schema and queries. It catches the kinds of issues that lint rules don't surface and that would otherwise only show up in a manual review or an incident: dangerous functions, over-broad GRANTs, cartesian joins, unbounded LIKE patterns, SECURITY DEFINER functions without a pinned search_path, role privilege escalation, literal passwords in DDL, weak hashes over credential columns, SELECT * over PII columns, and runtime session-state mutation.

The output is human-readable by default, and SARIF 2.1.0 or flat JSON for CI integration.

Quick start

# Audit the SQL referenced by scythe.toml
scythe audit

# Audit one or more files directly
scythe audit migrations/*.sql

# Emit SARIF for GitHub code scanning
scythe audit --format sarif -o audit.sarif

Exit codes:

Code Meaning
0 No error-severity findings, or --exit-zero was set
1 Configuration error (bad scythe.toml, missing files, malformed rule file)
2 One or more error-severity findings — distinct from scythe lint so CI can tell them apart

Rule catalog

The shipped rules use the SC-SEC* prefix. Use scythe audit --list-rules to print the current set with effective severities, and scythe audit --explain <RULE_ID> for the description and CWE references of a specific rule.

ID Name Severity What it catches
SC-SEC01 dangerous-function error Calls to functions that grant filesystem, network, or shell access (CWE-78)
SC-SEC02 grant-all error GRANT ALL privilege widening (CWE-269)
SC-SEC03 grant-to-public error GRANT … TO PUBLIC (CWE-269)
SC-SEC04 superuser-role error CREATE/ALTER ROLE with SUPERUSER / CREATEROLE / similar high-privilege attributes (CWE-269) — Postgres only
SC-SEC05 literal-password error Hard-coded literal password in CREATE/ALTER ROLE (CWE-798) — Postgres only
SC-SEC06 weak-hash-in-auth error md5() / sha1() over credential-like columns (CWE-327, CWE-916)
SC-SEC07 select-star-pii warn SELECT * against tables with PII or credential columns (CWE-200)
SC-SEC08 cartesian-join error Unconstrained join producing a cartesian product (CWE-400)
SC-SEC09 unbounded-like warn LIKE '%…%' — both-side wildcards, full-scan, DoS-prone (CWE-1333)
SC-SEC10 security-definer-no-search-path error SECURITY DEFINER function without pinned search_path (CWE-426) — Postgres only
SC-SEC11 session-mutation error SET ROLE / SET SESSION AUTHORIZATION / RESET ROLE inside application SQL (CWE-269) — Postgres only

PG-only rules are no-ops on other dialects: when the dialect is not PostgreSQL they skip the AST entirely instead of producing false positives.

Suppression

A rule firing in one specific spot can be silenced with an inline annotation on the line above the offending statement:

-- scythe-audit: ignore[SC-SEC02] reason="security-reviewed: vetted role"
GRANT ALL ON internal_audit TO ops_admin;

Multiple rule IDs can be silenced on the same line by comma-separating them: ignore[SC-SEC01,SC-SEC02]. The annotation attaches to the next non-blank, non-comment line; only statements that begin on that line are exempt.

To run an audit without honouring any suppressions (useful for periodic strict scans), pass --ignore-suppressions.

Severity filtering and exit codes

CI gates often want a graduated rollout: surface warnings, but only fail the build on errors. The default behaviour matches that.

Want Flag
Only see errors --severity error
Surface warnings but never fail the build --exit-zero
Block on any error finding (default)
Treat the run as advisory --severity warn --exit-zero

User-defined rules

Custom rules live in scythe.toml under [audit], or in a separate TOML file referenced by extra_rules. Custom rule IDs must start with USER- to avoid collisions with shipped rules.

[audit]
extra_rules = ["./security_rules.toml"]

[[audit.rule]]
id = "USER-001"
name = "no-debug-functions"
category = "security"
severity = "error"
description = "calls to debug-only functions should not ship"
message = "call to debug function `{func}` — remove before merging"
matcher = "function_name_in_set"

[audit.rule.matcher_args]
functions = ["dump_internal_state", "debug_print"]

The matcher field references one of the in-tree matchers. Run scythe audit --list-rules after editing scythe.toml to confirm your rule is picked up.

Available matchers

Matcher Required matcher_args
function_name_in_set functions = ["fn1", "fn2", ...]
grant_kind kind = "all"
grantee_includes grantee = "public"
cartesian_join --
unbounded_pattern --
security_definer_no_search_path --
role_with_attribute attributes = ["superuser", "createdb", ...]
role_password_literal --
weak_hash_over_sensitive_column functions = ["md5", "sha1"], column_patterns = ["password", ...]
select_star_over_pii_columns column_patterns = ["password", "ssn", ...]
session_mutation kinds = ["set_role", "set_session_authorization", "reset_role"]

CI integration

GitHub Actions — SARIF upload

- name: Run scythe audit
  run: scythe audit --format sarif -o audit.sarif --exit-zero
- name: Upload SARIF
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: audit.sarif

--exit-zero keeps the job green so the SARIF upload always runs; GitHub Code Scanning still surfaces findings in the PR.

GitLab CI — SAST report

audit:
  image: rust:latest
  script:
    - cargo install scythe-cli
    - scythe audit --format json -o gl-sast-report.json --exit-zero
  artifacts:
    reports:
      sast: gl-sast-report.json

Pre-commit — block on errors

Use the public hooks published by this repo:

- repo: https://github.com/Goldziher/scythe
  rev: v0.9.0              # pin to a released tag
  hooks:
    - id: scythe-audit     # SC-SEC*/SC-RLS*/SC-MIG*/SC-CHK* on changed files
    # - id: scythe-lint    # full pipeline: sqruff + scythe-lint + audit (needs scythe.toml)

scythe-audit runs on every staged .sql file with the default postgres dialect. Override per-hook via the standard pre-commit args: block:

- id: scythe-audit
  args: [--dialect, mysql, --severity, warn]

scythe lint already invokes the audit rule pack with dialect gating — rules whose dialects list excludes the configured [[sql]].engine are silently skipped, so a mysql project will not see SC-MIG* (postgres-only) findings. The scythe-lint hook is the right choice when a scythe.toml is present at the repo root; scythe-audit covers projects that don't (yet) use scythe for codegen.

By default scythe audit exits 2 on error findings, which pre-commit treats as a failed hook. For advisory CI integration that publishes findings without blocking, add --exit-zero.