From 837bfed16780ecb31fa12f8b77df23fcefcfb380 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Thu, 4 Nov 2021 15:21:39 -0300 Subject: [PATCH] Try `goqu` as sql builder on `listSoftwareDB` (#2773) * Try goqu as sql builder on listSoftwareDB * Fix case insensitive search and add test --- go.mod | 3 +- go.sum | 9 ++ server/datastore/mysql/mysql.go | 51 ++++++- server/datastore/mysql/software.go | 169 +++++++++++++++++------- server/datastore/mysql/software_test.go | 22 ++- 5 files changed, 193 insertions(+), 61 deletions(-) diff --git a/go.mod b/go.mod index 670027dcdd..9e0acdb33f 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/dgraph-io/badger/v2 v2.2007.2 github.com/dnaeon/go-vcr/v2 v2.0.1 + github.com/doug-martin/goqu/v9 v9.18.0 // indirect github.com/e-dard/netbug v0.0.0-20151029172837-e64d308a0b20 github.com/elazarl/go-bindata-assetfs v1.0.0 github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c // indirect @@ -28,7 +29,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-kit/kit v0.9.0 github.com/go-logfmt/logfmt v0.5.0 // indirect - github.com/go-sql-driver/mysql v1.5.0 + github.com/go-sql-driver/mysql v1.6.0 github.com/golang-jwt/jwt/v4 v4.0.0 github.com/gomodule/redigo v1.8.5 github.com/google/go-cmp v0.5.6 diff --git a/go.sum b/go.sum index c0d6947d46..de6601c80b 100644 --- a/go.sum +++ b/go.sum @@ -212,6 +212,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingajkin/go-header v0.4.2 h1:jEeSF4sdv8/3cT/WY8AgDHUoItNSoEZ7qg9dX7pc218= github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/badger/v2 v2.2007.2 h1:EjjK0KqwaFMlPin1ajhP943VPENHJdEz1KLIegjaI3k= github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= @@ -225,6 +226,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuW github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr/v2 v2.0.1 h1:KQnAAR6r4GbcJ71KfVxM7qhX85oVj3A0zWbuoxpbYcA= github.com/dnaeon/go-vcr/v2 v2.0.1/go.mod h1:bklL092gNVdADdsX/u2vDs4wGZ52NSgh7YNcZRSiArs= +github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY= +github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -299,6 +302,8 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.7.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -340,6 +345,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -640,6 +646,7 @@ github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+s github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -672,6 +679,7 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= @@ -1046,6 +1054,7 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index 0057d94583..2a4400da6f 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -15,6 +15,8 @@ import ( "github.com/VividCortex/mysqlerr" "github.com/WatchBeam/clock" "github.com/cenkalti/backoff/v4" + "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/datastore/mysql/migrations/data" "github.com/fleetdm/fleet/v4/server/datastore/mysql/migrations/tables" @@ -31,10 +33,8 @@ const ( mySQLTimestampFormat = "2006-01-02 15:04:05" // %Y/%m/%d %H:%M:%S ) -var ( - // Matches all non-word and '-' characters for replacement - columnCharsRegexp = regexp.MustCompile(`[^\w-]`) -) +// Matches all non-word and '-' characters for replacement +var columnCharsRegexp = regexp.MustCompile(`[^\w-]`) // dbReader is an interface that defines the methods required for reads. type dbReader interface { @@ -354,6 +354,38 @@ func sanitizeColumn(col string) string { return columnCharsRegexp.ReplaceAllString(col, "") } +// appendListOptionsToSelect will apply the given list options to ds and +// return the new select dataset. +// +// NOTE: This is a copy of appendListOptionsToSQL that uses the goqu package. +func appendListOptionsToSelect(ds *goqu.SelectDataset, opts fleet.ListOptions) *goqu.SelectDataset { + if opts.OrderKey != "" { + var orderedExp exp.OrderedExpression + ident := goqu.I(sanitizeColumn(opts.OrderKey)) + if opts.OrderDirection == fleet.OrderDescending { + orderedExp = ident.Desc() + } else { + orderedExp = ident.Asc() + } + ds = ds.Order(orderedExp) + } + + perPage := opts.PerPage + // If caller doesn't supply a limit apply a default limit of 1000 + // to insure that an unbounded query with many results doesn't consume too + // much memory or hang + if perPage == 0 { + perPage = defaultSelectLimit + } + ds = ds.Limit(perPage) + + offset := perPage * opts.Page + if offset > 0 { + ds = ds.Offset(offset) + } + return ds +} + func appendListOptionsToSQL(sql string, opts fleet.ListOptions) string { if opts.OrderKey != "" { direction := "ASC" @@ -545,6 +577,13 @@ func isChildForeignKeyError(err error) bool { return mysqlErr.Number == ER_NO_REFERENCED_ROW_2 } +// likePattern returns a pattern to match m with LIKE. +func likePattern(m string) string { + m = strings.Replace(m, "_", "\\_", -1) + m = strings.Replace(m, "%", "\\%", -1) + return "%" + m + "%" +} + // searchLike adds SQL and parameters for a "search" using LIKE syntax. // // The input columns must be sanitized if they are provided by the user. @@ -553,9 +592,7 @@ func searchLike(sql string, params []interface{}, match string, columns ...strin return sql, params } - match = strings.Replace(match, "_", "\\_", -1) - match = strings.Replace(match, "%", "\\%", -1) - pattern := "%" + match + "%" + pattern := likePattern(match) ors := make([]string, 0, len(columns)) for _, column := range columns { ors = append(ors, column+" LIKE ?") diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index 614461679e..0ff1a7b660 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/doug-martin/goqu/v9" + _ "github.com/doug-martin/goqu/v9/dialect/mysql" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/jmoiron/sqlx" "github.com/pkg/errors" @@ -200,58 +202,136 @@ func insertNewInstalledHostSoftwareDB( return nil } -func listSoftwareDB(ctx context.Context, q sqlx.QueryerContext, hostID *uint, opt fleet.SoftwareListOptions) ([]fleet.Software, error) { - hostWhere := `hs.host_id=?` - if hostID == nil { - hostWhere = "TRUE" +var dialect = goqu.Dialect("mysql") + +// listSoftwareDB returns all the software installed in the given hostID and list options. +// If hostID is nil, then the method will look into the installed software of all hosts. +func listSoftwareDB( + ctx context.Context, q sqlx.QueryerContext, hostID *uint, opts fleet.SoftwareListOptions, +) ([]fleet.Software, error) { + ds := dialect.From(goqu.I("host_software").As("hs")).SelectDistinct( + "s.*", + goqu.COALESCE(goqu.I("scp.cpe"), "").As("generated_cpe"), + ).Join( + goqu.I("hosts").As("h"), + goqu.On( + goqu.I("hs.host_id").Eq(goqu.I("h.id")), + ), + ).Join( + goqu.I("software").As("s"), + goqu.On( + goqu.I("hs.software_id").Eq(goqu.I("s.id")), + ), + ) + + if hostID != nil { + ds = ds.Where(goqu.I("hs.host_id").Eq(hostID)) } - teamWhere := `h.team_id=?` - if opt.TeamID == nil { - teamWhere = "TRUE" + if opts.TeamID != nil { + ds = ds.Where(goqu.I("h.team_id").Eq(opts.TeamID)) } - vulnerableJoin := "LEFT JOIN software_cpe scp ON (s.id=scp.software_id)" - if opt.VulnerableOnly { - vulnerableJoin = `JOIN software_cpe scp ON (s.id=scp.software_id) - JOIN software_cve scv ON (scp.id=scv.cpe_id)` + + if match := opts.MatchQuery; match != "" { + match = likePattern(match) + ds = ds.Where( + goqu.Or( + goqu.I("s.name").ILike(match), + goqu.I("s.version").ILike(match), + ), + ) + } + + ds = ds.GroupBy( + goqu.I("s.id"), + goqu.I("s.name"), + goqu.I("s.version"), + goqu.I("s.source"), + goqu.I("generated_cpe"), + ) + + ds = appendListOptionsToSelect(ds, opts.ListOptions) + + if opts.VulnerableOnly { + ds = ds.Join( + goqu.I("software_cpe").As("scp"), + goqu.On( + goqu.I("s.id").Eq(goqu.I("scp.software_id")), + ), + ).Join( + goqu.I("software_cve").As("scv"), + goqu.On(goqu.I("scp.id").Eq(goqu.I("scv.cpe_id"))), + ) + } else { + ds = ds.LeftJoin( + goqu.I("software_cpe").As("scp"), + goqu.On( + goqu.I("s.id").Eq(goqu.I("scp.software_id")), + ), + ) + } + + sql, args, err := ds.ToSQL() + if err != nil { + return nil, errors.Wrap(err, "sql build") } - sql := fmt.Sprintf(` - SELECT DISTINCT s.*, coalesce(scp.cpe, "") as generated_cpe - FROM host_software hs - JOIN hosts h ON (hs.host_id=h.id) - JOIN software s ON (hs.software_id=s.id) - %s - WHERE %s AND %s - `, vulnerableJoin, hostWhere, teamWhere) var result []fleet.Software - vars := []interface{}{} - if hostID != nil { - vars = append(vars, hostID) - } - if opt.TeamID != nil { - vars = append(vars, opt.TeamID) - } - sql, listVars := searchLike(sql, vars, opt.MatchQuery, "s.name", "s.version") - sql += ` GROUP BY s.id, s.name, s.version, s.source, generated_cpe ` - sql = appendListOptionsToSQL(sql, opt.ListOptions) - if err := sqlx.SelectContext(ctx, q, &result, sql, listVars...); err != nil { + if err := sqlx.SelectContext(ctx, q, &result, sql, args...); err != nil { return nil, errors.Wrap(err, "load host software") } - if opt.SkipLoadingCVEs { + if opts.SkipLoadingCVEs { return result, nil } - sql = fmt.Sprintf(` - SELECT DISTINCT hs.software_id, scv.cve - FROM host_software hs - JOIN hosts h ON (hs.host_id=h.id) - JOIN software_cpe scp ON (hs.software_id=scp.software_id) - JOIN software_cve scv ON (scp.id=scv.cpe_id) - WHERE %s AND %s - `, hostWhere, teamWhere) + cvesBySoftware, err := loadCVEsBySoftware(ctx, q, hostID, opts) + if err != nil { + return nil, errors.Wrap(err, "load CVEs by software") + } + for i := range result { + result[i].Vulnerabilities = cvesBySoftware[result[i].ID] + } + return result, nil +} - rows, err := q.QueryxContext(ctx, sql, vars...) +// loadCVEsbySoftware loads all the CVEs on software installed on the given hostID and list options. +// If hostID is nil, then the method will look into the installed software of all hosts. +func loadCVEsBySoftware( + ctx context.Context, q sqlx.QueryerContext, hostID *uint, opt fleet.SoftwareListOptions, +) (map[uint]fleet.VulnerabilitiesSlice, error) { + ds := dialect.From(goqu.I("host_software").As("hs")).SelectDistinct( + goqu.I("hs.software_id"), + goqu.I("scv.cve"), + ).Join( + goqu.I("hosts").As("h"), + goqu.On( + goqu.I("hs.host_id").Eq(goqu.I("h.id")), + ), + ).Join( + goqu.I("software_cpe").As("scp"), + goqu.On( + goqu.I("hs.software_id").Eq(goqu.I("scp.software_id")), + ), + ).Join( + goqu.I("software_cve").As("scv"), + goqu.On( + goqu.I("scp.id").Eq(goqu.I("scv.cpe_id")), + ), + ) + + if hostID != nil { + ds = ds.Where(goqu.I("hs.host_id").Eq(hostID)) + } + if opt.TeamID != nil { + ds = ds.Where(goqu.I("h.team_id").Eq(opt.TeamID)) + } + + sql, args, err := ds.ToSQL() + if err != nil { + return nil, errors.Wrap(err, "sql2 build") + } + + rows, err := q.QueryxContext(ctx, sql, args...) if err != nil { return nil, errors.Wrap(err, "load host software") } @@ -272,14 +352,7 @@ func listSoftwareDB(ctx context.Context, q sqlx.QueryerContext, hostID *uint, op if err := rows.Err(); err != nil { return nil, errors.Wrap(err, "error iterating through cve rows") } - - var resultWithCVEs []fleet.Software - for _, software := range result { - software.Vulnerabilities = cvesBySoftware[software.ID] - resultWithCVEs = append(resultWithCVEs, software) - } - - return resultWithCVEs, nil + return cvesBySoftware, nil } func (d *Datastore) LoadHostSoftware(ctx context.Context, host *fleet.Host) error { diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index a960f75115..9d4ec03383 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -425,7 +425,7 @@ func testSoftwareList(t *testing.T, ds *Datastore) { soft2 := fleet.HostSoftware{ Modified: true, Software: []fleet.Software{ - {Name: "foo", Version: "0.0.2", Source: "chrome_extensions"}, + {Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"}, {Name: "foo", Version: "0.0.3", Source: "chrome_extensions"}, {Name: "bar", Version: "0.0.3", Source: "deb_packages"}, }, @@ -452,7 +452,7 @@ func testSoftwareList(t *testing.T, ds *Datastore) { {CVE: "cve-333-444-555", DetailsLink: "https://nvd.nist.gov/vuln/detail/cve-333-444-555"}, }, } - foo002 := fleet.Software{Name: "foo", Version: "0.0.2", Source: "chrome_extensions"} + foo002 := fleet.Software{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"} foo003 := fleet.Software{Name: "foo", Version: "0.0.3", Source: "chrome_extensions", GenerateCPE: "someothercpewithoutvulns"} bar003 := fleet.Software{Name: "bar", Version: "0.0.3", Source: "deb_packages"} @@ -479,7 +479,7 @@ func testSoftwareList(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Len(t, software, 1) - expected := []fleet.Software{foo002} + expected := []fleet.Software{foo003} test.ElementsMatchSkipID(t, software, expected) }) @@ -519,11 +519,23 @@ func testSoftwareList(t *testing.T, ds *Datastore) { }) t.Run("filters by query", func(t *testing.T) { - software, err := ds.ListSoftware(context.Background(), fleet.SoftwareListOptions{ListOptions: fleet.ListOptions{MatchQuery: "bar"}}) + // query by name (case insensitive) + software, err := ds.ListSoftware(context.Background(), fleet.SoftwareListOptions{ListOptions: fleet.ListOptions{MatchQuery: "baR"}}) require.NoError(t, err) - require.Len(t, software, 1) expected := []fleet.Software{bar003} test.ElementsMatchSkipID(t, software, expected) + // query by version + software, err = ds.ListSoftware(context.Background(), fleet.SoftwareListOptions{ListOptions: fleet.ListOptions{MatchQuery: "0.0.3"}}) + require.NoError(t, err) + require.Len(t, software, 2) + expected = []fleet.Software{foo003, bar003} + test.ElementsMatchSkipID(t, software, expected) + // query by version (case insensitive) + software, err = ds.ListSoftware(context.Background(), fleet.SoftwareListOptions{ListOptions: fleet.ListOptions{MatchQuery: "V0.0.2"}}) + require.NoError(t, err) + require.Len(t, software, 1) + expected = []fleet.Software{foo002} + test.ElementsMatchSkipID(t, software, expected) }) }