Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ reviews:
path_instructions:
- path: "**/*.go"
instructions: >-
Go 1.26; modern idioms expected (range-over-int, `any`, compile-time
Go 1.27; modern idioms expected (range-over-int, `any`, compile-time
interface asserts `var _ I = (*T)(nil)`). Keep the core dependency-light:
analyzer, middleware and reporter must stay free of third-party deps and
of YAML. Flag new external imports in those packages. Watch for
Expand Down
69 changes: 63 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.26"]
go-version: ["1.27"]
steps:
- uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -109,7 +109,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: "1.26"
go-version: "1.27"

- name: Run integration tests
run: make test-integration
Expand Down Expand Up @@ -139,14 +139,71 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: "1.26"
go-version: "1.27"

# Version comes from the Makefile so a local `make lint` and this gate
# run the same linter. Nothing bumps it automatically — Dependabot does
# not track action `with:` inputs — so refresh GOLANGCI_LINT_VERSION
# deliberately.
- name: Resolve golangci-lint version
id: golangci-lint-version
run: echo "version=$(make -s print-golangci-lint-version)" >> "$GITHUB_OUTPUT"

- uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
version: ${{ steps.golangci-lint-version.outputs.version }}
working-directory: ${{ matrix.module }}
args: --timeout=5m

# Not redundant with Dependabot or CodeQL, which cover different things:
# Dependabot reports that a dependency is behind, CodeQL looks for bug
# patterns in our own code. govulncheck cross-references the Go
# vulnerability database against the call graph, so it reports only
# advisories this code actually reaches.
#
# Scanned per module for the same reason `lint` is: the root module's
# ./... stops at nested go.mod boundaries, so the satellites' own
# dependency trees are invisible to a scan run from the root.
vuln:
name: govulncheck (${{ matrix.module }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
module:
- .
- integrations/gormguard
- integrations/sqlxguard
- integrations/pgxguard
- integrations/bunguard
- integrations/xormguard
- integrations/entguard
- parsers/pgparser
- parsers/mysqlparser
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: actions/setup-go@v7
with:
go-version: "1.27"

# Version comes from the Makefile so a local `make vuln` and this gate
# run the same scanner. Nothing bumps it automatically — Dependabot
# does not track `go install` lines — so refresh GOVULNCHECK_VERSION
# deliberately. The advisory database is still fetched at run time, so
# an older scanner still reports current advisories.
- name: Install govulncheck
run: |
version="$(make -s print-govulncheck-version)"
echo "govulncheck $version"
go install "golang.org/x/vuln/cmd/govulncheck@$version"

- name: Scan
working-directory: ${{ matrix.module }}
run: govulncheck ./...

build:
runs-on: ubuntu-latest
steps:
Expand All @@ -156,7 +213,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: "1.26"
go-version: "1.27"

- name: Build CLI
run: go build -o bin/sqlguard ./cmd/sqlguard
Expand All @@ -170,7 +227,7 @@ jobs:

- uses: actions/setup-go@v7
with:
go-version: "1.26"
go-version: "1.27"

# `make coverage` runs every module and merges into a single coverage.out
# (root go test does not reach the satellite modules).
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v7
with:
go-version: "1.26"
go-version: "1.27"

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
Expand Down
44 changes: 44 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
version: "2"

# Report every occurrence. The defaults (max-same-issues: 3) hide duplicates,
# which makes a lint failure look smaller than it is.
issues:
max-issues-per-linter: 0
max-same-issues: 0

linters:
enable:
- errcheck
Expand All @@ -15,10 +21,31 @@ linters:
# Security & correctness (sqlguard is a security-adjacent tool).
- gosec # SAST — SQL injection, unhandled crypto/file ops, etc.
- errorlint # correct error wrapping / errors.Is / errors.As usage
- errname # sentinel errors must be named Err*, error types *Error
- bodyclose # response/rows bodies must be closed
- nilerr # returning nil after a non-nil error check
- nilnesserr # returning a nil error while a wrapped error is still live
# Concurrency and context correctness — the driver chain and QueryTracker
# are concurrent, and every analysis entry point takes a context.Context.
- contextcheck
- fatcontext
- noctx
- durationcheck
- unconvert # remove redundant type conversions
- usestdlibvars # prefer stdlib constants (http.MethodGet, sql.LevelReadOnly…)
# Guard.Check/Observe run on every query through the driver chain and are
# documented as allocation-light — keep the hot path honest.
- perfsprint
- makezero
- wastedassign
- asasalint
- reassign
# Modern Go idioms expected repo-wide (see AGENTS.md): range-over-int,
# no unnecessary loop-var copies now that Go 1.22+ scopes them per-iteration.
- copyloopvar
- intrange
# Keep //nolint directives specific and explained.
- nolintlint
settings:
gocyclo:
min-complexity: 15
Expand All @@ -31,14 +58,31 @@ linters:
# and file/crypto checks (G201/G202/G3xx/G4xx) stay on.
excludes:
- G115
errcheck:
# An unchecked type assertion panics; deliberate `_ =` discards still pass.
check-type-assertions: true
nolintlint:
require-explanation: true
require-specific: true
exclusions:
rules:
- linters:
- errcheck
- gosec # test fixtures use lax file perms / ignore setup errors
path: _test\.go
# Tests may assert on unwrapped errors, build requests/queries without a
# context, and use fmt.Sprintf freely; none of that ships to consumers.
- linters:
- bodyclose
- errorlint
- noctx
- perfsprint
path: _test\.go

formatters:
enable:
- gofmt
- goimports
settings:
gofmt:
simplify: true
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ or `parsers/*`, which are separate Go modules. The `MODULES` variable drives the
loop so a target can't silently skip a satellite.

- `make all` — `tidy fmt vet lint build test` across all modules.
- `make ci` — the CI pipeline: `fmt-check vet lint test-race`.
- `make ci` — the CI pipeline: `fmt-check vet lint vuln test-race`.
- `make build` — `go build ./...` in every module (compile check). `make cli` builds the `bin/sqlguard` binary; `make install` installs it.
- `make test` — `go test -count=1 ./...` in every module. `make test-race` adds `-race`; `make coverage` writes a merged `coverage.out`.
- `make lint` — `golangci-lint run` in every module (config in `.golangci.yml`, v2 schema). `make fmt` / `make fmt-check` run `gofmt -s` + `goimports`.
- `make tidy` — `go mod tidy` across all nine modules. Run after any dependency change; tidying only the root leaves the others stale.
- `make setup` — installs pinned `golangci-lint` / `goimports` if missing (a prereq of `lint`/`fmt`).
- `make tidy` — `go mod tidy` across all nine modules. Run after any dependency change; tidying only the root leaves the others stale. `make tidy-check` fails (without leaving the change behind) if any module's go.mod/go.sum is stale — CI hygiene, not part of `all`/`ci`.
- `make vuln` — `govulncheck` in every module, filtered to advisories the code actually reaches. Needs network access (fetches the advisory database each run).
- `make setup` — installs pinned `golangci-lint` / `goimports` / `govulncheck` if missing (a prereq of `lint`/`fmt`/`vuln`). `make print-golangci-lint-version` / `make print-govulncheck-version` print the pinned versions so CI resolves them from here instead of a second hardcoded copy.
- The committed `go.work` makes every satellite compile against this tree, not the published core it `require`s — so a breaking change to `analyzer/`/`middleware/` fails their tests. No `go.mod` here has a `replace`. Use `GOWORK=off` to see a consumer's build. Releasing is manual (see CONTRIBUTING.md).
- `make db-up` / `make test-integration` / `make db-down` — run `explain/` against live Postgres, MySQL and MariaDB (`test/integration/`, behind the `integration` build tag).

Expand All @@ -27,7 +28,7 @@ Run a single test: `go test ./middleware/ -run TestDriver_QueryDetectsSelectStar

## Module topology

Nine Go modules, all on **Go 1.26**, kept in lockstep:
Nine Go modules, all on **Go 1.27**, kept in lockstep:

- root (`github.com/KARTIKrocks/sqlguard`) — core analyzer, middleware, reporter, `config`, CLI. Near-zero-dependency: `analyzer`/`middleware`/`reporter` stay dependency-free; the only third-party deps are sqlite3 (CLI `db`/tests), cobra (CLI), and `gopkg.in/yaml.v3` (isolated to the `config` package). Importing `analyzer`/`middleware` does not pull YAML.
- `parsers/pgparser`, `parsers/mysqlparser` — opt-in real SQL grammars, isolated in their own modules so the heavy parser deps never enter a consumer's build unless explicitly imported.
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ project-specific things that aren't obvious from a quick look at the repo.

## Project layout

sqlguard is a **multi-module repo** — nine Go modules on Go 1.26, kept in
sqlguard is a **multi-module repo** — nine Go modules on Go 1.27, kept in
lockstep:

- **root** (`github.com/KARTIKrocks/sqlguard`) — core analyzer, middleware,
Expand All @@ -32,9 +32,9 @@ depends on these modules. To reproduce a consumer's build, set `GOWORK=off`.
## Development workflow

```bash
make setup # install pinned golangci-lint + goimports (one-time)
make setup # install pinned golangci-lint + goimports + govulncheck (one-time)
make all # tidy, fmt, vet, lint, build, test across all nine modules
make ci # what CI runs: fmt-check, vet, lint, test-race
make ci # what CI runs: fmt-check, vet, lint, vuln, test-race
make test-race # race detector (required for anything touching middleware)
make help # list every target
```
Expand Down
56 changes: 52 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
GOLANGCI_LINT_VERSION := v2.12.2
GOLANGCI_LINT_VERSION := v2.13.2
GOIMPORTS_VERSION := v0.45.0
GOVULNCHECK_VERSION := v1.8.0

# Sub-modules carry their own go.mod (heavy/opt-in deps kept out of the core
# import graph). `go test ./...` from root does NOT reach them, so every
Expand Down Expand Up @@ -28,7 +29,7 @@ SQLGUARD_TEST_PG_DSN ?= postgres://sqlguard:sqlguard@localhost:55432/sqlguard?ss
SQLGUARD_TEST_MYSQL_DSN ?= root:sqlguard@tcp(localhost:53306)/sqlguard
SQLGUARD_TEST_MARIADB_DSN ?= root:sqlguard@tcp(localhost:53307)/sqlguard

.PHONY: all help setup deps ci test test-v test-race coverage lint lint-fix fix fmt fmt-check vet tidy build cli install bench clean db-up db-down test-integration vet-integration
.PHONY: all help setup deps ci test test-v test-race coverage lint lint-fix fix fmt fmt-check vet tidy tidy-check build cli install bench clean db-up db-down test-integration vet-integration vuln print-golangci-lint-version print-govulncheck-version

all: tidy fmt vet lint build test

Expand All @@ -53,6 +54,8 @@ help:
@echo " fmt - Format code (gofmt -s + goimports)"
@echo " fmt-check - Verify formatting without modifying files"
@echo " tidy - Run go mod tidy (all modules)"
@echo " tidy-check - Fail if go.mod/go.sum are not tidy (all modules)"
@echo " vuln - Run govulncheck (all modules)"
@echo " build - Build all packages (all modules)"
@echo " cli - Build the sqlguard CLI to bin/sqlguard"
@echo " install - Install the CLI to \$$GOPATH/bin"
Expand All @@ -69,6 +72,10 @@ setup:
echo "Installing goimports $(GOIMPORTS_VERSION)..."; \
go install golang.org/x/tools/cmd/goimports@$(GOIMPORTS_VERSION); \
}
@command -v govulncheck >/dev/null 2>&1 || { \
echo "Installing govulncheck $(GOVULNCHECK_VERSION)..."; \
go install golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION); \
}

## Download module dependencies across all modules
deps:
Expand All @@ -77,8 +84,8 @@ deps:
(cd $$mod && go mod download) || exit 1; \
done

## CI: run formatting check, vet, lint and tests with race detector
ci: fmt-check vet lint test-race
## CI: run formatting check, vet, lint, vulnerability scan and tests with race detector
ci: fmt-check vet lint vuln test-race

## Build all packages across all modules (compile check)
build:
Expand Down Expand Up @@ -189,6 +196,47 @@ tidy:
(cd $$mod && go mod tidy) || exit 1; \
done

## Fail if any go.mod/go.sum is not tidy, without leaving the change behind.
## Suitable for CI, where a stale go.sum should block the merge.
tidy-check:
@status=$$(git status --porcelain -- $(foreach mod,$(MODULES),$(mod)/go.mod $(mod)/go.sum)); \
if [ -n "$$status" ]; then \
echo "go.mod/go.sum already modified; commit or stash before running tidy-check"; \
exit 1; \
fi
@$(MAKE) --no-print-directory tidy
@if ! git diff --quiet -- '*go.mod' '*go.sum'; then \
echo "go.mod/go.sum are not tidy — run 'make tidy' and commit:"; \
git diff --stat -- '*go.mod' '*go.sum'; \
git checkout -- '*go.mod' '*go.sum'; \
exit 1; \
fi
@echo "all modules tidy"

## Scan every module for known vulnerabilities, filtered to advisories the code
## actually reaches. Needs network access — the advisory database is fetched on
## every run. Note this also scans the standard library of whichever Go
## toolchain you have installed, so it can fail locally on a green branch when
## your Go is a patch release behind the one CI pins — that is a real finding
## about your machine, not a false positive.
vuln: setup
@for mod in $(MODULES); do \
echo "==> Scanning $$mod"; \
(cd $$mod && govulncheck ./...) || exit 1; \
done

## Print the pinned linter version. CI resolves golangci-lint-action's version
## input from this rather than hardcoding a second copy of the number, so the
## workflow and this file cannot drift apart.
print-golangci-lint-version:
@echo $(GOLANGCI_LINT_VERSION)

## Print the pinned scanner version. CI installs govulncheck with this rather
## than hardcoding a second copy of the number, so the workflow and this file
## cannot drift apart.
print-govulncheck-version:
@echo $(GOVULNCHECK_VERSION)

## Run benchmarks across all modules
bench:
@for mod in $(MODULES); do \
Expand Down
2 changes: 1 addition & 1 deletion analyzer/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func hasUnsafeAddNotNull(sanitized string) bool {
func splitTopLevelCommas(s string) []string {
var segs []string
depth, start := 0, 0
for i := 0; i < len(s); i++ {
for i := range len(s) {
switch s[i] {
case '(':
depth++
Expand Down
5 changes: 3 additions & 2 deletions cmd/sqlguard/db.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package main

import (
"context"
"database/sql"
"fmt"
)

// openDB opens a database connection using the appropriate driver.
func openDB(dialect, dsn string) (*sql.DB, error) {
func openDB(ctx context.Context, dialect, dsn string) (*sql.DB, error) {
var driverName string
switch dialect {
case "postgres":
Expand All @@ -22,7 +23,7 @@ func openDB(dialect, dsn string) (*sql.DB, error) {
return nil, err
}

if err := db.Ping(); err != nil {
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return nil, fmt.Errorf("cannot reach database: %w", err)
}
Expand Down
Loading
Loading