|
| 1 | +<p align="center"> |
| 2 | + <h1 align="center">sqlaudit</h1> |
| 3 | + <p align="center">SQL Query Auditor & Optimizer — analyze SQL for performance, security, and best practices</p> |
| 4 | +</p> |
| 5 | + |
| 6 | +<p align="center"> |
| 7 | + <a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.9+-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python"></a> |
| 8 | + <a href="https://github.com/JSLEEKR/sqlaudit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License"></a> |
| 9 | + <a href="https://github.com/JSLEEKR/sqlaudit"><img src="https://img.shields.io/badge/Tests-287-blue?style=for-the-badge" alt="Tests"></a> |
| 10 | + <a href="https://github.com/JSLEEKR/sqlaudit"><img src="https://img.shields.io/badge/Rules-27-orange?style=for-the-badge" alt="Rules"></a> |
| 11 | + <a href="https://github.com/JSLEEKR/sqlaudit"><img src="https://img.shields.io/badge/Zero_Dependencies-brightgreen?style=for-the-badge" alt="Zero Dependencies"></a> |
| 12 | +</p> |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Why This Exists |
| 17 | + |
| 18 | +Every SQL query you write is a potential performance bottleneck, security vulnerability, or maintenance headache. Code linters catch bugs in Python, Go, and TypeScript — but who catches bugs in your SQL? |
| 19 | + |
| 20 | +**sqlaudit** is a zero-dependency SQL static analyzer that catches: |
| 21 | +- **Performance killers**: `SELECT *`, missing `WHERE` on `DELETE/UPDATE`, leading wildcards in `LIKE`, excessive JOINs |
| 22 | +- **Security risks**: SQL injection patterns, `GRANT ALL`, always-true conditions (`1=1`), comment injection |
| 23 | +- **Correctness bugs**: `= NULL` instead of `IS NULL`, `HAVING` without `GROUP BY`, implicit column lists |
| 24 | +- **Style issues**: inconsistent keyword casing, implicit joins, unnecessary parentheses |
| 25 | + |
| 26 | +No database connection required. No configuration needed. Just point it at SQL and get actionable feedback. |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## Installation |
| 31 | + |
| 32 | +```bash |
| 33 | +pip install sqlaudit |
| 34 | +``` |
| 35 | + |
| 36 | +Or install from source: |
| 37 | + |
| 38 | +```bash |
| 39 | +git clone https://github.com/JSLEEKR/sqlaudit.git |
| 40 | +cd sqlaudit |
| 41 | +pip install -e . |
| 42 | +``` |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## Quick Start |
| 47 | + |
| 48 | +### CLI Usage |
| 49 | + |
| 50 | +```bash |
| 51 | +# Audit a SQL file |
| 52 | +sqlaudit audit -f queries.sql |
| 53 | + |
| 54 | +# Audit a single query |
| 55 | +sqlaudit audit -q "SELECT * FROM users WHERE name = NULL" |
| 56 | + |
| 57 | +# Audit from stdin |
| 58 | +cat migration.sql | sqlaudit audit --stdin |
| 59 | + |
| 60 | +# JSON output for CI/CD integration |
| 61 | +sqlaudit audit -f queries.sql --format json |
| 62 | + |
| 63 | +# Only check security rules |
| 64 | +sqlaudit audit -f queries.sql --category security |
| 65 | + |
| 66 | +# Fail CI pipeline on any error or warning |
| 67 | +sqlaudit audit -f queries.sql --fail-on warning |
| 68 | + |
| 69 | +# Show fix suggestions |
| 70 | +sqlaudit audit -q "DELETE FROM users" --verbose |
| 71 | + |
| 72 | +# List all available rules |
| 73 | +sqlaudit rules |
| 74 | +``` |
| 75 | + |
| 76 | +### Python API |
| 77 | + |
| 78 | +```python |
| 79 | +from sqlaudit import Analyzer |
| 80 | + |
| 81 | +analyzer = Analyzer() |
| 82 | +report = analyzer.analyze(""" |
| 83 | + SELECT * FROM users WHERE name = NULL; |
| 84 | + DELETE FROM audit_logs; |
| 85 | + INSERT INTO users VALUES (1, 'test'); |
| 86 | +""") |
| 87 | + |
| 88 | +print(f"Score: {report.score}/100") |
| 89 | +print(f"Errors: {report.total_errors}") |
| 90 | +print(f"Warnings: {report.total_warnings}") |
| 91 | + |
| 92 | +for query in report.queries: |
| 93 | + for finding in query.findings: |
| 94 | + print(f" [{finding.severity.value}] {finding.rule_id}: {finding.message}") |
| 95 | + print(f" Fix: {finding.suggestion}") |
| 96 | +``` |
| 97 | + |
| 98 | +### Quick Check |
| 99 | + |
| 100 | +```python |
| 101 | +from sqlaudit import Analyzer |
| 102 | + |
| 103 | +analyzer = Analyzer() |
| 104 | +findings = analyzer.check_query("DELETE FROM important_table") |
| 105 | + |
| 106 | +for f in findings: |
| 107 | + print(f"{f.rule_id}: {f.message}") |
| 108 | +# P002: DELETE without WHERE clause will affect all rows |
| 109 | +``` |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +## Rules Reference |
| 114 | + |
| 115 | +sqlaudit ships with 27 built-in rules across 5 categories. |
| 116 | + |
| 117 | +### Performance (P001-P010) |
| 118 | + |
| 119 | +| Rule | Severity | Description | |
| 120 | +|------|----------|-------------| |
| 121 | +| P001 | WARNING | `SELECT *` retrieves all columns | |
| 122 | +| P002 | ERROR | `UPDATE/DELETE` without `WHERE` clause | |
| 123 | +| P003 | WARNING | Leading wildcard in `LIKE` prevents index usage | |
| 124 | +| P004 | WARNING | Too many JOINs (>5 tables) | |
| 125 | +| P005 | WARNING | `NOT IN` with subquery (NULL handling issues) | |
| 126 | +| P006 | INFO | Multiple `OR` on same column (use `IN` instead) | |
| 127 | +| P007 | HINT | `SELECT` without `LIMIT` | |
| 128 | +| P008 | WARNING | Function on column in `WHERE` prevents index usage | |
| 129 | +| P009 | HINT | `SELECT DISTINCT` with JOINs may indicate bad join | |
| 130 | +| P010 | INFO | `ORDER BY` without `LIMIT` sorts entire result set | |
| 131 | + |
| 132 | +### Security (S001-S007) |
| 133 | + |
| 134 | +| Rule | Severity | Description | |
| 135 | +|------|----------|-------------| |
| 136 | +| S001 | ERROR | String concatenation (SQL injection risk) | |
| 137 | +| S002 | ERROR | `GRANT ALL PRIVILEGES` (excessive permissions) | |
| 138 | +| S003 | WARNING | `DROP` without `IF EXISTS` | |
| 139 | +| S004 | WARNING | `TRUNCATE` detected (no row-level logging) | |
| 140 | +| S005 | ERROR | Comment patterns inside string literals | |
| 141 | +| S006 | WARNING | `UNION` injection pattern | |
| 142 | +| S007 | WARNING | Always-true condition (`1=1`, `'a'='a'`) | |
| 143 | + |
| 144 | +### Best Practice (B001-B005) |
| 145 | + |
| 146 | +| Rule | Severity | Description | |
| 147 | +|------|----------|-------------| |
| 148 | +| B001 | WARNING | `INSERT` without column list | |
| 149 | +| B002 | INFO | `SELECT INTO` (non-standard) | |
| 150 | +| B003 | HINT | Multi-table query without aliases | |
| 151 | +| B004 | WARNING | Deeply nested subqueries (depth >= 3) | |
| 152 | +| B005 | INFO | Implicit join (comma-separated `FROM`) | |
| 153 | + |
| 154 | +### Correctness (C001-C003) |
| 155 | + |
| 156 | +| Rule | Severity | Description | |
| 157 | +|------|----------|-------------| |
| 158 | +| C001 | ERROR | `= NULL` instead of `IS NULL` | |
| 159 | +| C002 | WARNING | `SELECT *` with `GROUP BY` | |
| 160 | +| C003 | ERROR | `HAVING` without `GROUP BY` | |
| 161 | + |
| 162 | +### Style (T001-T002) |
| 163 | + |
| 164 | +| Rule | Severity | Description | |
| 165 | +|------|----------|-------------| |
| 166 | +| T001 | HINT | Inconsistent keyword casing | |
| 167 | +| T002 | HINT | Unnecessary parentheses in simple conditions | |
| 168 | + |
| 169 | +--- |
| 170 | + |
| 171 | +## Advanced Usage |
| 172 | + |
| 173 | +### Category Filtering |
| 174 | + |
| 175 | +```python |
| 176 | +from sqlaudit import Analyzer |
| 177 | +from sqlaudit.rules import Category |
| 178 | + |
| 179 | +# Only security checks |
| 180 | +analyzer = Analyzer(enable_categories=[Category.SECURITY]) |
| 181 | +report = analyzer.analyze("SELECT * FROM users") |
| 182 | +``` |
| 183 | + |
| 184 | +### Severity Filtering |
| 185 | + |
| 186 | +```python |
| 187 | +from sqlaudit import Analyzer |
| 188 | +from sqlaudit.rules import Severity |
| 189 | + |
| 190 | +# Only errors and warnings (skip info/hint) |
| 191 | +analyzer = Analyzer(min_severity=Severity.WARNING) |
| 192 | +``` |
| 193 | + |
| 194 | +### Disable Specific Rules |
| 195 | + |
| 196 | +```python |
| 197 | +analyzer = Analyzer(disable_rules=["P001", "P007", "T001"]) |
| 198 | +``` |
| 199 | + |
| 200 | +### Custom Rules |
| 201 | + |
| 202 | +```python |
| 203 | +from sqlaudit import Analyzer, Rule, Severity, Category |
| 204 | + |
| 205 | +def check_table_prefix(info): |
| 206 | + """All tables should use 'app_' prefix.""" |
| 207 | + for table in info.tables: |
| 208 | + if not table.startswith("app_"): |
| 209 | + return f"Table '{table}' missing 'app_' prefix" |
| 210 | + return None |
| 211 | + |
| 212 | +analyzer = Analyzer() |
| 213 | +analyzer.register_rule(Rule( |
| 214 | + id="CUSTOM001", |
| 215 | + name="table-prefix", |
| 216 | + description="Tables must use app_ prefix", |
| 217 | + severity=Severity.WARNING, |
| 218 | + category=Category.BEST_PRACTICE, |
| 219 | + check=check_table_prefix, |
| 220 | + suggestion="Rename table to use app_ prefix", |
| 221 | +)) |
| 222 | + |
| 223 | +report = analyzer.analyze("SELECT * FROM users") |
| 224 | +# Finds: Table 'users' missing 'app_' prefix |
| 225 | +``` |
| 226 | + |
| 227 | +### Output Formats |
| 228 | + |
| 229 | +```bash |
| 230 | +# Human-readable text (default) |
| 231 | +sqlaudit audit -q "SELECT * FROM t" --format text |
| 232 | + |
| 233 | +# JSON for programmatic consumption |
| 234 | +sqlaudit audit -q "SELECT * FROM t" --format json |
| 235 | + |
| 236 | +# CSV for spreadsheet import |
| 237 | +sqlaudit audit -q "SELECT * FROM t" --format csv |
| 238 | + |
| 239 | +# Summary only (score + counts) |
| 240 | +sqlaudit audit -q "SELECT * FROM t" --format summary |
| 241 | +``` |
| 242 | + |
| 243 | +### CI/CD Integration |
| 244 | + |
| 245 | +```yaml |
| 246 | +# GitHub Actions example |
| 247 | +- name: SQL Audit |
| 248 | + run: | |
| 249 | + pip install sqlaudit |
| 250 | + sqlaudit audit -f migrations/*.sql --format json --fail-on warning |
| 251 | +``` |
| 252 | +
|
| 253 | +```bash |
| 254 | +# Pre-commit hook |
| 255 | +#!/bin/bash |
| 256 | +SQL_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sql$') |
| 257 | +if [ -n "$SQL_FILES" ]; then |
| 258 | + sqlaudit audit -f $SQL_FILES --fail-on error --no-color |
| 259 | +fi |
| 260 | +``` |
| 261 | + |
| 262 | +--- |
| 263 | + |
| 264 | +## Scoring |
| 265 | + |
| 266 | +sqlaudit assigns a score from 0-100 based on findings: |
| 267 | + |
| 268 | +| Severity | Point Deduction | |
| 269 | +|----------|----------------| |
| 270 | +| ERROR | -10 per finding | |
| 271 | +| WARNING | -5 per finding | |
| 272 | +| INFO | -2 per finding | |
| 273 | +| HINT | -1 per finding | |
| 274 | + |
| 275 | +A **pass rate** is also computed: percentage of queries with zero errors or warnings. |
| 276 | + |
| 277 | +--- |
| 278 | + |
| 279 | +## Architecture |
| 280 | + |
| 281 | +``` |
| 282 | +sqlaudit/ |
| 283 | + parser.py # SQL tokenizer + query parser (zero dependencies) |
| 284 | + rules.py # Rule engine with 27 built-in rules |
| 285 | + analyzer.py # Main analyzer (parser + rules + report) |
| 286 | + report.py # Report data model (Finding, QueryResult, Report) |
| 287 | + formatter.py # Output formatters (text, JSON, CSV, summary) |
| 288 | + cli.py # Command-line interface |
| 289 | +``` |
| 290 | + |
| 291 | +The architecture is designed for extensibility: |
| 292 | +- **Parser** produces a `QueryInfo` object with structured query metadata |
| 293 | +- **Rules** are pure functions: `(QueryInfo) -> Optional[str]` |
| 294 | +- **Custom rules** can be registered at runtime |
| 295 | +- **Formatters** are pluggable for any output format |
| 296 | + |
| 297 | +--- |
| 298 | + |
| 299 | +## License |
| 300 | + |
| 301 | +MIT |
0 commit comments