diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c8941ad..4d20da3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,9 +5,14 @@ on: tags: - 'v*' # Run on any tag that starts with v (e.g., v1.0.0) +permissions: + contents: read + jobs: publish: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 816c480..d7d7a64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ main ] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md b/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md index 9d5eb0e..b0ad86b 100644 --- a/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md +++ b/docs/blog/posts/mongodb-atlas-cloud-deployment-management.md @@ -1339,7 +1339,7 @@ QueryLeaf provides seamless integration with MongoDB Atlas through familiar SQL -- QueryLeaf Atlas connection and management -- Connect to Atlas cluster with connection string CONNECT TO atlas_cluster WITH ( - connection_string = 'mongodb+srv://username:password@cluster.mongodb.net/database', + connection_string = 'mongodb+srv://:@cluster.mongodb.net/database', read_preference = 'secondaryPreferred', write_concern = 'majority', max_pool_size = 50, diff --git a/docs/blog/posts/mongodb-gridfs-file-management-sql.md b/docs/blog/posts/mongodb-gridfs-file-management-sql.md index da0e8c4..2f4c712 100644 --- a/docs/blog/posts/mongodb-gridfs-file-management-sql.md +++ b/docs/blog/posts/mongodb-gridfs-file-management-sql.md @@ -262,7 +262,7 @@ const fs = require('fs'); const crypto = require('crypto'); const path = require('path'); -const client = new MongoClient('mongodb+srv://username:password@cluster.mongodb.net'); +const client = new MongoClient(process.env.MONGODB_URI); const db = client.db('file_storage_platform'); // Advanced GridFS file management system diff --git a/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md b/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md index 436bdf8..f6439fd 100644 --- a/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md +++ b/docs/blog/posts/mongodb-vector-search-ai-applications-semantic-similarity.md @@ -284,7 +284,7 @@ const { MongoClient } = require('mongodb'); const { OpenAI } = require('openai'); const tf = require('@tensorflow/tfjs-node'); -const client = new MongoClient('mongodb+srv://username:password@cluster.mongodb.net'); +const client = new MongoClient(process.env.MONGODB_URI); const db = client.db('advanced_ai_search_platform'); // Advanced AI-powered search and recommendation engine diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 7929976..db2b854 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -125,5 +125,7 @@ export { DummyMongoClient, }; +export { redactSql } from './redact'; + // Re-export interfaces export * from './interfaces'; diff --git a/packages/lib/src/parser.ts b/packages/lib/src/parser.ts index 37a560d..9c19482 100644 --- a/packages/lib/src/parser.ts +++ b/packages/lib/src/parser.ts @@ -1,8 +1,14 @@ import { From, Parser as NodeSqlParser } from 'node-sql-parser'; import { SqlParser, SqlStatement } from './interfaces'; +import { redactSql } from './redact'; import debug from 'debug'; -const log = debug('queryleaf:parser'); +const rawLog = debug('queryleaf:parser'); + +function log(message: string, ...args: unknown[]): void { + const safeArgs = args.map((arg) => (typeof arg === 'string' ? redactSql(arg) : arg)); + rawLog(message, ...safeArgs); +} // Custom PostgreSQL mode with extensions to support our syntax needs const CUSTOM_DIALECT = { diff --git a/packages/lib/src/redact.ts b/packages/lib/src/redact.ts new file mode 100644 index 0000000..2fca9b3 --- /dev/null +++ b/packages/lib/src/redact.ts @@ -0,0 +1,10 @@ +// SQL fed to the parser or the postgres protocol handler may carry literal +// credentials (CREATE USER ... PASSWORD '...', ALTER USER ... IDENTIFIED BY ...). +// Mask them before they reach debug logs. +export function redactSql(sql: string | undefined): string { + if (!sql) return ''; + return sql.replace( + /\b(PASSWORD|IDENTIFIED\s+BY|IDENTIFIED\s+WITH\s+\S+\s+AS)\s+('([^']|'')*'|"([^"]|"")*"|\S+)/gi, + '$1 ***' + ); +} diff --git a/packages/postgres-server/src/protocol-handler.ts b/packages/postgres-server/src/protocol-handler.ts index db75d03..8e98d44 100644 --- a/packages/postgres-server/src/protocol-handler.ts +++ b/packages/postgres-server/src/protocol-handler.ts @@ -1,11 +1,26 @@ import { Socket } from 'net'; -import { QueryLeaf } from '@queryleaf/lib'; +import { QueryLeaf, redactSql } from '@queryleaf/lib'; import { Transform } from 'stream'; +import { randomInt } from 'crypto'; import debugLib from 'debug'; import { MongoClient, Document } from 'mongodb'; const debug = debugLib('queryleaf:pg-server:protocol'); +// Strip password / query payloads from a parsed client message before logging. +function redactMessage(message: { type?: string; string?: string; query?: string }): object { + const { string: _str, query, ...rest } = message; + const safe: Record = { ...rest }; + if (message.type === 'password') { + safe.string = '***'; + } else if (message.type === 'query' || message.type === 'parse') { + safe.query = redactSql(query ?? message.string); + } else if (message.string !== undefined) { + safe.string = message.string; + } + return safe; +} + // Simplified protocol implementation for demo purposes interface BackendMessage { // Buffer containing the formatted message ready to send @@ -564,7 +579,7 @@ export class ProtocolHandler { this.buffer = this.buffer.subarray(message.length); debug(`Message processed, remaining buffer length: ${this.buffer.length}`); - debug('Received message type:', messageType, message); + debug('Received message type:', messageType, redactMessage(message)); // Handle the message this.handleMessage(message.type as MessageName, message); @@ -634,7 +649,7 @@ export class ProtocolHandler { * Handle a startup message */ private handleStartup(message: ClientMessage): void { - debug('Startup message:', message); + debug('Startup message received'); // Extract user and database from parameters if (message.parameters) { @@ -795,7 +810,7 @@ export class ProtocolHandler { * Handle a query message */ private async handleQuery(queryString: string): Promise { - debug('Query message:', queryString); + debug('Query message:', redactSql(queryString)); if (!this.authenticated) { debug('Not authenticated, rejecting query'); @@ -912,7 +927,7 @@ export class ProtocolHandler { private handleParse(message: ClientMessage): void { const { name, query } = message; - debug('Parse message:', name, query); + debug('Parse message:', name, redactSql(query)); try { // Store the prepared statement for later @@ -931,7 +946,7 @@ export class ProtocolHandler { * Handle a bind message */ private handleBind(message: ClientMessage): void { - debug('Bind message:', message); + debug('Bind message:', redactMessage(message)); // In a real implementation, you would bind parameters to a prepared statement // For now, just acknowledge the bind @@ -942,7 +957,7 @@ export class ProtocolHandler { * Handle a describe message */ private handleDescribe(message: ClientMessage): void { - debug('Describe message:', message); + debug('Describe message:', redactMessage(message)); const type = message.string; const name = message.name; @@ -969,7 +984,7 @@ export class ProtocolHandler { * Handle an execute message */ private async handleExecute(message: ClientMessage): Promise { - debug('Execute message:', message); + debug('Execute message:', redactMessage(message)); const { portal, maxRows } = message; @@ -1060,9 +1075,9 @@ export class ProtocolHandler { * Send backend key data */ private sendBackendKeyData(): void { - // Generate random process ID and key - const processId = Math.floor(Math.random() * 10000); - const secretKey = Math.floor(Math.random() * 1000000); + // The secret key authenticates CancelRequest messages, so it must be unguessable. + const processId = randomInt(1, 0x7fffffff); + const secretKey = randomInt(1, 0x7fffffff); this.sendMessage(this.serializer.backendKeyData(processId, secretKey)); } diff --git a/packages/postgres-server/tests/integration/minimal-integration.test.ts b/packages/postgres-server/tests/integration/minimal-integration.test.ts index a0da51b..2b3d200 100644 --- a/packages/postgres-server/tests/integration/minimal-integration.test.ts +++ b/packages/postgres-server/tests/integration/minimal-integration.test.ts @@ -13,7 +13,7 @@ class MockQueryLeaf { constructor(public client: any, public dbName: string) {} execute(query: string) { - log(`Mock executing query: ${query}`); + log(`Mock executing query of length ${query.length}`); if (query.includes('users')) { return [ { name: 'John', age: 30 }, diff --git a/packages/postgres-server/tests/integration/minimal.integration.test.ts b/packages/postgres-server/tests/integration/minimal.integration.test.ts index 49b8268..a4058f9 100644 --- a/packages/postgres-server/tests/integration/minimal.integration.test.ts +++ b/packages/postgres-server/tests/integration/minimal.integration.test.ts @@ -14,7 +14,7 @@ class MockQueryLeaf { constructor(public client: any, public dbName: string) {} execute(query: string): any[] { - log(`Mock executing query: ${query}`); + log(`Mock executing query of length ${query.length}`); if (query.includes('test')) { return [{ test: 'success' }]; } diff --git a/packages/postgres-server/tests/unit/basic.test.ts b/packages/postgres-server/tests/unit/basic.test.ts index 2e1731c..7e9205c 100644 --- a/packages/postgres-server/tests/unit/basic.test.ts +++ b/packages/postgres-server/tests/unit/basic.test.ts @@ -4,10 +4,12 @@ import { Socket } from 'net'; // Mock the QueryLeaf import jest.mock('@queryleaf/lib', () => { + const actual = jest.requireActual('@queryleaf/lib'); return { + ...actual, QueryLeaf: class MockQueryLeaf { constructor(public client: any, public dbName: string) {} - + async execute(sql: string): Promise { if (sql === 'SELECT * FROM users') { return [