Repository navigation
fix: security and connection-lifecycle hardening - #11
Merged
Merged
Conversation
Schema, table and index names reached SQL text unquoted or with quotes that were never doubled. VACUUM of a table whose name contains a double quote failed outright, and generate-drop-sql emitted SQL that errors when pasted into psql. Route every identifier through quoteIdentifier. --ssl passed rejectUnauthorized: false, so it encrypted without authenticating the server. It now verifies by default, with --ssl-ca and an explicit --ssl-no-verify opt-out. run-vacuum and the pg_stat_statements DDL commands ran immediately from the CLI while the interactive menus prompted first. They now require --yes, and --dry-run reports what they would do. Also: add a pool error listener so an idle-client failure no longer kills watch mode, reject unknown commands instead of silently running a full analysis, handle SIGTERM and a second SIGINT in the watch loop, clear its pending countdown timer, and set process.exitCode rather than calling process.exit, which could truncate piped --json output.
Move the command and destructive-command checks out of executeCommand into src/cli/validate.ts, called from main before the pool is created, so an invalid command fails immediately instead of after a connection attempt.
The README documented `new DatabaseAnalyzer({...})` with
`.analyze()/.printSummary()/.generateReport()/.close()`. No such symbol
existed anywhere in the codebase, and the package exported nothing at all:
index.ts ended in a bare main(), so requiring the package connected to
Postgres and ran an analysis as a side effect of the import, while the
generated .d.ts was empty.
The CLI moves to src/cli/main.ts (the bin target) and src/index.ts becomes a
side-effect-free library entry exporting a real PostgresAnalyzer plus the
analyzers, collectors, reporters and report types. Adds an `exports` map and
`sideEffects: false`.
PostgresAnalyzer keeps `vacuum()` separate from `analyze()`, since it changes
server state, and can wrap a pool the caller already has, in which case
`close()` leaves it alone. Its SSL options mirror the CLI's: verification on by
default, with `sslCa` and an explicit `sslRejectUnauthorized: false` opt-out.
Also corrects the architecture tree, which still listed `src/interactive.ts`
and omitted `src/cli/`, `src/config/`, `src/watch/` and `src/utils/`.
…rough The repo had no tests at all — no tests/, no runner, no test script, and no CI step. tsconfig even excluded **/*.test.ts for tests that did not exist. Adds 40 tests covering the CLI parser, the config loader and profiles, SQL identifier quoting, the health score, the report writers' content rather than just their file extension, and the programmatic API. Includes a regression test that a hostile table name cannot reach the generated HTML unescaped. Bugs the tests found: - `--watch -1` was mistaken for a flag and silently replaced by the default interval instead of being rejected. The parser now distinguishes a negative number from a flag. - Numeric flags ran through Number.parseInt with no check, so `--port abc` produced NaN and a driver error far from the actual mistake. Each flag now reports its own name. - A flag at the end of argv stored undefined rather than failing. - An unknown flag was ignored, so a typo like `--jsno` silently produced human-readable output instead of JSON. Tooling: enable noUncheckedIndexedAccess — the missing flag that made `argv[++index]` look like a `string` when it is `string | undefined`, which is the root cause of the last two — plus noImplicitOverride, noFallthroughCasesInSwitch, noUnusedLocals and noUnusedParameters, and fix what they found. Biome's noUnusedVariables and noUnusedImports were warnings, so `biome check` exited 0 on dead code; both are now errors, and the lint scope covers tests/ and scripts/, which were unchecked.
CI only ran on pushes to main and develop, so a feature branch was checked by nothing until it became a PR. It now runs on every branch, cancels superseded runs, tests on Node 24 as well as 20 and 22, and runs `pnpm test` — which the mongo and postgres workflows never did because there were no tests. publish.yml had no `id-token: write`, so releases carried no npm provenance, and nothing verified that the tag matched package.json — a mismatched tag would have published the wrong version. It now checks that first, lints, builds and tests before publishing, publishes with `--provenance`, and reads the release notes from CHANGELOG.md with awk taking the version from the environment rather than interpolating it into the script. The job is gated on an `npm` environment so an irreversible publish can require approval. Adds the missing SECURITY.md — with the operational notes this tool needs, since it takes database credentials and can change server state — plus CODE_OF_CONDUCT.md and CODEOWNERS. Bumps the version to 1.1.0 with a changelog entry.
It needs the repository's Dependency graph enabled, which is a settings change rather than something the workflow can do, so the job failed with "Dependency review is not supported on this repository". Removed so CI is green; worth adding back once Dependency graph and Dependabot security updates are switched on in repository settings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit pass on the PostgreSQL analyzer ahead of treating it as production-grade.
Security
SQL identifiers reached SQL text unquoted or half-quoted.
vacuumTablebuilt"${schema}"."${table}"without doubling embedded quotes, andgenerate-drop-sqlemittedDROP INDEX ${schema}.${index};into a blob the README tells you to paste into psql. Everything now goes throughquoteIdentifier.Verified against a real Postgres with a table named
evil" ; CREATE TABLE pwned_marker(x int); --: the old code failed the vacuum outright, and its generated drop SQL errors when pasted. The new code vacuums it correctly and emits SQL that drops exactly the intended index. (Postgres itself blocks the DDL-injection payload on the VACUUM path, since a multi-statement query runs in an implicit transaction and VACUUM refuses to — so this is a correctness fix that also closes the injection class, not a live RCE.)--ssldisabled certificate verification. It passedrejectUnauthorized: false, so it encrypted without authenticating the server — full MITM exposure for the remote-production caseanalyzerrc.example.jsonadvertises, with no way to supply a CA. It now verifies by default, with--ssl-ca <path>and an explicit--ssl-no-verifyopt-out.Destructive commands ran unconfirmed from the CLI.
-c run-vacuumimmediately vacuumed every bloated table and thepg_stat_statementscommands ran DDL, while the interactive menus prompted first. They now require--yes, and--dry-runreports what they would do.Robustness
pg.Poolemitserroron idle-client failure; with no listener Node treated it as an unhandled error event and killed the process. Near-certain in--watchmode, which holds the pool open for hours.default: buildFullReport(...), so-c helthsilently ran the most expensive path. Now rejected — and validated before the connection opens, so a typo fails in 0.3s instead of after a connection timeout.docker stopreaches connection cleanup, and the pending countdown timer is cleared.process.exit(1)in the top-level catch could truncate buffered stdout, which matters when--jsonis piped to a file. Usesprocess.exitCode.Verification
pnpm lint,pnpm build, and the--helpsmoke test pass. All behaviour above was exercised end to end against apostgres:17-alpinecontainer, including the hostile-identifier case.Tests, the programmatic API, and the CI/tooling changes follow in separate PRs.
Update: this branch now also contains
A real programmatic API. The README documented
new DatabaseAnalyzer({...})with.analyze()/.close(), a symbol that existed nowhere. The package exported nothing at all —index.tsended in a baremain(), sorequire()ing the package connected to the database and ran an analysis as an import side effect, and the generated.d.tswas empty. The CLI moved tosrc/cli/main.tsandsrc/index.tsis now a side-effect-free library entry exportingPostgresAnalyzerplus the analyzers, collectors, reporters and report types.A test suite (40 tests), and the tooling settings that let the bugs through.
noUncheckedIndexedAccesswas off, which is whyargv[++index]looked like astringwhen it isstring | undefined— the root cause of two argument-parsing bugs the new tests found:--watch -1was mistaken for a flag and silently replaced by the default interval.--port abcproducedNaNand a driver error far from the mistake.undefinedrather than failing.--jsnosilently produced human output instead of JSON.Biome's
noUnusedVariables/noUnusedImportswere warnings, sobiome checkexited 0 on dead code; both are errors now, and the lint scope coverstests/andscripts/, which were unchecked.CI and release. CI only ran on
mainanddevelop, so a feature branch was checked by nothing until it became a PR. It now runs on every branch, cancels superseded runs, adds Node 24, and runspnpm test.publish.ymlhad noid-token: write— so releases carried no npm provenance — and nothing verified the tag matchedpackage.json. Both fixed, plus annpmenvironment gate on the irreversible publish.Adds the missing
SECURITY.mdandCODE_OF_CONDUCT.md, and bumps to 1.1.0 with a changelog entry.Verification
pnpm lint,pnpm build,pnpm testand the--helpsmoke test pass on Node 20, 22 and 24. The CLI and the new API were both exercised end to end against a real container.Needs you
Enable Dependency graph in repository settings if you want a
dependency-reviewjob on PRs — I removed the one I had added, because without that setting it fails with "not supported on this repository". Publishing1.1.0to npm is yours; the publish workflow now waits on thenpmenvironment.