From ea5a61a6e5c17ff61c521bcaff1b7965974d2b42 Mon Sep 17 00:00:00 2001 From: moksh-solankipy <46241527+moksh-solankipy@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:48:56 +0530 Subject: [PATCH 1/9] feat: add open-source community standards and PII redaction mode --- CODE_OF_CONDUCT.md | 18 ++++++++++++++++++ CONTRIBUTING.md | 17 +++++++++++++++++ SECURITY.md | 15 +++++++++++++++ cmd/sqlens/main.go | 2 +- config/config.go | 30 ++++++++++++++++++++---------- proxy/server.go | 26 +++++++++++++++++--------- 6 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9d72492 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,18 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d97ca88 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing to SQLens + +Thank you for your interest in SQLens! We welcome contributions from the community. + +## How to Contribute + +1. **Fork the repository.** +2. **Create a branch** for your feature or bug fix: `git checkout -b my-new-feature` +3. **Make your changes.** Ensure code follows standard Go formatting (`go fmt`). +4. **Run tests**: `make test` +5. **Commit your changes**: `git commit -m "feat: my new feature"` +6. **Push to your branch** and open a **Pull Request**. + +## Guidelines +- All new features must include unit tests. +- Keep Pull Requests focused on a single change. +- Follow the [Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..bdcccf4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Reporting a Vulnerability + +We take the security of SQLens seriously. If you believe you have found a security vulnerability, please report it to us by following these steps: + +1. **Do not open a GitHub issue.** +2. Email your report to: `security@example.com` (Placeholder) +3. Include as much detail as possible: steps to reproduce, impact, and a proof of concept if available. + +We will acknowledge your report within 48 hours and keep you updated on our progress. + +## Redact Mode + +SQLens includes a `RedactSensitive` mode (set via `SQLENS_REDACT_SENSITIVE=true`). When enabled, the proxy will mask all literals in SQL queries before they reach the dashboard or logs. We recommend enabling this in production environments where queries may contain PII. diff --git a/cmd/sqlens/main.go b/cmd/sqlens/main.go index a1047e0..84eb87c 100644 --- a/cmd/sqlens/main.go +++ b/cmd/sqlens/main.go @@ -31,7 +31,7 @@ func main() { ) // Initialize TCP Proxy - proxyServer := proxy.NewServer(cfg.ListenAddr, cfg.TargetAddr, pipeline, memStore) + proxyServer := proxy.NewServer(cfg.ListenAddr, cfg.TargetAddr, pipeline, memStore, cfg.RedactSensitive) // Initialize Web Dashboard webServer := web.NewServer(":8080", memStore) diff --git a/config/config.go b/config/config.go index d6a7bc5..1770fef 100644 --- a/config/config.go +++ b/config/config.go @@ -6,23 +6,33 @@ import ( ) type Config struct { - ListenAddr string - TargetAddr string - SlowQueryMs int - N1WindowSecs int - N1Threshold int + ListenAddr string + TargetAddr string + SlowQueryMs int + N1WindowSecs int + N1Threshold int + RedactSensitive bool } func LoadConfig() Config { return Config{ - ListenAddr: getEnv("SQLENS_LISTEN_ADDR", ":5433"), - TargetAddr: getEnv("SQLENS_TARGET_ADDR", "localhost:5432"), - SlowQueryMs: getEnvAsInt("SQLENS_SLOW_QUERY_MS", 100), - N1WindowSecs: getEnvAsInt("SQLENS_N1_WINDOW_SECS", 10), - N1Threshold: getEnvAsInt("SQLENS_N1_THRESHOLD", 5), + ListenAddr: getEnv("SQLENS_LISTEN_ADDR", ":5433"), + TargetAddr: getEnv("SQLENS_TARGET_ADDR", "localhost:5432"), + SlowQueryMs: getEnvAsInt("SQLENS_SLOW_QUERY_MS", 100), + N1WindowSecs: getEnvAsInt("SQLENS_N1_WINDOW_SECS", 10), + N1Threshold: getEnvAsInt("SQLENS_N1_THRESHOLD", 5), + RedactSensitive: getEnvAsBool("SQLENS_REDACT_SENSITIVE", false), } } +func getEnvAsBool(key string, fallback bool) bool { + strValue := getEnv(key, "") + if value, err := strconv.ParseBool(strValue); err == nil { + return value + } + return fallback +} + func getEnv(key, fallback string) string { if value, exists := os.LookupEnv(key); exists { return value diff --git a/proxy/server.go b/proxy/server.go index 1064c2b..61f04cb 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -11,10 +11,11 @@ import ( ) type Server struct { - listenAddr string - targetAddr string - pipeline *analyzer.Pipeline - store EventStore + listenAddr string + targetAddr string + pipeline *analyzer.Pipeline + store EventStore + redactSensitive bool } // EventStore represents an interface to save and retrieve query events @@ -22,12 +23,13 @@ type EventStore interface { Save(event analyzer.QueryEvent) } -func NewServer(listen, target string, p *analyzer.Pipeline, s EventStore) *Server { +func NewServer(listen, target string, p *analyzer.Pipeline, s EventStore, redact bool) *Server { return &Server{ - listenAddr: listen, - targetAddr: target, - pipeline: p, - store: s, + listenAddr: listen, + targetAddr: target, + pipeline: p, + store: s, + redactSensitive: redact, } } @@ -153,6 +155,12 @@ func (s *Server) handleConnection(ctx context.Context, clientConn net.Conn) { Latency: l, } s.pipeline.Process(context.Background(), event) + + // If redaction is enabled, we use the fingerprint instead of raw SQL + if s.redactSensitive { + event.RawQuery = event.Fingerprint + } + s.store.Save(event) }(query, latency) } else { From b3fb9c84ba399a9a535a38dada69c0122ffb7f68 Mon Sep 17 00:00:00 2001 From: Moksh Solanki <46241527+mx-gp@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:51:04 +0530 Subject: [PATCH 2/9] updated --- SECURITY.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index bdcccf4..248a040 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,9 +4,8 @@ We take the security of SQLens seriously. If you believe you have found a security vulnerability, please report it to us by following these steps: -1. **Do not open a GitHub issue.** -2. Email your report to: `security@example.com` (Placeholder) -3. Include as much detail as possible: steps to reproduce, impact, and a proof of concept if available. +1. open a GitHub issue. +2. Include as much detail as possible: steps to reproduce, impact, and a proof of concept if available. We will acknowledge your report within 48 hours and keep you updated on our progress. From 7cdfb81ee402f57e3a6634e6a7656a5ba3f713a2 Mon Sep 17 00:00:00 2001 From: moksh-solankipy <46241527+moksh-solankipy@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:53:15 +0530 Subject: [PATCH 3/9] feat: add real-time SQL Performance Guardrails (antipattern detection) --- analyzer/analyzer.go | 1 + analyzer/guardrail.go | 34 ++++++++++++++++++++++++++++++++++ benchmark.go | 12 +++++++++++- cmd/sqlens/main.go | 1 + web/server.go | 1 + 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 analyzer/guardrail.go diff --git a/analyzer/analyzer.go b/analyzer/analyzer.go index fea1d3e..5a965b0 100644 --- a/analyzer/analyzer.go +++ b/analyzer/analyzer.go @@ -12,6 +12,7 @@ type QueryEvent struct { Timestamp time.Time Fingerprint string N1Flag bool + Violations []string // New: Performance/Safety guardrail warnings } type AnalysisResult struct { diff --git a/analyzer/guardrail.go b/analyzer/guardrail.go new file mode 100644 index 0000000..a8e6eb4 --- /dev/null +++ b/analyzer/guardrail.go @@ -0,0 +1,34 @@ +package analyzer + +import ( + "context" + "strings" +) + +type GuardrailAnalyzer struct{} + +func NewGuardrailAnalyzer() *GuardrailAnalyzer { + return &GuardrailAnalyzer{} +} + +func (g *GuardrailAnalyzer) Analyze(ctx context.Context, event QueryEvent) (*AnalysisResult, error) { + upperQuery := strings.ToUpper(event.RawQuery) + + // 🚨 Guardrail 1: DELETE/UPDATE without WHERE (Dangerous!) + if (strings.Contains(upperQuery, "DELETE") || strings.Contains(upperQuery, "UPDATE")) && + !strings.Contains(upperQuery, "WHERE") { + event.Violations = append(event.Violations, "āš ļø DANGEROUS: Modification query without WHERE clause detected!") + } + + // 🐌 Guardrail 2: SELECT * (Performance Antipattern) + if strings.Contains(upperQuery, "SELECT *") { + event.Violations = append(event.Violations, "🐌 SLOW: SELECT * usage (fetch only needed columns)") + } + + // šŸ›‘ Guardrail 3: SELECT without LIMIT (Scale Risk) + if strings.Contains(upperQuery, "SELECT") && !strings.Contains(upperQuery, "LIMIT") { + event.Violations = append(event.Violations, "šŸ“¦ UNBOUNDED: SELECT without LIMIT (potential large result set)") + } + + return &AnalysisResult{Event: event}, nil +} diff --git a/benchmark.go b/benchmark.go index ba70f05..33581de 100644 --- a/benchmark.go +++ b/benchmark.go @@ -90,7 +90,17 @@ func main() { for i := 0; i < 50; i++ { _, _ = db.Exec("UPDATE users SET created_at = NOW() WHERE id = $1", (i%20)+1) } - fmt.Println("Finished 50 quick updates.") + + // 5. Performance Guardrails Test + fmt.Println("\n--- šŸ›”ļø Scenario: Performance Guardrails ---") + fmt.Println("Triggering 'SELECT *' warning...") + _, _ = db.Exec("SELECT * FROM users") + + fmt.Println("Triggering 'Missing WHERE' warning...") + _, _ = db.Exec("UPDATE users SET name = 'Ghost'") + + fmt.Println("Triggering 'Missing LIMIT' warning...") + _, _ = db.Exec("SELECT name FROM users") fmt.Println("\nāœ… Benchmark Complete!") fmt.Println("Check the SQLens Dashboard (http://localhost:8080) for N+1 alerts and latency maps.") diff --git a/cmd/sqlens/main.go b/cmd/sqlens/main.go index 84eb87c..5c5d97b 100644 --- a/cmd/sqlens/main.go +++ b/cmd/sqlens/main.go @@ -28,6 +28,7 @@ func main() { time.Duration(cfg.N1WindowSecs)*time.Second, cfg.N1Threshold, ), + analyzer.NewGuardrailAnalyzer(), // New Feature: Real-time SQL Guardrails ) // Initialize TCP Proxy diff --git a/web/server.go b/web/server.go index 228acf1..177c296 100644 --- a/web/server.go +++ b/web/server.go @@ -54,6 +54,7 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { '' + (q.Latency / 1000000) + 'ms
' + '' + q.RawQuery + '' + (q.N1Flag ? '
N+1 Detected!' : '') + + (q.Violations ? q.Violations.map(v => '
' + v + '').join('') : '') + '' ).join(''); } From 65fdd3a21f1ce4f0aaf281127fa9a8da006924a6 Mon Sep 17 00:00:00 2001 From: moksh-solankipy <46241527+moksh-solankipy@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:54:45 +0530 Subject: [PATCH 4/9] chore: remove emojis and non-ASCII symbols for cleaner appearance --- analyzer/guardrail.go | 12 ++++++------ benchmark.go | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/analyzer/guardrail.go b/analyzer/guardrail.go index a8e6eb4..1bc35db 100644 --- a/analyzer/guardrail.go +++ b/analyzer/guardrail.go @@ -14,20 +14,20 @@ func NewGuardrailAnalyzer() *GuardrailAnalyzer { func (g *GuardrailAnalyzer) Analyze(ctx context.Context, event QueryEvent) (*AnalysisResult, error) { upperQuery := strings.ToUpper(event.RawQuery) - // 🚨 Guardrail 1: DELETE/UPDATE without WHERE (Dangerous!) + // Guardrail 1: DELETE/UPDATE without WHERE (Dangerous!) if (strings.Contains(upperQuery, "DELETE") || strings.Contains(upperQuery, "UPDATE")) && !strings.Contains(upperQuery, "WHERE") { - event.Violations = append(event.Violations, "āš ļø DANGEROUS: Modification query without WHERE clause detected!") + event.Violations = append(event.Violations, "DANGEROUS: Modification query without WHERE clause detected!") } - // 🐌 Guardrail 2: SELECT * (Performance Antipattern) + // Guardrail 2: SELECT * (Performance Antipattern) if strings.Contains(upperQuery, "SELECT *") { - event.Violations = append(event.Violations, "🐌 SLOW: SELECT * usage (fetch only needed columns)") + event.Violations = append(event.Violations, "SLOW: SELECT * usage (fetch only needed columns)") } - // šŸ›‘ Guardrail 3: SELECT without LIMIT (Scale Risk) + // Guardrail 3: SELECT without LIMIT (Scale Risk) if strings.Contains(upperQuery, "SELECT") && !strings.Contains(upperQuery, "LIMIT") { - event.Violations = append(event.Violations, "šŸ“¦ UNBOUNDED: SELECT without LIMIT (potential large result set)") + event.Violations = append(event.Violations, "UNBOUNDED: SELECT without LIMIT (potential large result set)") } return &AnalysisResult{Event: event}, nil diff --git a/benchmark.go b/benchmark.go index 33581de..b22344e 100644 --- a/benchmark.go +++ b/benchmark.go @@ -18,10 +18,10 @@ func main() { } defer db.Close() - fmt.Println("šŸš€ Starting Realistic SQLens Benchmark...") + fmt.Println("Starting Realistic SQLens Benchmark...") // 1. Setup Table and Seed Data - fmt.Println("\n--- šŸ› ļø Setup: Creating 'users' table ---") + fmt.Println("\n--- Setup: Creating 'users' table ---") _, err = db.Exec(` DROP TABLE IF EXISTS users; CREATE TABLE users ( @@ -47,7 +47,7 @@ func main() { // 2. Realistic N+1 Simulation // Scenario: Fetching user IDs, then fetching full details for each one individually - fmt.Println("\n--- šŸ•µļø Scenario: N+1 Detection (Fetching user details one by one) ---") + fmt.Println("\n--- Scenario: N+1 Detection (Fetching user details one by one) ---") rows, err := db.Query("SELECT id FROM users LIMIT 10") if err != nil { log.Fatal(err) @@ -75,7 +75,7 @@ func main() { } // 3. Slow Query Simulation - fmt.Println("\n--- 🐢 Scenario: Slow Query Detection (Complex search) ---") + fmt.Println("\n--- Scenario: Slow Query Detection (Complex search) ---") start := time.Now() var count int // Using pg_sleep to force a slow query report in SQLens @@ -86,13 +86,13 @@ func main() { fmt.Printf("Slow query finished (latency: %v)\n", time.Since(start)) // 4. Batch Updates - fmt.Println("\n--- ⚔ Scenario: Batch Updates ---") + fmt.Println("\n--- Scenario: Batch Updates ---") for i := 0; i < 50; i++ { _, _ = db.Exec("UPDATE users SET created_at = NOW() WHERE id = $1", (i%20)+1) } // 5. Performance Guardrails Test - fmt.Println("\n--- šŸ›”ļø Scenario: Performance Guardrails ---") + fmt.Println("\n--- Scenario: Performance Guardrails ---") fmt.Println("Triggering 'SELECT *' warning...") _, _ = db.Exec("SELECT * FROM users") @@ -102,6 +102,6 @@ func main() { fmt.Println("Triggering 'Missing LIMIT' warning...") _, _ = db.Exec("SELECT name FROM users") - fmt.Println("\nāœ… Benchmark Complete!") + fmt.Println("\nBenchmark Complete!") fmt.Println("Check the SQLens Dashboard (http://localhost:8080) for N+1 alerts and latency maps.") } From 0b2a3f7e85e18bd168afb0c77750c10858c15328 Mon Sep 17 00:00:00 2001 From: moksh-solankipy <46241527+moksh-solankipy@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:59:37 +0530 Subject: [PATCH 5/9] docs: update README for performance guardrails and redaction features --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 69999e3..d1bd277 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ SQLens is a transparent TCP proxy that intercepts and analyzes SQL queries in re - **Transparent Proxy:** Just point your app to SQLens instead of your DB. - **N+1 Detection:** Automatically flags inefficient ORM patterns. +- **Performance Guardrails:** Real-time detection of SQL anti-patterns (e.g., SELECT *, missing WHERE). +- **Data Privacy:** Optional PII redaction mode to mask sensitive query values. - **Slow Query Tracking:** Real-time latency measurement and visualization. - **Live Dashboard:** Web-based interface to see what's happening under the hood. @@ -44,6 +46,7 @@ SQLens can be configured via environment variables: - `SQLENS_LISTEN_ADDR`: Proxy listen address (default `:5433`) - `SQLENS_TARGET_ADDR`: Target database address (default `localhost:5432`) - `SQLENS_N1_THRESHOLD`: Number of repeated queries to trigger alert (default `5`) +- `SQLENS_REDACT_SENSITIVE`: Enable to mask sensitive data in logs/dashboard (default `false`) --- Built for SQL performance observability. From d64d1c62e008d9d434d3a16f730f2cc2b258ec07 Mon Sep 17 00:00:00 2001 From: moksh-solankipy <46241527+moksh-solankipy@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:12:14 +0530 Subject: [PATCH 6/9] fix: ensure analysis results are correctly preserved and visualized on the dashboard --- analyzer/analyzer.go | 4 ++-- proxy/server.go | 6 ++++-- web/server.go | 22 ++++++++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/analyzer/analyzer.go b/analyzer/analyzer.go index 5a965b0..e3a6bf0 100644 --- a/analyzer/analyzer.go +++ b/analyzer/analyzer.go @@ -31,16 +31,16 @@ func NewPipeline(analyzers ...Analyzer) *Pipeline { return &Pipeline{analyzers: analyzers} } -func (p *Pipeline) Process(ctx context.Context, event QueryEvent) { +func (p *Pipeline) Process(ctx context.Context, event QueryEvent) QueryEvent { currentEvent := event for _, a := range p.analyzers { result, err := a.Analyze(ctx, currentEvent) if err != nil { - // In a real app we'd log this, but we continue processing continue } if result != nil { currentEvent = result.Event } } + return currentEvent } diff --git a/proxy/server.go b/proxy/server.go index 61f04cb..8ef8ccd 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -154,9 +154,11 @@ func (s *Server) handleConnection(ctx context.Context, clientConn net.Conn) { Timestamp: time.Now(), Latency: l, } - s.pipeline.Process(context.Background(), event) - // If redaction is enabled, we use the fingerprint instead of raw SQL + // Reassign event to the one that's been through the pipeline + event = s.pipeline.Process(context.Background(), event) + + // If redaction is enabled, mask the raw SQL if s.redactSensitive { event.RawQuery = event.Fingerprint } diff --git a/web/server.go b/web/server.go index 177c296..f48a641 100644 --- a/web/server.go +++ b/web/server.go @@ -39,24 +39,30 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { SQLens Dashboard