diff --git a/changes/15522-query-host-search b/changes/15522-query-host-search new file mode 100644 index 0000000000..88659edca9 --- /dev/null +++ b/changes/15522-query-host-search @@ -0,0 +1 @@ +- Fixes bug in searching for hosts by email addresses. \ No newline at end of file diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 54419a2548..6c78a794ee 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -2369,7 +2369,7 @@ func (ds *Datastore) SearchHosts(ctx context.Context, filter fleet.TeamFilter, m if len(matchQuery) > 0 { // first we'll find the hosts that match the search criteria, to keep thing simple, then we'll query again // to get all the additional data for hosts that match the search criteria by host_id - matchingHosts := "SELECT id FROM hosts WHERE TRUE" + matchingHosts := "SELECT h.id FROM hosts h WHERE TRUE" var args []interface{} // TODO: should search columns include display_name (requires join to host_display_names)? searchHostsQuery, args, matchesEmail := hostSearchLike(matchingHosts, args, matchQuery, hostSearchColumns...) diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index f50fce3e42..44b15e0e3c 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -1763,6 +1763,15 @@ func testHostsSearch(t *testing.T, ds *Datastore) { require.NoError(t, err) assert.Len(t, hits, 3) assert.Equal(t, []uint{h3.ID, h2.ID, h1.ID}, []uint{hits[0].ID, hits[1].ID, hits[2].ID}) + + // Add email to mapping table + _, err = ds.writer(context.Background()).ExecContext(context.Background(), `INSERT INTO host_emails (host_id, email, source) VALUES (?, ?, ?)`, + hosts[0].ID, "a@b.c", "src1") + require.NoError(t, err) + // Verify search works + hits, err = ds.SearchHosts(context.Background(), filter, "a@b.c") + require.NoError(t, err) + assert.Len(t, hits, 1) } func testSearchHostsWildCards(t *testing.T, ds *Datastore) { diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index c1f08b6ac2..78975dbf4d 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -1118,6 +1118,8 @@ var ( nonacsiiReplace = regexp.MustCompile(`[^[:ascii:]]`) ) +// hostSearchLike searches hosts based on the given columns plus searching in hosts_emails. Note: +// the host from the `hosts` table must be aliased to `h` in `sql`. func hostSearchLike(sql string, params []interface{}, match string, columns ...string) (string, []interface{}, bool) { var matchesEmail bool base, args := searchLike(sql, params, match, columns...) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index fb5ef658f6..00a4b1d4ee 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -6232,6 +6232,22 @@ func (s *integrationTestSuite) TestSearchHosts() { s.DoJSON("POST", "/api/latest/fleet/hosts/search", searchHostsRequest{MatchQuery: "foo.local0"}, http.StatusOK, &searchResp) require.Len(t, searchResp.Hosts, 1) require.Greater(t, searchResp.Hosts[0].SoftwareUpdatedAt, searchResp.Hosts[0].CreatedAt) + + mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error { + _, err := db.ExecContext( + context.Background(), + `INSERT INTO host_emails (host_id, email, source) VALUES (?, ?, ?)`, + hosts[0].ID, "a@b.c", "src1") + + return err + }) + + s.DoJSON("POST", "/api/latest/fleet/hosts/search", searchHostsRequest{MatchQuery: "a@b.c"}, http.StatusOK, &searchResp) + require.Len(t, searchResp.Hosts, 1) + + // search for non-existent email, shouldn't get anything back + s.DoJSON("POST", "/api/latest/fleet/hosts/search", searchHostsRequest{MatchQuery: "not@found.com"}, http.StatusOK, &searchResp) + require.Len(t, searchResp.Hosts, 0) } func (s *integrationTestSuite) TestCountTargets() {