Try goqu as sql builder on listSoftwareDB (#2773)

* Try goqu as sql builder on listSoftwareDB

* Fix case insensitive search and add test
This commit is contained in:
Lucas Manuel Rodriguez
2021-11-04 15:21:39 -03:00
committed by GitHub
parent 21908f31cf
commit 837bfed167
5 changed files with 193 additions and 61 deletions
+2 -1
View File
@@ -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
+9
View File
@@ -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=
+44 -7
View File
@@ -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 ?")
+121 -48
View File
@@ -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 {
+17 -5
View File
@@ -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)
})
}