diff --git a/go.mod b/go.mod index 3e8ceac..e0228ed 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,15 @@ go 1.24.0 toolchain go1.24.6 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf + github.com/getsentry/sentry-go v0.40.0 github.com/glebarez/sqlite v1.11.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.17.1 + github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 @@ -30,7 +33,6 @@ require ( ) require ( - github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -38,7 +40,6 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/getsentry/sentry-go v0.40.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -66,7 +67,6 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect diff --git a/go.sum b/go.sum index e10c562..b9335b2 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo= @@ -27,6 +29,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -77,6 +81,10 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= diff --git a/pkg/common/sql_helpers.go b/pkg/common/sql_helpers.go index 4c99001..1cf0089 100644 --- a/pkg/common/sql_helpers.go +++ b/pkg/common/sql_helpers.go @@ -393,6 +393,7 @@ func getValidColumnsForTable(tableName string) map[string]bool { // extractTableAndColumn extracts the table prefix and column name from a qualified reference // For example: "users.status = 'active'" returns ("users", "status") // Returns empty strings if no table prefix is found +// This function is parenthesis-aware and will only look for operators outside of subqueries func extractTableAndColumn(cond string) (table string, column string) { // Common SQL operators to find the column reference operators := []string{" = ", " != ", " <> ", " > ", " >= ", " < ", " <= ", " LIKE ", " like ", " IN ", " in ", " IS ", " is "} @@ -400,13 +401,20 @@ func extractTableAndColumn(cond string) (table string, column string) { var columnRef string // Find the column reference (left side of the operator) + // We need to find the first operator that appears OUTSIDE of parentheses + minIdx := -1 + for _, op := range operators { - if idx := strings.Index(cond, op); idx > 0 { - columnRef = strings.TrimSpace(cond[:idx]) - break + idx := findOperatorOutsideParentheses(cond, op) + if idx > 0 && (minIdx == -1 || idx < minIdx) { + minIdx = idx } } + if minIdx > 0 { + columnRef = strings.TrimSpace(cond[:minIdx]) + } + // If no operator found, the whole condition might be the column reference if columnRef == "" { parts := strings.Fields(cond) @@ -437,6 +445,52 @@ func extractTableAndColumn(cond string) (table string, column string) { return "", "" } +// findOperatorOutsideParentheses finds the first occurrence of an operator outside of parentheses +// Returns the index of the operator, or -1 if not found or only found inside parentheses +func findOperatorOutsideParentheses(s string, operator string) int { + depth := 0 + inSingleQuote := false + inDoubleQuote := false + + for i := 0; i < len(s); i++ { + ch := s[i] + + // Track quote state (operators inside quotes should be ignored) + if ch == '\'' && !inDoubleQuote { + inSingleQuote = !inSingleQuote + continue + } + if ch == '"' && !inSingleQuote { + inDoubleQuote = !inDoubleQuote + continue + } + + // Skip if we're inside quotes + if inSingleQuote || inDoubleQuote { + continue + } + + // Track parenthesis depth + if ch == '(' { + depth++ + } else if ch == ')' { + depth-- + } + + // Only look for the operator when we're outside parentheses (depth == 0) + if depth == 0 { + // Check if the operator starts at this position + if i+len(operator) <= len(s) { + if s[i:i+len(operator)] == operator { + return i + } + } + } + } + + return -1 +} + // isValidColumn checks if a column name exists in the valid columns map // Handles case-insensitive comparison func isValidColumn(columnName string, validColumns map[string]bool) bool { diff --git a/pkg/common/sql_helpers_test.go b/pkg/common/sql_helpers_test.go index 92c6d12..0add7f1 100644 --- a/pkg/common/sql_helpers_test.go +++ b/pkg/common/sql_helpers_test.go @@ -122,6 +122,18 @@ func TestSanitizeWhereClause(t *testing.T) { tableName: "users", expected: "", }, + { + name: "subquery with table alias should not be modified", + where: "apiprovider.rid_apiprovider in (select l.rid_apiprovider from core.apiproviderlink l where l.rid_hub = 2576)", + tableName: "apiprovider", + expected: "apiprovider.rid_apiprovider in (select l.rid_apiprovider from core.apiproviderlink l where l.rid_hub = 2576)", + }, + { + name: "complex subquery with AND and multiple operators", + where: "apiprovider.type in ('softphone') AND (apiprovider.rid_apiprovider in (select l.rid_apiprovider from core.apiproviderlink l where l.rid_hub = 2576))", + tableName: "apiprovider", + expected: "apiprovider.type in ('softphone') AND (apiprovider.rid_apiprovider in (select l.rid_apiprovider from core.apiproviderlink l where l.rid_hub = 2576))", + }, } for _, tt := range tests {