Skip to content

Commit b30b8b4

Browse files
committed
refactor: move query filter building out of API layer
Remove BSON filter construction from api.go and map request fields directly onto FindFlowsOptions. Add ExcludePorts field to handle the dst_port=-1 (exclude all service ports) case. Update mongo.go to apply ExcludePorts via $nin. Simplify apiFlowEntry to embed db.FlowEntry instead of manually copying fields.
1 parent 92ace71 commit b30b8b4

4 files changed

Lines changed: 27 additions & 161 deletions

File tree

services/cmd/api/api.go

Lines changed: 22 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import (
2020
"tulip/pkg/db"
2121

2222
"github.com/labstack/echo/v4"
23-
"go.mongodb.org/mongo-driver/bson"
24-
"go.mongodb.org/mongo-driver/bson/primitive"
2523
)
2624

2725
// Router holds dependencies for handlers
@@ -71,8 +69,6 @@ func (api *Router) getTickInfo(c echo.Context) error {
7169

7270
func (api *Router) query(c echo.Context) error {
7371

74-
// TODO: this is horrible, the API layer should not be aware of the database structure
75-
7672
type flowQueryRequest struct {
7773
IncludeTags []string `json:"includeTags"`
7874
ExcludeTags []string `json:"excludeTags"`
@@ -94,157 +90,41 @@ func (api *Router) query(c echo.Context) error {
9490
return c.JSON(http.StatusBadRequest, apiError{Error: "Invalid request format"})
9591
}
9692

97-
filter := bson.D{}
98-
99-
// Handle "flow.data" regex filter
100-
if req.FlowData != "" {
101-
filter = append(filter, bson.E{
102-
Key: "flow.data",
103-
Value: bson.M{
104-
"$regex": req.FlowData,
105-
"$options": "i", // Case-insensitive regex
106-
},
107-
})
108-
}
109-
110-
// Handle "dst_ip"
111-
if req.DstIp != "" {
112-
filter = append(filter, bson.E{Key: "dst_ip", Value: req.DstIp})
113-
}
114-
115-
// Handle "dst_port"
116-
if req.DstPort != 0 {
117-
if req.DstPort == -1 {
118-
// Remove dst_ip
119-
for i, e := range filter {
120-
if e.Key == "dst_ip" {
121-
filter = append(filter[:i], filter[i+1:]...)
122-
break
123-
}
124-
}
125-
126-
// Exclude all service ports
127-
ninPorts := []int{}
128-
for _, svc := range api.Config.Services {
129-
if svc.Port != 0 {
130-
ninPorts = append(ninPorts, svc.Port)
131-
}
132-
}
133-
filter = append(filter, bson.E{Key: "dst_port", Value: bson.M{"$nin": ninPorts}})
134-
} else {
135-
filter = append(filter, bson.E{Key: "dst_port", Value: req.DstPort})
136-
}
137-
}
138-
139-
// Handle time range
140-
if req.FromTime != 0 && req.ToTime != 0 {
141-
filter = append(filter, bson.E{Key: "time", Value: bson.D{
142-
{Key: "$gte", Value: req.FromTime},
143-
{Key: "$lt", Value: req.ToTime},
144-
}})
145-
}
146-
147-
// Handle tags
148-
tagQueries := bson.M{}
149-
150-
if len(req.IncludeTags) > 0 {
151-
tagQueries["$all"] = req.IncludeTags
152-
}
153-
154-
if len(req.ExcludeTags) > 0 {
155-
tagQueries["$nin"] = req.ExcludeTags
156-
}
157-
158-
if len(tagQueries) > 0 {
159-
filter = append(filter, bson.E{Key: "tags", Value: tagQueries})
160-
}
161-
162-
type apiFlowEntry struct {
163-
Id primitive.ObjectID `json:"_id"` // MongoDB unique identifier
164-
SrcPort int `json:"src_port"` // Source port
165-
DstPort int `json:"dst_port"` // Destination port
166-
SrcIp string `json:"src_ip"` // Source IP address
167-
DstIp string `json:"dst_ip"` // Destination IP address
168-
Time int `json:"time"` // Timestamp (epoch)
169-
Duration int `json:"duration"` // Duration in milliseconds
170-
Num_packets int `json:"num_packets"` // Number of packets
171-
Blocked bool `json:"blocked"`
172-
Filename string `json:"filename"` // Name of the pcap file this flow was captured in
173-
Fingerprints []uint32 `json:"fingerprints"`
174-
Signatures []db.SuricataSig `json:"signatures"` // Signatures matched by this flow
175-
Flow []db.FlowItem `json:"flow"`
176-
Tags []string `json:"tags"` // Tags associated with this flow, e.g. "starred", "tcp", "udp", "blocked"
177-
Size int `json:"size"` // Size of the flow in bytes
178-
Flags []string `json:"flags"` // Flags contained in the flow
179-
Flagids []string `json:"flagids"` // Flag IDs associated with this flow
180-
}
181-
182-
// Convert bson.D filter to GetFlowsOptions
18393
opts := &db.FindFlowsOptions{
94+
FromTime: req.FromTime,
95+
ToTime: req.ToTime,
96+
IncludeTags: req.IncludeTags,
97+
ExcludeTags: req.ExcludeTags,
98+
DstIp: req.DstIp,
99+
FlowData: req.FlowData,
184100
Limit: req.Limit,
185101
Offset: req.Offset,
186102
Fingerprints: req.Fingerprints,
187103
}
188104

189-
// Set default limit if not specified
190105
if opts.Limit <= 0 {
191106
opts.Limit = 50
192107
}
193108

194-
// Parse filter to populate options
195-
for _, elem := range filter {
196-
switch elem.Key {
197-
case "dst_ip":
198-
if str, ok := elem.Value.(string); ok {
199-
opts.DstIp = str
200-
}
201-
case "dst_port":
202-
if port, ok := elem.Value.(int); ok {
203-
opts.DstPort = port
204-
}
205-
case "time":
206-
if timeRange, ok := elem.Value.(bson.D); ok {
207-
for _, timeElem := range timeRange {
208-
switch timeElem.Key {
209-
case "$gte":
210-
if val, ok := timeElem.Value.(int64); ok {
211-
opts.FromTime = val
212-
}
213-
case "$lt":
214-
if val, ok := timeElem.Value.(int64); ok {
215-
opts.ToTime = val
216-
}
217-
}
218-
}
219-
}
220-
case "tags":
221-
if tagQuery, ok := elem.Value.(bson.M); ok {
222-
if all, exists := tagQuery["$all"]; exists {
223-
if tags, ok := all.([]string); ok {
224-
opts.IncludeTags = tags
225-
}
226-
}
227-
if nin, exists := tagQuery["$nin"]; exists {
228-
if tags, ok := nin.([]string); ok {
229-
opts.ExcludeTags = tags
230-
}
231-
}
232-
}
233-
case "flow.data":
234-
if regexQuery, ok := elem.Value.(bson.M); ok {
235-
if regex, exists := regexQuery["$regex"]; exists {
236-
if str, ok := regex.(string); ok {
237-
opts.FlowData = str
238-
}
239-
}
109+
// DstPort == -1 means "exclude all known service ports" (show non-service traffic)
110+
if req.DstPort == -1 {
111+
excludePorts := make([]int, 0, len(api.Config.Services))
112+
for _, svc := range api.Config.Services {
113+
if svc.Port != 0 {
114+
excludePorts = append(excludePorts, svc.Port)
240115
}
241116
}
117+
opts.ExcludePorts = excludePorts
118+
} else if req.DstPort != 0 {
119+
opts.DstPort = req.DstPort
242120
}
243121

244-
slog.Info("Querying flows",
245-
slog.Any("filter", filter),
246-
slog.Any("options", opts),
247-
)
122+
type apiFlowEntry struct {
123+
db.FlowEntry
124+
Signatures []db.SuricataSig `json:"signatures"` // Signatures matched by this flow
125+
}
126+
127+
slog.Info("Querying flows", slog.Any("options", opts))
248128

249129
results, err := api.DB.GetFlows(c.Request().Context(), opts)
250130
if err != nil {
@@ -287,24 +167,7 @@ func (api *Router) query(c echo.Context) error {
287167

288168
apiResults := make([]apiFlowEntry, len(results))
289169
for i, flow := range results {
290-
res := apiFlowEntry{
291-
Id: flow.Id,
292-
SrcPort: flow.SrcPort,
293-
DstPort: flow.DstPort,
294-
SrcIp: flow.SrcIp,
295-
DstIp: flow.DstIp,
296-
Time: flow.Time,
297-
Duration: flow.Duration,
298-
Num_packets: flow.NumPackets,
299-
Blocked: flow.Blocked,
300-
Filename: flow.Filename,
301-
Fingerprints: flow.Fingerprints,
302-
Flow: flow.Flow,
303-
Tags: flow.Tags,
304-
Size: flow.Size,
305-
Flags: flow.Flags,
306-
Flagids: flow.Flagids,
307-
}
170+
res := apiFlowEntry{FlowEntry: flow}
308171

309172
res.Signatures = make([]db.SuricataSig, 0, len(flow.Suricata))
310173
for _, sigID := range flow.Suricata {

services/cmd/api/query_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ func (m *mockDB) GetFlows(ctx context.Context, opts *db.FindFlowsOptions) ([]db.
3333
func (m *mockDB) GetSignaturesBatch(ctx context.Context, ids []string) ([]db.SuricataSig, error) {
3434
return nil, nil
3535
}
36-
func (m *mockDB) InsertFlows(context.Context, []db.FlowEntry) error { return nil }
37-
func (m *mockDB) CountFlows(context.Context, bson.D) (int64, error) { return 0, nil }
36+
func (m *mockDB) InsertFlows(context.Context, []db.FlowEntry) error { return nil }
37+
func (m *mockDB) CountFlows(context.Context, bson.D) (int64, error) { return 0, nil }
3838
func (m *mockDB) CountFlowsByOpts(context.Context, *db.FindFlowsOptions) (int64, error) {
3939
return 0, nil
4040
}

services/pkg/db/db.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type FindFlowsOptions struct {
6969
IncludeTags []string
7070
ExcludeTags []string
7171
DstPort int
72+
ExcludePorts []int // Exclude flows matching any of these destination ports
7273
DstIp string
7374
SrcPort int
7475
SrcIp string

services/pkg/db/mongo.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ func buildFlowQuery(opts *FindFlowsOptions) bson.M {
440440

441441
if opts.DstPort > 0 {
442442
query["dst_port"] = opts.DstPort
443+
} else if len(opts.ExcludePorts) > 0 {
444+
query["dst_port"] = bson.M{"$nin": opts.ExcludePorts}
443445
}
444446
if opts.DstIp != "" {
445447
query["dst_ip"] = opts.DstIp

0 commit comments

Comments
 (0)