packagedatabaseimport("database/sql""encoding/binary""errors""fmt""honeypot/internal/utils""net""net/url""strings""time")typeLabelCountstruct{Labelstring`json:"label"`Countint`json:"count"`}typeActivityCountstruct{Timetime.Time`json:"time"`Typestring`json:"type"`Countfloat64`json:"count"`}// getTopNFields returns the top n fields for a given event type and field expressionfunc(db*Database)GetTopNFields(fieldSelectstring,whereClausestring,args[]any,limitint)([]LabelCount,error){rows,err:=db.DB.Query(fmt.Sprintf(` SELECT %s as field, COUNT(*) as count
FROM honeypot_events
WHERE %s AND field IS NOT NULL
GROUP BY field
ORDER BY count DESC
LIMIT ?
`,fieldSelect,whereClause),append(args,limit)...)iferr!=nil{returnnil,err}deferrows.Close()varcounts[]LabelCountforrows.Next(){varfcLabelCountiferr:=rows.Scan(&fc.Label,&fc.Count);err!=nil{returnnil,err}counts=append(counts,fc)}returncounts,nil}// HoneypotStats contains stats for a honeypot type: top remote addrs, ports, event typestypeHoneypotStatsstruct{Titlestring`json:"title,omitempty"`Metadatamap[string]any`json:"metadata,omitempty"`TotalEventsint`json:"total_events,omitempty"`FirstSeenstring`json:"first_seen,omitempty"`LastSeenstring`json:"last_seen,omitempty"`RemoteAddrs[]LabelCount`json:"top_addrs"`Ports[]LabelCount`json:"top_ports"`EventTypes[]LabelCount`json:"top_events"`Methods[]LabelCount`json:"methods,omitempty"`UserAgents[]LabelCount`json:"user_agents,omitempty"`URIs[]LabelCount`json:"uris,omitempty"`Hosts[]LabelCount`json:"hosts,omitempty"`Referers[]LabelCount`json:"referers,omitempty"`ClientVersions[]LabelCount`json:"client_versions,omitempty"`Usernames[]LabelCount`json:"usernames,omitempty"`Passwords[]LabelCount`json:"passwords,omitempty"`SecurityLayers[]LabelCount`json:"security_layers,omitempty"`FQDNs[]LabelCount`json:"fqdns,omitempty"`}varhttpFields=[][2]string{{"fields.headers.\"User-Agent\"","user-agent"},{"fields.uri","uri"},{"fields.host","host"},{"fields.headers.\"Referer\"","referer"},{"fields.username","username"},{"fields.password","password"},{"fields.method","method"},}varsshFields=[][2]string{{"fields.client_version","client_version"},{"fields.username","username"},{"fields.password","password"},}vartelnetFields=[][2]string{{"fields.username","username"},{"fields.password","password"},}varftpFields=[][2]string{{"fields.username","username"},{"fields.password","password"},}varrdpFields=[][2]string{{"fields.username","username"},{"fields.security_layer","security_layer"},}// GetHoneypotStats returns the top 50 remote addresses, top 20 ports, top 20 events for a given honeypot typefunc(db*Database)GetHoneypotStats(eventTypestring)(*HoneypotStats,error){ifdb.DB==nil{returnnil,errors.New("database is not connected")}stats:=&HoneypotStats{}varfields[][2]stringswitcheventType{case"http":fields=httpFieldscase"ssh":fields=sshFieldscase"telnet":fields=telnetFieldscase"ftp":fields=ftpFieldscase"rdp":fields=rdpFields}// Get overall statsvarfirstSeen,lastSeensql.NullTimeerr:=db.DB.QueryRow(` SELECT COUNT(*), MIN(time), MAX(time)
FROM honeypot_events
WHERE type = ?`,eventType).Scan(&stats.TotalEvents,&firstSeen,&lastSeen)iferr!=nil{returnnil,err}iffirstSeen.Valid{stats.FirstSeen=firstSeen.Time.Format(time.RFC3339Nano)}iflastSeen.Valid{stats.LastSeen=lastSeen.Time.Format(time.RFC3339Nano)}fields=append(fields,[2]string{"event","event"})fields=append(fields,[2]string{"remote_addr","remote_addr"})fields=append(fields,[2]string{"dst_port","dst_port"})for_,fieldName:=rangefields{counts,err:=db.GetTopNFields(fieldName[0],"type = ?",[]any{eventType},20)iferr!=nil{returnnil,err}for_,c:=rangecounts{switchfieldName[1]{case"event":stats.EventTypes=append(stats.EventTypes,LabelCount{Label:c.Label,Count:c.Count})case"remote_addr":stats.RemoteAddrs=append(stats.RemoteAddrs,LabelCount{Label:c.Label,Count:c.Count})case"dst_port":stats.Ports=append(stats.Ports,LabelCount{Label:c.Label,Count:c.Count})case"method":stats.Methods=append(stats.Methods,LabelCount{Label:c.Label,Count:c.Count})case"user-agent":stats.UserAgents=append(stats.UserAgents,LabelCount{Label:c.Label,Count:c.Count})case"uri":stats.URIs=append(stats.URIs,LabelCount{Label:c.Label,Count:c.Count})case"host":stats.Hosts=append(stats.Hosts,LabelCount{Label:c.Label,Count:c.Count})case"referer":stats.Referers=append(stats.Referers,LabelCount{Label:c.Label,Count:c.Count})case"client_version":stats.ClientVersions=append(stats.ClientVersions,LabelCount{Label:c.Label,Count:c.Count})case"username":stats.Usernames=append(stats.Usernames,LabelCount{Label:c.Label,Count:c.Count})case"password":stats.Passwords=append(stats.Passwords,LabelCount{Label:c.Label,Count:c.Count})case"security_layer":stats.SecurityLayers=append(stats.SecurityLayers,LabelCount{Label:c.Label,Count:c.Count})}}}returnstats,nil}// GetFirstLastSeenTotalForNet returns the first and last seen timestamps and total count for a given networkfunc(db*Database)GetFirstLastSeenTotalForNet(networkstring)(string,string,int,error){ifdb.DB==nil{return"","",0,errors.New("database is not connected")}varwherestringvarargs[]anyvarerrerrorifwhere,args,err=GetIpWhere(network);err!=nil{return"","",0,err}query:=fmt.Sprintf(` SELECT COUNT(*) as count, MIN(time) as first_seen, MAX(time) as last_seen
FROM honeypot_events
WHERE %s
`,where)row:=db.DB.QueryRow(query,args...)varcountintvarfirstSeen,lastSeensql.NullTimeerr=row.Scan(&count,&firstSeen,&lastSeen)iferr!=nil{return"","",0,err}firstSeenStr:=""iffirstSeen.Valid{firstSeenStr=firstSeen.Time.Format(time.RFC3339Nano)}lastSeenStr:=""iflastSeen.Valid{lastSeenStr=lastSeen.Time.Format(time.RFC3339Nano)}returnfirstSeenStr,lastSeenStr,count,nil}// GetPortStatsOverview returns the first and last seen timestamps and total count for a given portfunc(db*Database)GetPortStatsOverview(portint)(string,string,int,error){ifdb.DB==nil{return"","",0,errors.New("database is not connected")}query:=` SELECT COUNT(*) as count, MIN(time) as first_seen, MAX(time) as last_seen
FROM honeypot_events
WHERE dst_port = ?
`row:=db.DB.QueryRow(query,port)varcountintvarfirstSeen,lastSeensql.NullTimeerr:=row.Scan(&count,&firstSeen,&lastSeen)iferr!=nil{return"","",0,err}firstSeenStr:=""iffirstSeen.Valid{firstSeenStr=firstSeen.Time.Format(time.RFC3339Nano)}lastSeenStr:=""iflastSeen.Valid{lastSeenStr=lastSeen.Time.Format(time.RFC3339Nano)}returnfirstSeenStr,lastSeenStr,count,nil}// GetSubnetStats returns the addresses and counts for a given subnetfunc(db*Database)GetSubnetStats(subnetstring)([]LabelCount,error){ifdb.DB==nil{returnnil,errors.New("database is not connected")}varwherestringvarargs[]anyvarerrerrorifwhere,args,err=GetIpWhere(subnet);err!=nil{returnnil,err}query:=fmt.Sprintf(` SELECT remote_addr, COUNT(*) as count
FROM honeypot_events
WHERE %s
GROUP BY remote_addr
ORDER BY count DESC, remote_addr ASC
`,where)rows,err:=db.DB.Query(query,args...)iferr!=nil{returnnil,err}deferrows.Close()varstats[]LabelCountforrows.Next(){varacLabelCountiferr:=rows.Scan(&ac.Label,&ac.Count);err!=nil{returnnil,err}stats=append(stats,ac)}returnstats,nil}// GetIpWhere returns the where clause and arguments for a given IP or subnetfuncGetIpWhere(ipstring)(string,[]any,error){varwherestringvarargs[]anyifstrings.Contains(ip,"/"){_,ipNet,err:=net.ParseCIDR(ip)iferr==nil&&ipNet.IP.To4()!=nil{// IPv4 Subnetmask:=binary.BigEndian.Uint32(ipNet.Mask)start:=binary.BigEndian.Uint32(ipNet.IP.To4())end:=start|^maskwhere="remote_ip_int >= ? AND remote_ip_int <= ?"args=[]any{start,end}}else{// Fallback to string matching for IPv6 or if parsing failedwhere="remote_addr::INET <<= ?::INET"args=[]any{ip}}}else{where="remote_ip_int = ?"ipInt,err:=utils.IPToInt(ip)iferr!=nil{return"",nil,err}args=[]any{ipInt}}returnwhere,args,nil}// GetActivityOverTime returns the event count per time slot and honeypot typefunc(db*Database)GetActivityOverTime(qurl.Values)([]ActivityCount,error){ifdb.DB==nil{returnnil,errors.New("database is not connected")}query,err:=db.parseEventQuery(q)iferr!=nil{returnnil,err}bucketInterval,bucketMinutes:=db.calculateBucketInterval(query.TimeStart,query.TimeEnd)hasGeo:=len(query.ASNs)>0||len(query.Countries)>0||len(query.Cities)>0||len(query.Domains)>0||len(query.FQDNs)>0whereClauses,args:=buildWhereClauses(query,hasGeo)whereStr:=""iflen(whereClauses)>0{whereStr="WHERE "+strings.Join(whereClauses," AND ")}joinStr:=""typeField:="type"timeField:="time"ifhasGeo{joinStr="JOIN ips ON honeypot_events.remote_addr = ips.ip"typeField="honeypot_events.type"timeField="honeypot_events.time"}sql:=fmt.Sprintf(` SELECT time_bucket(INTERVAL '%s', %s) AS bucket, %s, CAST(COUNT(*) AS FLOAT) / %f as count
FROM honeypot_events
%s
%s
GROUP BY bucket, honeypot_events.type
ORDER BY bucket ASC, count DESC
`,bucketInterval,timeField,typeField,bucketMinutes,joinStr,whereStr)rows,err:=db.DB.Query(sql,args...)iferr!=nil{returnnil,err}deferrows.Close()varactivity[]ActivityCountforrows.Next(){varacActivityCountiferr:=rows.Scan(&ac.Time,&ac.Type,&ac.Count);err!=nil{returnnil,err}activity=append(activity,ac)}returnactivity,nil}func(db*Database)calculateBucketInterval(start,endtime.Time)(string,float64){ifstart.IsZero(){return"1 HOUR",60.0}ifend.IsZero(){end=time.Now()}duration:=end.Sub(start)ifduration<0{duration=-duration}switch{caseduration<=24*time.Hour:return"1 MINUTE",1.0caseduration<=7*24*time.Hour:return"5 MINUTES",5.0caseduration<=14*24*time.Hour:return"15 MINUTES",15.0caseduration<=30*24*time.Hour:return"1 HOUR",60.0caseduration<=6*30*24*time.Hour:return"6 HOURS",360.0caseduration<=12*30*24*time.Hour:return"12 HOURS",720.0default:return"1 DAY",1440.0}}