packagedatabaseimport("net/url""reflect""strings""testing""time")funcTestBuildQueryString(t*testing.T){tests:=[]struct{namestringqueryEventQuerytotalboolwantQuerystringwantArgs[]any}{{name:"empty query total",query:EventQuery{},total:true,wantQuery:"SELECT COUNT(*) FROM honeypot_events",wantArgs:[]any{},},{name:"empty query non-total",query:EventQuery{},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",wantArgs:[]any{},},{name:"with IDs filter",query:EventQuery{IDs:[]string{"1","2","3"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE id IN (?, ?, ?)",wantArgs:[]any{"1","2","3"},},{name:"with time range",query:EventQuery{TimeStart:time.Date(2024,1,1,0,0,0,0,time.UTC),TimeEnd:time.Date(2024,12,31,23,59,59,0,time.UTC),},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE time >= ? AND time <= ?",wantArgs:[]any{"2024-01-01T00:00:00Z","2024-12-31T23:59:59Z"},},{name:"with remote addresses",query:EventQuery{RemoteAddrs:[]string{"192.168.1.1","10.0.0.1"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_ip_int = ? OR remote_ip_int = ?)",wantArgs:[]any{uint32(3232235777),uint32(167772161)},},{name:"with remote addresses negation",query:EventQuery{RemoteAddrs:[]string{"192.168.1.1","!10.0.0.1"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_ip_int = ?) AND (remote_ip_int != ?)",wantArgs:[]any{uint32(3232235777),uint32(167772161)},},{name:"with destination ports",query:EventQuery{DstPorts:[]string{"80","443","8000-8080","!22"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (dst_port = ? OR dst_port = ? OR dst_port BETWEEN ? AND ?) AND (dst_port != ?)",wantArgs:[]any{80,443,8000,8080,22},},{name:"with destination ports 2",query:EventQuery{DstPorts:[]string{"!80","443"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (dst_port = ?) AND (dst_port != ?)",wantArgs:[]any{443,80},},{name:"with type filter",query:EventQuery{Type:[]string{"ssh","http"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE type IN (?, ?)",wantArgs:[]any{"ssh","http"},},{name:"with event filter",query:EventQuery{Event:[]string{"login","!logout"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE event IN (?) AND event NOT IN (?)",wantArgs:[]any{"login","logout"},},{name:"with custom columns",query:EventQuery{Columns:[]string{"id","time","type"},},total:false,wantQuery:"SELECT id, time, type FROM honeypot_events",wantArgs:[]any{},},{name:"with invalid columns filtered out",query:EventQuery{Columns:[]string{"id","time","invalid_col","type"},},total:false,wantQuery:"SELECT id, time, type FROM honeypot_events",wantArgs:[]any{},},{name:"with all valid columns",query:EventQuery{Columns:[]string{"id","time","type","event","remote_addr","remote_port","dst_port","fields"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",wantArgs:[]any{},},{name:"with JSON field filter",query:EventQuery{FieldFilters:map[string][]string{"username":{"admin"}},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?)",wantArgs:[]any{"$.username","admin"},},{name:"with JSON field filter multiple values",query:EventQuery{FieldFilters:map[string][]string{"username":{"admin","user","guest"}},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?, ?, ?)",wantArgs:[]any{"$.username","admin","user","guest"},},{name:"with JSON field filter negation",query:EventQuery{FieldFilters:map[string][]string{"username":{"admin","!user"}},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?) AND json_extract_string(fields, ?) NOT IN (?)",wantArgs:[]any{"$.username","admin","$.username","user"},},{name:"with multiple JSON field filters",query:EventQuery{FieldFilters:map[string][]string{"username":{"admin"},"status":{"active"}},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) IN (?) AND json_extract_string(fields, ?) IN (?)",wantArgs:[]any{"$.username","admin","$.status","active"},},{name:"with field exists",query:EventQuery{FieldExists:[]string{"username","password"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract(fields, ?) IS NOT NULL AND json_extract(fields, ?) IS NOT NULL",wantArgs:[]any{"$.username","$.password"},},{name:"with order direction",query:EventQuery{OrderDirection:"ASC",},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events ORDER BY time ASC",wantArgs:[]any{},},{name:"with limit",query:EventQuery{Limit:10,},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events LIMIT 10",wantArgs:[]any{},},{name:"with offset",query:EventQuery{Offset:20,},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events OFFSET 20",wantArgs:[]any{},},{name:"with limit and offset",query:EventQuery{Limit:10,Offset:20,},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events LIMIT 10 OFFSET 20",wantArgs:[]any{},},{name:"complex query with all filters",query:EventQuery{IDs:[]string{"1","2"},TimeStart:time.Date(2024,1,1,0,0,0,0,time.UTC),TimeEnd:time.Date(2024,12,31,23,59,59,0,time.UTC),RemoteAddrs:[]string{"192.168.1.1","!10.0.0.1"},DstPorts:[]string{"80","443"},Type:[]string{"http"},Event:[]string{"request"},FieldFilters:map[string][]string{"username":{"admin"}},FieldExists:[]string{"session_id"},Columns:[]string{"id","time","type"},OrderDirection:"DESC",Limit:50,Offset:100,},total:false,wantQuery:"SELECT id, time, type FROM honeypot_events WHERE time >= ? AND time <= ? AND id IN (?, ?) AND (remote_ip_int = ?) AND (remote_ip_int != ?) AND (dst_port = ? OR dst_port = ?) AND type IN (?) AND event IN (?) AND json_extract_string(fields, ?) IN (?) AND json_extract(fields, ?) IS NOT NULL ORDER BY time DESC LIMIT 50 OFFSET 100",wantArgs:[]any{"2024-01-01T00:00:00Z","2024-12-31T23:59:59Z","1","2",uint32(3232235777),uint32(167772161),80,443,"http","request","$.username","admin","$.session_id"},},{name:"complex query total",query:EventQuery{IDs:[]string{"1","2"},TimeStart:time.Date(2024,1,1,0,0,0,0,time.UTC),TimeEnd:time.Date(2024,12,31,23,59,59,0,time.UTC),RemoteAddrs:[]string{"192.168.1.1"},DstPorts:[]string{"80"},Type:[]string{"http"},Event:[]string{"request"},FieldFilters:map[string][]string{"username":{"admin"}},FieldExists:[]string{"session_id"},OrderDirection:"DESC",Limit:50,Offset:100,},total:true,wantQuery:"SELECT COUNT(*) FROM honeypot_events WHERE time >= ? AND time <= ? AND id IN (?, ?) AND (remote_ip_int = ?) AND (dst_port = ?) AND type IN (?) AND event IN (?) AND json_extract_string(fields, ?) IN (?) AND json_extract(fields, ?) IS NOT NULL",wantArgs:[]any{"2024-01-01T00:00:00Z","2024-12-31T23:59:59Z","1","2",uint32(3232235777),80,"http","request","$.username","admin","$.session_id"},},{name:"empty arrays don't add clauses",query:EventQuery{IDs:[]string{},RemoteAddrs:[]string{},DstPorts:[]string{},Type:[]string{},Event:[]string{},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",wantArgs:[]any{},},{name:"single negation value",query:EventQuery{RemoteAddrs:[]string{"!192.168.1.1"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_ip_int != ?)",wantArgs:[]any{uint32(3232235777)},},{name:"negation with single character ignored",query:EventQuery{RemoteAddrs:[]string{"!"},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (remote_addr = ?)",wantArgs:[]any{"!"},},{name:"zero limit and offset ignored",query:EventQuery{Limit:0,Offset:0,},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events",wantArgs:[]any{},},{name:"domain with wildcards",query:EventQuery{Domains:[]string{"*.censys-scanner.com","google.*","!*.evil.com"},},total:false,wantQuery:"SELECT honeypot_events.id, honeypot_events.time, honeypot_events.type, honeypot_events.event, honeypot_events.remote_addr, honeypot_events.remote_port, honeypot_events.dst_port, honeypot_events.fields, ips.country, ips.city, ips.latitude, ips.longitude FROM honeypot_events JOIN ips ON honeypot_events.remote_addr = ips.ip WHERE (ips.domain GLOB ? OR ips.domain GLOB ?) AND ips.domain NOT GLOB ?",wantArgs:[]any{"*.censys-scanner.com","google.*","*.evil.com"},},{name:"with JSON field wildcard",query:EventQuery{FieldFilters:map[string][]string{"uri":{"/v1/*"}},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE json_extract_string(fields, ?) GLOB ?",wantArgs:[]any{"$.uri","/v1/*"},},{name:"with complex JSON field filters",query:EventQuery{FieldFilters:map[string][]string{"uri":{"/v1/*","/v2/*","!/v1/private/*"}},},total:false,wantQuery:"SELECT id, time, type, event, remote_addr, remote_port, dst_port, fields FROM honeypot_events WHERE (json_extract_string(fields, ?) GLOB ? OR json_extract_string(fields, ?) GLOB ?) AND json_extract_string(fields, ?) NOT GLOB ?",wantArgs:[]any{"$.uri","/v1/*","$.uri","/v2/*","$.uri","/v1/private/*"},},}for_,tt:=rangetests{t.Run(tt.name,func(t*testing.T){gotQuery,gotArgs:=buildQueryString(tt.query,tt.total)assertEqualSQL(t,gotQuery,gotArgs,tt.wantQuery,tt.wantArgs)})}}funcTestParseEventQuery(t*testing.T){db:=&Database{}tests:=[]struct{namestring// description of this test case// Named input parameters for target function.queryurl.ValueswantEventQuerywantErrbool}{{name:"empty query",query:url.Values{},want:EventQuery{Limit:100,Offset:0,OrderDirection:"desc",IDs:nil,RemoteAddrs:nil,RemotePorts:nil,DstPorts:nil,Event:nil,Type:nil,FieldFilters:map[string][]string{},FieldExists:[]string{},Columns:nil,},},{name:"basic parameters",query:url.Values{"limit":[]string{"50"},"offset":[]string{"10"},"order_direction":[]string{"asc"},},want:EventQuery{Limit:50,Offset:10,OrderDirection:"asc",IDs:nil,RemoteAddrs:nil,RemotePorts:nil,DstPorts:nil,Event:nil,Type:nil,FieldFilters:map[string][]string{},FieldExists:[]string{},Columns:nil,},},{name:"csv parameters",query:url.Values{"id":[]string{"1","2","3"},"remote_addr":[]string{"1.1.1.1","2.2.2.2"},"dst_port":[]string{"80","443"},"type":[]string{"http","ssh"},"event":[]string{"login","logout"},},want:EventQuery{Limit:100,Offset:0,OrderDirection:"desc",IDs:[]string{"1","2","3"},RemoteAddrs:[]string{"1.1.1.1","2.2.2.2"},RemotePorts:nil,DstPorts:[]string{"80","443"},Event:[]string{"login","logout"},Type:[]string{"http","ssh"},FieldFilters:map[string][]string{},FieldExists:[]string{},Columns:nil,},},{name:"time parameters",query:url.Values{"time_start":[]string{"2024-01-01 00:00:00"},"time_end":[]string{"2024-01-01 23:59:59"},},want:EventQuery{Limit:100,Offset:0,OrderDirection:"desc",IDs:nil,RemoteAddrs:nil,RemotePorts:nil,DstPorts:nil,Event:nil,Type:nil,FieldFilters:map[string][]string{},FieldExists:[]string{},Columns:nil,TimeStart:time.Date(2024,1,1,0,0,0,0,time.UTC),TimeEnd:time.Date(2024,1,1,23,59,59,0,time.UTC),},},{name:"field parameters",query:url.Values{"f:user":[]string{"admin"},"f:pass":[]string{"1234"},"fe:session":[]string{""},},want:EventQuery{Limit:100,Offset:0,OrderDirection:"desc",IDs:nil,RemoteAddrs:nil,RemotePorts:nil,DstPorts:nil,Event:nil,Type:nil,FieldFilters:map[string][]string{"user":{"admin"},"pass":{"1234"}},FieldExists:[]string{"session"},Columns:nil,},},{name:"columns parameter",query:url.Values{"columns":[]string{"id","time","type"},},want:EventQuery{Limit:100,Offset:0,OrderDirection:"desc",IDs:nil,RemoteAddrs:nil,RemotePorts:nil,DstPorts:nil,Event:nil,Type:nil,FieldFilters:map[string][]string{},FieldExists:[]string{},Columns:[]string{"id","time","type"},},},}for_,tt:=rangetests{t.Run(tt.name,func(t*testing.T){got,gotErr:=db.parseEventQuery(tt.query)if(gotErr!=nil)!=tt.wantErr{t.Errorf("parseEventQuery() error = %v, wantErr %v",gotErr,tt.wantErr)return}iftt.wantErr{return}if!reflect.DeepEqual(got,tt.want){t.Errorf("parseEventQuery() = %v, want %v",got,tt.want)}})}}funcTest_addPortClauses(t*testing.T){tests:=[]struct{namestringvalues[]stringwantClauses[]stringwantArgs[]any}{{name:"empty values",values:[]string{},wantClauses:nil,wantArgs:nil,},{name:"single port",values:[]string{"80"},wantClauses:[]string{"(dst_port = ?)"},wantArgs:[]any{80},},{name:"multiple ports",values:[]string{"80","443"},wantClauses:[]string{"(dst_port = ? OR dst_port = ?)"},wantArgs:[]any{80,443},},{name:"port range",values:[]string{"8000-8080"},wantClauses:[]string{"(dst_port BETWEEN ? AND ?)"},wantArgs:[]any{8000,8080},},{name:"negated port",values:[]string{"!22"},wantClauses:[]string{"(dst_port != ?)"},wantArgs:[]any{22},},{name:"negated range",values:[]string{"!1024-2048"},wantClauses:[]string{"(dst_port NOT BETWEEN ? AND ?)"},wantArgs:[]any{1024,2048},},{name:"mixed include and exclude",values:[]string{"!80","443"},wantClauses:[]string{"(dst_port = ?)","(dst_port != ?)"},wantArgs:[]any{443,80},},{name:"complex mixed",values:[]string{"80","443-445","!22","!3000-3001"},wantClauses:[]string{"(dst_port = ? OR dst_port BETWEEN ? AND ?)","(dst_port != ? AND dst_port NOT BETWEEN ? AND ?)"},wantArgs:[]any{80,443,445,22,3000,3001},},{name:"invalid port ignored",values:[]string{"abc","80"},wantClauses:[]string{"(dst_port = ?)"},wantArgs:[]any{80},},}for_,tt:=rangetests{t.Run(tt.name,func(t*testing.T){vargotClauses[]stringvargotArgs[]anyaddPortClauses(&gotClauses,&gotArgs,tt.values,"dst_port")if!reflect.DeepEqual(gotClauses,tt.wantClauses){t.Errorf("addPortClauses() clauses = %v, want %v",gotClauses,tt.wantClauses)}if!reflect.DeepEqual(gotArgs,tt.wantArgs){t.Errorf("addPortClauses() args = %v, want %v",gotArgs,tt.wantArgs)}})}}typesqlClausestruct{querystringargs[]any}funcparseSQL(querystring,args[]any)(basestring,clauses[]sqlClause,tailstring){query=strings.Join(strings.Fields(query)," ")whereIdx:=strings.Index(query," WHERE ")ifwhereIdx==-1{// No WHERE, but might have ORDER BY etc.tailIdx:=-1for_,kw:=range[]string{" ORDER BY "," LIMIT "," OFFSET "}{idx:=strings.Index(query,kw)ifidx!=-1&&(tailIdx==-1||idx<tailIdx){tailIdx=idx}}iftailIdx==-1{returnquery,nil,""}returnquery[:tailIdx],nil,query[tailIdx:]}base=query[:whereIdx]afterWhere:=query[whereIdx+7:]// len(" WHERE ")tailIdx:=-1for_,kw:=range[]string{" ORDER BY "," LIMIT "," OFFSET "}{idx:=strings.Index(afterWhere,kw)ifidx!=-1&&(tailIdx==-1||idx<tailIdx){tailIdx=idx}}varwherePartstringiftailIdx!=-1{wherePart=afterWhere[:tailIdx]tail=afterWhere[tailIdx:]}else{wherePart=afterWhere}// Split wherePart into clausesrawClauses:=splitWhereClauses(wherePart)argIdx:=0for_,rc:=rangerawClauses{placeholders:=strings.Count(rc,"?")ifargIdx+placeholders>len(args){clauses=append(clauses,sqlClause{query:rc,args:nil})continue}clauses=append(clauses,sqlClause{query:rc,args:args[argIdx:argIdx+placeholders]})argIdx+=placeholders}returnbase,clauses,tail}funcsplitWhereClauses(wherePartstring)[]string{varclauses[]stringvarcurrentstrings.BuilderparenLevel:=0chars:=[]rune(wherePart)fori:=0;i<len(chars);i++{c:=chars[i]ifc=='('{parenLevel++current.WriteRune(c)}elseifc==')'{parenLevel--current.WriteRune(c)}elseifparenLevel==0&&i+5<=len(chars)&&string(chars[i:i+5])==" AND "{clauses=append(clauses,strings.TrimSpace(current.String()))current.Reset()i+=4// Skip " AND"}else{current.WriteRune(c)}}ifcurrent.Len()>0{clauses=append(clauses,strings.TrimSpace(current.String()))}returnclauses}funcassertEqualSQL(t*testing.T,gotQuerystring,gotArgs[]any,wantQuerystring,wantArgs[]any){t.Helper()gotBase,gotClauses,gotTail:=parseSQL(gotQuery,gotArgs)wantBase,wantClauses,wantTail:=parseSQL(wantQuery,wantArgs)ifgotBase!=wantBase{t.Errorf("SQL base mismatch:\ngot: %s\nwant: %s",gotBase,wantBase)return}ifgotTail!=wantTail{t.Errorf("SQL tail mismatch:\ngot: %s\nwant: %s",gotTail,wantTail)return}iflen(gotClauses)!=len(wantClauses){t.Errorf("SQL WHERE clauses count mismatch: got %d, want %d\ngot query: %s\nwant query: %s",len(gotClauses),len(wantClauses),gotQuery,wantQuery)return}// Match clausesvisited:=make([]bool,len(gotClauses))for_,wantC:=rangewantClauses{found:=falsefori,gotC:=rangegotClauses{if!visited[i]&&gotC.query==wantC.query&&reflect.DeepEqual(gotC.args,wantC.args){visited[i]=truefound=truebreak}}if!found{t.Errorf("Missing expected WHERE clause: %s with args %v\nGot query: %s\nGot args: %v",wantC.query,wantC.args,gotQuery,gotArgs)return}}}