diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 69ea327ab6..9b684327d6 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -123,7 +123,7 @@ func printSecret(c *cli.Context, secret *fleet.EnrollSecretSpec) error { return printSpec(c, spec) } -func printHost(c *cli.Context, host *service.HostResponse) error { +func printHost(c *cli.Context, host *fleet.HostResponse) error { spec := specGeneric{ Kind: fleet.HostKind, Version: fleet.ApiVersion, diff --git a/cmd/fleetctl/package_test.go b/cmd/fleetctl/package_test.go index d2258da930..b6693e62b5 100644 --- a/cmd/fleetctl/package_test.go +++ b/cmd/fleetctl/package_test.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" "runtime" @@ -35,12 +34,12 @@ func TestPackage(t *testing.T) { // Test invalid PEM file provided in --fleet-certificate. certDir := t.TempDir() fleetCertificate := filepath.Join(certDir, "fleet.pem") - err = ioutil.WriteFile(fleetCertificate, []byte("undefined"), os.FileMode(0o644)) + err = os.WriteFile(fleetCertificate, []byte("undefined"), os.FileMode(0o644)) require.NoError(t, err) runAppCheckErr(t, []string{"package", "--type=deb", fmt.Sprintf("--fleet-certificate=%s", fleetCertificate)}, fmt.Sprintf("failed to read certificate %q: invalid PEM file", fleetCertificate)) if runtime.GOOS != "linux" { - runAppCheckErr(t, []string{"package", "--type=msi", "--native-tooling"}, "native on non-linux platforms fails") + runAppCheckErr(t, []string{"package", "--type=msi", "--native-tooling"}, "native tooling is only available in Linux") } t.Run("deb", func(t *testing.T) { diff --git a/cmd/fleetctl/query_test.go b/cmd/fleetctl/query_test.go index b752acf53b..a9618c6042 100644 --- a/cmd/fleetctl/query_test.go +++ b/cmd/fleetctl/query_test.go @@ -99,7 +99,7 @@ func TestLiveQuery(t *testing.T) { fleet.DistributedQueryResult{ DistributedQueryCampaignID: 321, Rows: []map[string]string{{"bing": "fds"}}, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 99, UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ UpdateTimestamp: fleet.UpdateTimestamp{ @@ -108,7 +108,7 @@ func TestLiveQuery(t *testing.T) { }, DetailUpdatedAt: time.Now().UTC(), Hostname: "somehostname", - }, + }), }, )) }() diff --git a/server/fleet/campaigns.go b/server/fleet/campaigns.go index 4653ba2cb2..b393f8d07d 100644 --- a/server/fleet/campaigns.go +++ b/server/fleet/campaigns.go @@ -35,7 +35,7 @@ type DistributedQueryCampaignTarget struct { // distributed query on a single host. type DistributedQueryResult struct { DistributedQueryCampaignID uint `json:"distributed_query_execution_id"` - Host Host `json:"host"` + Host *HostResponse `json:"host"` Rows []map[string]string `json:"rows"` // osquery currently doesn't return any helpful error information, // but we use string here instead of bool for future-proofing. Note also diff --git a/server/fleet/hostresponse.go b/server/fleet/hostresponse.go new file mode 100644 index 0000000000..cc5aa163dd --- /dev/null +++ b/server/fleet/hostresponse.go @@ -0,0 +1,46 @@ +package fleet + +import ( + "context" + "time" +) + +// HostResponse is the response struct that contains the full host information +// along with the host online status and the "display text" to be used when +// rendering in the UI. +type HostResponse struct { + *Host + Status HostStatus `json:"status" csv:"status"` + DisplayText string `json:"display_text" csv:"display_text"` + DisplayName string `json:"display_name" csv:"display_name"` + Labels []Label `json:"labels,omitempty" csv:"-"` + Geolocation *GeoLocation `json:"geolocation,omitempty" csv:"-"` + CSVDeviceMapping string `json:"-" db:"-" csv:"device_mapping"` +} + +// HostResponseForHost returns a HostResponse from Host with Geolocation. +func HostResponseForHost(ctx context.Context, svc Service, host *Host) (*HostResponse, error) { + hr := HostResponseForHostCheap(host) + hr.Geolocation = svc.LookupGeoIP(ctx, host.PublicIP) + return hr, nil +} + +// HostResponseForHostCheap returns a new HostResponse from a Host without computing Geolocation. +func HostResponseForHostCheap(host *Host) *HostResponse { + return &HostResponse{ + Host: host, + Status: host.Status(time.Now()), + DisplayText: host.Hostname, + DisplayName: host.DisplayName(), + } +} + +// HostResponsesForHostsCheap returns a HostResponses from Hosts without computing Geolocation. +func HostResponsesForHostsCheap(hosts []Host) []HostResponse { + hrs := make([]HostResponse, len(hosts)) + for i, h := range hosts { + h := h + hrs[i] = *HostResponseForHostCheap(&h) + } + return hrs +} diff --git a/server/fleet/teams.go b/server/fleet/teams.go index 6845538721..fd2575a657 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -65,7 +65,7 @@ func (t Team) MarshalJSON() ([]byte, error) { UserCount int `json:"user_count"` Users []TeamUser `json:"users,omitempty"` HostCount int `json:"host_count"` - Hosts []Host `json:"hosts,omitempty"` + Hosts []HostResponse `json:"hosts,omitempty"` Secrets []*EnrollSecret `json:"secrets,omitempty"` }{ ID: t.ID, @@ -76,7 +76,7 @@ func (t Team) MarshalJSON() ([]byte, error) { UserCount: t.UserCount, Users: t.Users, HostCount: t.HostCount, - Hosts: t.Hosts, + Hosts: HostResponsesForHostsCheap(t.Hosts), Secrets: t.Secrets, } diff --git a/server/fleet/users.go b/server/fleet/users.go index 3e83a92418..26cf12fdfb 100644 --- a/server/fleet/users.go +++ b/server/fleet/users.go @@ -60,7 +60,7 @@ func (u UserTeam) MarshalJSON() ([]byte, error) { UserCount int `json:"user_count"` Users []TeamUser `json:"users,omitempty"` HostCount int `json:"host_count"` - Hosts []Host `json:"hosts,omitempty"` + Hosts []HostResponse `json:"hosts,omitempty"` Secrets []*EnrollSecret `json:"secrets,omitempty"` Role string `json:"role"` }{ @@ -72,7 +72,7 @@ func (u UserTeam) MarshalJSON() ([]byte, error) { UserCount: u.UserCount, Users: u.Users, HostCount: u.HostCount, - Hosts: u.Hosts, + Hosts: HostResponsesForHostsCheap(u.Hosts), Secrets: u.Secrets, Role: u.Role, } diff --git a/server/pubsub/query_results_test.go b/server/pubsub/query_results_test.go index d9691ea3a5..5102804e92 100644 --- a/server/pubsub/query_results_test.go +++ b/server/pubsub/query_results_test.go @@ -34,7 +34,7 @@ func TestQueryResultsStoreErrors(t *testing.T) { result := fleet.DistributedQueryResult{ DistributedQueryCampaignID: 9999, Rows: []map[string]string{{"bing": "fds"}}, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 4, UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ UpdateTimestamp: fleet.UpdateTimestamp{ @@ -42,7 +42,7 @@ func TestQueryResultsStoreErrors(t *testing.T) { }, }, DetailUpdatedAt: time.Now().UTC(), - }, + }), } // Write with no subscriber @@ -108,7 +108,7 @@ func TestQueryResultsStore(t *testing.T) { { DistributedQueryCampaignID: 1, Rows: []map[string]string{{"foo": "bar"}}, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 1, // Note these times need to be set to avoid // issues with roundtrip serializing the zero @@ -124,12 +124,12 @@ func TestQueryResultsStore(t *testing.T) { DetailUpdatedAt: time.Now().UTC(), SeenTime: time.Now().UTC(), - }, + }), }, { DistributedQueryCampaignID: 1, Rows: []map[string]string{{"whoo": "wahh"}}, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 3, UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ UpdateTimestamp: fleet.UpdateTimestamp{ @@ -142,12 +142,12 @@ func TestQueryResultsStore(t *testing.T) { DetailUpdatedAt: time.Now().UTC(), SeenTime: time.Now().UTC(), - }, + }), }, { DistributedQueryCampaignID: 1, Rows: []map[string]string{{"bing": "fds"}}, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 4, UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ UpdateTimestamp: fleet.UpdateTimestamp{ @@ -160,7 +160,7 @@ func TestQueryResultsStore(t *testing.T) { DetailUpdatedAt: time.Now().UTC(), SeenTime: time.Now().UTC(), - }, + }), }, } @@ -174,7 +174,7 @@ func TestQueryResultsStore(t *testing.T) { { DistributedQueryCampaignID: 2, Rows: []map[string]string{{"tim": "tom"}}, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 1, UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ UpdateTimestamp: fleet.UpdateTimestamp{ @@ -187,12 +187,12 @@ func TestQueryResultsStore(t *testing.T) { DetailUpdatedAt: time.Now().UTC(), SeenTime: time.Now().UTC(), - }, + }), }, { DistributedQueryCampaignID: 2, Rows: []map[string]string{{"slim": "slam"}}, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 3, UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ UpdateTimestamp: fleet.UpdateTimestamp{ @@ -205,7 +205,7 @@ func TestQueryResultsStore(t *testing.T) { DetailUpdatedAt: time.Now().UTC(), SeenTime: time.Now().UTC(), - }, + }), }, } diff --git a/server/service/client_hosts.go b/server/service/client_hosts.go index 2e8fa245e2..b4cdb5103c 100644 --- a/server/service/client_hosts.go +++ b/server/service/client_hosts.go @@ -8,7 +8,7 @@ import ( ) // GetHosts retrieves the list of all Hosts -func (c *Client) GetHosts(query string) ([]HostResponse, error) { +func (c *Client) GetHosts(query string) ([]fleet.HostResponse, error) { verb, path := "GET", "/api/latest/fleet/hosts" var responseBody listHostsResponse err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query) diff --git a/server/service/client_live_query_test.go b/server/service/client_live_query_test.go index b52beeaffc..13d71f1e4d 100644 --- a/server/service/client_live_query_test.go +++ b/server/service/client_live_query_test.go @@ -64,10 +64,10 @@ func TestLiveQueryWithContext(t *testing.T) { Type: "result", Data: fleet.DistributedQueryResult{ DistributedQueryCampaignID: 99, - Host: fleet.Host{ + Host: fleet.HostResponseForHostCheap(&fleet.Host{ ID: 23, Hostname: "somehostaaa", - }, + }), Rows: []map[string]string{ { "col1": "aaa", diff --git a/server/service/client_targets.go b/server/service/client_targets.go index b32ddb4f26..16c124831c 100644 --- a/server/service/client_targets.go +++ b/server/service/client_targets.go @@ -25,7 +25,7 @@ func (c *Client) SearchTargets(query string, hostIDs, labelIDs []uint) (*fleet.T hosts := make([]*fleet.Host, len(responseBody.Targets.Hosts)) for i, h := range responseBody.Targets.Hosts { - hosts[i] = h.HostResponse.Host + hosts[i] = h.Host } labels := make([]*fleet.Label, len(responseBody.Targets.Labels)) diff --git a/server/service/hosts.go b/server/service/hosts.go index f2e8c30794..8e2d5457c8 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -19,29 +19,6 @@ import ( "github.com/gocarina/gocsv" ) -// HostResponse is the response struct that contains the full host information -// along with the host online status and the "display text" to be used when -// rendering in the UI. -type HostResponse struct { - *fleet.Host - Status fleet.HostStatus `json:"status" csv:"status"` - DisplayText string `json:"display_text" csv:"display_text"` - DisplayName string `json:"display_name" csv:"display_name"` - Labels []fleet.Label `json:"labels,omitempty" csv:"-"` - Geolocation *fleet.GeoLocation `json:"geolocation,omitempty" csv:"-"` - CSVDeviceMapping string `json:"-" db:"-" csv:"device_mapping"` -} - -func hostResponseForHost(ctx context.Context, svc fleet.Service, host *fleet.Host) (*HostResponse, error) { - return &HostResponse{ - Host: host, - Status: host.Status(time.Now()), - DisplayText: host.Hostname, - DisplayName: host.DisplayName(), - Geolocation: svc.LookupGeoIP(ctx, host.PublicIP), - }, nil -} - // HostDetailResponse is the response struct that contains the full host information // with the HostDetail details. type HostDetailResponse struct { @@ -71,8 +48,8 @@ type listHostsRequest struct { } type listHostsResponse struct { - Hosts []HostResponse `json:"hosts"` - Software *fleet.Software `json:"software,omitempty"` + Hosts []fleet.HostResponse `json:"hosts"` + Software *fleet.Software `json:"software,omitempty"` // MDMSolution is populated with the MDM solution corresponding to the mdm_id // filter if one is provided with the request (and it exists in the // database). It is nil otherwise and absent of the JSON response payload. @@ -123,9 +100,9 @@ func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Servi return listHostsResponse{Err: err}, nil } - hostResponses := make([]HostResponse, len(hosts)) + hostResponses := make([]fleet.HostResponse, len(hosts)) for i, host := range hosts { - h, err := hostResponseForHost(ctx, svc, host) + h, err := fleet.HostResponseForHost(ctx, svc, host) if err != nil { return listHostsResponse{Err: err}, nil } @@ -317,8 +294,8 @@ type searchHostsRequest struct { } type searchHostsResponse struct { - Hosts []*hostSearchResult `json:"hosts"` - Err error `json:"error,omitempty"` + Hosts []*fleet.HostResponse `json:"hosts"` + Err error `json:"error,omitempty"` } func (r searchHostsResponse) error() error { return r.Err } @@ -331,18 +308,10 @@ func searchHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Ser return searchHostsResponse{Err: err}, nil } - results := []*hostSearchResult{} + results := []*fleet.HostResponse{} for _, h := range hosts { - results = append(results, - &hostSearchResult{ - HostResponse{ - Host: h, - Status: h.Status(time.Now()), - }, - h.Hostname, - }, - ) + results = append(results, fleet.HostResponseForHostCheap(h)) } return searchHostsResponse{ @@ -1085,9 +1054,9 @@ type hostsReportRequest struct { } type hostsReportResponse struct { - Columns []string `json:"-"` // used to control the generated csv, see the hijackRender method - Hosts []*HostResponse `json:"-"` // they get rendered explicitly, in csv - Err error `json:"error,omitempty"` + Columns []string `json:"-"` // used to control the generated csv, see the hijackRender method + Hosts []*fleet.HostResponse `json:"-"` // they get rendered explicitly, in csv + Err error `json:"error,omitempty"` } func (r hostsReportResponse) error() error { return r.Err } @@ -1228,9 +1197,9 @@ func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Ser return hostsReportResponse{Err: err}, nil } - hostResps := make([]*HostResponse, len(hosts)) + hostResps := make([]*fleet.HostResponse, len(hosts)) for i, h := range hosts { - hr, err := hostResponseForHost(ctx, svc, h) + hr, err := fleet.HostResponseForHost(ctx, svc, h) if err != nil { return hostsReportResponse{Err: err}, nil } diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 01acb103a8..12a36e6040 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -2427,7 +2427,7 @@ func (s *integrationTestSuite) TestHostDeviceMapping() { // list hosts response includes device mappings s.DoJSON("GET", "/api/latest/fleet/hosts?device_mapping=true", nil, http.StatusOK, &listHosts) require.Len(t, listHosts.Hosts, 3) - hostsByID := make(map[uint]HostResponse) + hostsByID := make(map[uint]fleet.HostResponse) for _, h := range listHosts.Hosts { hostsByID[h.ID] = h } @@ -2493,7 +2493,7 @@ func (s *integrationTestSuite) TestListHostsDeviceMappingSize() { var listHosts listHostsResponse s.DoJSON("GET", "/api/latest/fleet/hosts?device_mapping=true", nil, http.StatusOK, &listHosts) - hostsByID := make(map[uint]HostResponse) + hostsByID := make(map[uint]fleet.HostResponse) for _, h := range listHosts.Hosts { hostsByID[h.ID] = h } diff --git a/server/service/labels.go b/server/service/labels.go index 6b80baf9d2..071439d5ce 100644 --- a/server/service/labels.go +++ b/server/service/labels.go @@ -259,9 +259,9 @@ func listHostsInLabelEndpoint(ctx context.Context, request interface{}, svc flee return listLabelsResponse{Err: err}, nil } - hostResponses := make([]HostResponse, len(hosts)) + hostResponses := make([]fleet.HostResponse, len(hosts)) for i, host := range hosts { - h, err := hostResponseForHost(ctx, svc, host) + h, err := fleet.HostResponseForHost(ctx, svc, host) if err != nil { return listHostsResponse{Err: err}, nil } diff --git a/server/service/osquery.go b/server/service/osquery.go index f62ed2aed6..6cc69c8dd3 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -981,7 +981,7 @@ func (svc *Service) ingestDistributedQuery(ctx context.Context, host fleet.Host, // Write the results to the pubsub store res := fleet.DistributedQueryResult{ DistributedQueryCampaignID: uint(campaignID), - Host: host, + Host: fleet.HostResponseForHostCheap(&host), Rows: rows, } if failed { diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index fee8121cdc..7cbc22bf6f 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -1555,7 +1555,7 @@ func TestDistributedQueryResults(t *testing.T) { if res, ok := val.(fleet.DistributedQueryResult); ok { assert.Equal(t, campaign.ID, res.DistributedQueryCampaignID) assert.Equal(t, expectedRows, res.Rows) - assert.Equal(t, *host, res.Host) + assert.Equal(t, host, res.Host.Host) } else { t.Error("Wrong result type") } diff --git a/server/service/targets.go b/server/service/targets.go index 13bc9eab2c..ff07ef716e 100644 --- a/server/service/targets.go +++ b/server/service/targets.go @@ -25,11 +25,6 @@ type searchTargetsRequest struct { Selected fleet.HostTargets `json:"selected"` } -type hostSearchResult struct { - HostResponse - DisplayText string `json:"display_text"` -} - type labelSearchResult struct { *fleet.Label DisplayText string `json:"display_text"` @@ -52,7 +47,7 @@ func (t teamSearchResult) MarshalJSON() ([]byte, error) { UserCount int `json:"user_count"` Users []fleet.TeamUser `json:"users,omitempty"` HostCount int `json:"host_count"` - Hosts []fleet.Host `json:"hosts,omitempty"` + Hosts []fleet.HostResponse `json:"hosts,omitempty"` Secrets []*fleet.EnrollSecret `json:"secrets,omitempty"` DisplayText string `json:"display_text"` Count int `json:"count"` @@ -65,7 +60,7 @@ func (t teamSearchResult) MarshalJSON() ([]byte, error) { UserCount: t.UserCount, Users: t.Users, HostCount: t.HostCount, - Hosts: t.Hosts, + Hosts: fleet.HostResponsesForHostsCheap(t.Hosts), Secrets: t.Secrets, DisplayText: t.DisplayText, Count: t.Count, @@ -115,9 +110,9 @@ func (t *teamSearchResult) UnmarshalJSON(b []byte) error { } type targetsData struct { - Hosts []hostSearchResult `json:"hosts"` - Labels []labelSearchResult `json:"labels"` - Teams []teamSearchResult `json:"teams"` + Hosts []*fleet.HostResponse `json:"hosts"` + Labels []labelSearchResult `json:"labels"` + Teams []teamSearchResult `json:"teams"` } type searchTargetsResponse struct { @@ -140,21 +135,13 @@ func searchTargetsEndpoint(ctx context.Context, request interface{}, svc fleet.S } targets := &targetsData{ - Hosts: []hostSearchResult{}, + Hosts: []*fleet.HostResponse{}, Labels: []labelSearchResult{}, Teams: []teamSearchResult{}, } for _, host := range results.Hosts { - targets.Hosts = append(targets.Hosts, - hostSearchResult{ - HostResponse{ - Host: host, - Status: host.Status(time.Now()), - }, - host.Hostname, - }, - ) + targets.Hosts = append(targets.Hosts, fleet.HostResponseForHostCheap(host)) } for _, label := range results.Labels { diff --git a/server/webhooks/mapper.go b/server/webhooks/mapper.go index 63dee49d1e..7ea5c9ff80 100644 --- a/server/webhooks/mapper.go +++ b/server/webhooks/mapper.go @@ -16,9 +16,10 @@ type VulnMapper interface { } type hostPayloadPart struct { - ID uint `json:"id"` - Hostname string `json:"hostname"` - URL string `json:"url"` + ID uint `json:"id"` + Hostname string `json:"hostname"` + DisplayName string `json:"display_name"` + URL string `json:"url"` } type WebhookPayload struct { @@ -45,9 +46,10 @@ func (m *Mapper) getHostPayloadPart( hostURL := *hostBaseURL hostURL.Path = path.Join(hostURL.Path, "hosts", strconv.Itoa(int(h.ID))) shortHosts[i] = &hostPayloadPart{ - ID: h.ID, - Hostname: h.Hostname, - URL: hostURL.String(), + ID: h.ID, + Hostname: h.Hostname, + DisplayName: h.DisplayName, + URL: hostURL.String(), } } return shortHosts diff --git a/server/webhooks/vulnerabilities_test.go b/server/webhooks/vulnerabilities_test.go index b79dc98601..2d3641e097 100644 --- a/server/webhooks/vulnerabilities_test.go +++ b/server/webhooks/vulnerabilities_test.go @@ -82,15 +82,15 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) { now := time.Now() hosts := []*fleet.HostShort{ - {ID: 1, Hostname: "h1"}, - {ID: 2, Hostname: "h2"}, - {ID: 3, Hostname: "h3"}, - {ID: 4, Hostname: "h4"}, + {ID: 1, Hostname: "h1", DisplayName: "d1"}, + {ID: 2, Hostname: "h2", DisplayName: "d2"}, + {ID: 3, Hostname: "h3", DisplayName: "d3"}, + {ID: 4, Hostname: "h4", DisplayName: "d4"}, } - jsonH1 := fmt.Sprintf(`{"id":1,"hostname":"h1","url":"%s/hosts/1"}`, appCfg.ServerSettings.ServerURL) - jsonH2 := fmt.Sprintf(`{"id":2,"hostname":"h2","url":"%s/hosts/2"}`, appCfg.ServerSettings.ServerURL) - jsonH3 := fmt.Sprintf(`{"id":3,"hostname":"h3","url":"%s/hosts/3"}`, appCfg.ServerSettings.ServerURL) - jsonH4 := fmt.Sprintf(`{"id":4,"hostname":"h4","url":"%s/hosts/4"}`, appCfg.ServerSettings.ServerURL) + jsonH1 := fmt.Sprintf(`{"id":1,"hostname":"h1","display_name":"d1","url":"%s/hosts/1"}`, appCfg.ServerSettings.ServerURL) + jsonH2 := fmt.Sprintf(`{"id":2,"hostname":"h2","display_name":"d2","url":"%s/hosts/2"}`, appCfg.ServerSettings.ServerURL) + jsonH3 := fmt.Sprintf(`{"id":3,"hostname":"h3","display_name":"d3","url":"%s/hosts/3"}`, appCfg.ServerSettings.ServerURL) + jsonH4 := fmt.Sprintf(`{"id":4,"hostname":"h4","display_name":"d4","url":"%s/hosts/4"}`, appCfg.ServerSettings.ServerURL) cves := []string{ "CVE-2012-1234",