From 36e12d02e3c4e9dc741a7d5828ac74136f1f404d Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 16 Nov 2023 14:26:57 -0600 Subject: [PATCH] Show host display name in query results. (#15173) Query report now shows the host display name instead of hostname. #14763 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- ...763-show-host-display-name-in-query-report | 1 + server/datastore/mysql/query_results.go | 3 ++- server/fleet/hosts.go | 20 +++++++++++------- server/fleet/queries.go | 21 ++++++++++++++++--- server/fleet/queries_test.go | 13 ++++++------ server/service/integration_core_test.go | 9 ++++---- 6 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 changes/14763-show-host-display-name-in-query-report diff --git a/changes/14763-show-host-display-name-in-query-report b/changes/14763-show-host-display-name-in-query-report new file mode 100644 index 0000000000..0a9574165c --- /dev/null +++ b/changes/14763-show-host-display-name-in-query-report @@ -0,0 +1 @@ +Query report now shows the host display name instead of hostname. \ No newline at end of file diff --git a/server/datastore/mysql/query_results.go b/server/datastore/mysql/query_results.go index 61e56eea7a..87fa102776 100644 --- a/server/datastore/mysql/query_results.go +++ b/server/datastore/mysql/query_results.go @@ -91,7 +91,8 @@ func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet // (to avoid having to left join hosts). func (ds *Datastore) QueryResultRows(ctx context.Context, queryID uint) ([]*fleet.ScheduledQueryResultRow, error) { selectStmt := ` - SELECT qr.query_id, qr.host_id, COALESCE(h.hostname, '') as hostname, qr.last_fetched, qr.data + SELECT qr.query_id, qr.host_id, qr.last_fetched, qr.data, + h.hostname, h.computer_name, h.hardware_model, h.hardware_serial FROM query_results qr LEFT JOIN hosts h ON (qr.host_id=h.id) WHERE query_id = ? diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index db4f4bda7d..98e27544f0 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -632,23 +632,27 @@ func (h *Host) IsEligibleForBitLockerEncryption() bool { (needsEncryption || encryptedWithoutKey) } -// DisplayName returns ComputerName if it isn't empty. Otherwise, it returns Hostname if it isn't +// HostDisplayName returns ComputerName if it isn't empty. Otherwise, it returns Hostname if it isn't // empty. If Hostname is empty and both HardwareSerial and HardwareModel are not empty, it returns a // composite string with HardwareModel and HardwareSerial. If all else fails, it returns an empty // string. -func (h *Host) DisplayName() string { +func HostDisplayName(ComputerName string, Hostname string, HardwareModel string, HardwareSerial string) string { switch { - case h.ComputerName != "": - return h.ComputerName - case h.Hostname != "": - return h.Hostname - case h.HardwareModel != "" && h.HardwareSerial != "": - return fmt.Sprintf("%s (%s)", h.HardwareModel, h.HardwareSerial) + case ComputerName != "": + return ComputerName + case Hostname != "": + return Hostname + case HardwareModel != "" && HardwareSerial != "": + return fmt.Sprintf("%s (%s)", HardwareModel, HardwareSerial) default: return "" } } +func (h *Host) DisplayName() string { + return HostDisplayName(h.ComputerName, h.Hostname, h.HardwareModel, h.HardwareSerial) +} + type HostIssues struct { TotalIssuesCount int `json:"total_issues_count" db:"total_issues_count" csv:"issues"` // when exporting in CSV, we want that value as the "issues" column FailingPoliciesCount int `json:"failing_policies_count" db:"failing_policies_count" csv:"-"` diff --git a/server/fleet/queries.go b/server/fleet/queries.go index fc433aeed7..c422f588fb 100644 --- a/server/fleet/queries.go +++ b/server/fleet/queries.go @@ -1,6 +1,7 @@ package fleet import ( + "database/sql" "encoding/json" "errors" "fmt" @@ -380,7 +381,7 @@ func MapQueryReportResultsToRows(rows []*ScheduledQueryResultRow) ([]HostQueryRe } results = append(results, HostQueryResultRow{ HostID: row.HostID, - Hostname: row.Hostname, + Hostname: row.HostDisplayName(), LastFetched: row.LastFetched, Columns: columns, }) @@ -421,11 +422,25 @@ type ScheduledQueryResultRow struct { QueryID uint `db:"query_id"` // HostID is the unique identifier of the host. HostID uint `db:"host_id"` - // Hostname is the host's hostname. - Hostname string `db:"hostname"` + // Hostname is the host's hostname. NullString is used in case host does not exist. + Hostname sql.NullString `db:"hostname"` + // ComputerName is the host's computer_name. + ComputerName sql.NullString `db:"computer_name"` + // HardwareModel is the host's hardware_model. + HardwareModel sql.NullString `db:"hardware_model"` + // HardwareSerial is the host's hardware_serial. + HardwareSerial sql.NullString `db:"hardware_serial"` // Data holds a single result row. It holds a map where the map keys // are column names and map values are the values. Data json.RawMessage `db:"data"` // LastFetched is the time this result was received. LastFetched time.Time `db:"last_fetched"` } + +func (s *ScheduledQueryResultRow) HostDisplayName() string { + // If host does not exist, all values below default to empty string + return HostDisplayName( + s.ComputerName.String, s.Hostname.String, + s.HardwareModel.String, s.HardwareSerial.String, + ) +} diff --git a/server/fleet/queries_test.go b/server/fleet/queries_test.go index 32eb9e10c2..ec7b2558e0 100644 --- a/server/fleet/queries_test.go +++ b/server/fleet/queries_test.go @@ -1,6 +1,7 @@ package fleet import ( + "database/sql" "encoding/json" "testing" "time" @@ -228,7 +229,7 @@ func TestMapQueryReportResultRows(t *testing.T) { rows: []*ScheduledQueryResultRow{ { HostID: 1, - Hostname: "macOS host", + Hostname: sql.NullString{String: "macOS host", Valid: true}, LastFetched: macOSUSBDevicesLastFetched, Data: json.RawMessage(`{ "class": "9", @@ -247,7 +248,7 @@ func TestMapQueryReportResultRows(t *testing.T) { }, { HostID: 1, - Hostname: "macOS host", + Hostname: sql.NullString{String: "macOS host", Valid: true}, LastFetched: macOSUSBDevicesLastFetched, Data: json.RawMessage(`{ "class": "9", @@ -266,7 +267,7 @@ func TestMapQueryReportResultRows(t *testing.T) { }, { HostID: 2, - Hostname: "ubuntu host", + Hostname: sql.NullString{String: "ubuntu host", Valid: true}, LastFetched: ubuntuUSBDevicesLastFetched, Data: json.RawMessage(`{ "class": "9", @@ -350,7 +351,7 @@ func TestMapQueryReportResultRows(t *testing.T) { rows: []*ScheduledQueryResultRow{ { HostID: 1, - Hostname: "macOS host", + Hostname: sql.NullString{String: "macOS host", Valid: true}, LastFetched: macOSOsqueryInfoLastFetched, Data: json.RawMessage(`{ "build_distro": "10.14", @@ -396,7 +397,7 @@ func TestMapQueryReportResultRows(t *testing.T) { rows: []*ScheduledQueryResultRow{ { HostID: 3, - Hostname: "bar", + Hostname: sql.NullString{String: "bar", Valid: true}, LastFetched: time.Now(), Data: json.RawMessage(`invalid JSON`), }, @@ -408,7 +409,7 @@ func TestMapQueryReportResultRows(t *testing.T) { rows: []*ScheduledQueryResultRow{ { HostID: 3, - Hostname: "bar", + Hostname: sql.NullString{String: "bar", Valid: true}, LastFetched: time.Now(), Data: json.RawMessage(`{"foobar": 1}`), }, diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 4f974ab02a..f093193659 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -8163,6 +8163,7 @@ func (s *integrationTestSuite) TestQueryReports() { SeenTime: time.Now(), NodeKey: ptr.String("2"), UUID: "2", + ComputerName: "Foo Local2", Hostname: "foo.local2", OsqueryHostID: ptr.String("2"), PrimaryIP: "192.168.1.2", @@ -8339,7 +8340,7 @@ func (s *integrationTestSuite) TestQueryReports() { return gqrr.Results[i].Columns["usb_port"] < gqrr.Results[j].Columns["usb_port"] }) require.Equal(t, host2Team1.ID, gqrr.Results[0].HostID) - require.Equal(t, host2Team1.Hostname, gqrr.Results[0].Hostname) + require.Equal(t, host2Team1.DisplayName(), gqrr.Results[0].Hostname) require.NotZero(t, gqrr.Results[0].LastFetched) require.Equal(t, map[string]string{ "class": "239", @@ -8356,7 +8357,7 @@ func (s *integrationTestSuite) TestQueryReports() { "version": "0.19", }, gqrr.Results[0].Columns) require.Equal(t, host2Team1.ID, gqrr.Results[1].HostID) - require.Equal(t, host2Team1.Hostname, gqrr.Results[1].Hostname) + require.Equal(t, host2Team1.DisplayName(), gqrr.Results[1].Hostname) require.NotZero(t, gqrr.Results[1].LastFetched) require.Equal(t, map[string]string{ "class": "0", @@ -8383,7 +8384,7 @@ func (s *integrationTestSuite) TestQueryReports() { return gqrr.Results[i].Columns["version"] > gqrr.Results[j].Columns["version"] }) require.Equal(t, host1Global.ID, gqrr.Results[0].HostID) - require.Equal(t, host1Global.Hostname, gqrr.Results[0].Hostname) + require.Equal(t, host1Global.DisplayName(), gqrr.Results[0].Hostname) require.NotZero(t, gqrr.Results[0].LastFetched) require.Equal(t, map[string]string{ "build_distro": "centos7", @@ -8400,7 +8401,7 @@ func (s *integrationTestSuite) TestQueryReports() { "watcher": "3570", }, gqrr.Results[0].Columns) require.Equal(t, host2Team1.ID, gqrr.Results[1].HostID) - require.Equal(t, host2Team1.Hostname, gqrr.Results[1].Hostname) + require.Equal(t, host2Team1.DisplayName(), gqrr.Results[1].Hostname) require.NotZero(t, gqrr.Results[1].LastFetched) require.Equal(t, map[string]string{ "build_distro": "10.14",