mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
feat: device health endpoint (#15432)
> #14920 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [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] Documented any permissions changes (docs/Using Fleet/manage-access.md) - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
fb01e30f27
commit
5fd799ff2e
12 changed files with 414 additions and 0 deletions
2
changes/14920-device-health
Normal file
2
changes/14920-device-health
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Adds a `GET /hosts/{id}/health` endpoint which reports back some key data about host health such
|
||||
as vulnerable software and failing policies.
|
||||
|
|
@ -261,6 +261,21 @@ allow {
|
|||
action == write
|
||||
}
|
||||
|
||||
# Allow read for host health for global admin/maintainer, team admins, observer.
|
||||
allow {
|
||||
object.type == "host_health"
|
||||
subject.global_role == [admin, maintainer, observer][_]
|
||||
action == read
|
||||
}
|
||||
|
||||
|
||||
# Allow read for host health for team admin/maintainer, team admins, observer.
|
||||
allow {
|
||||
object.type == "host_health"
|
||||
team_role(subject, object.team_id) == [admin, maintainer, observer][_]
|
||||
action == read
|
||||
}
|
||||
|
||||
##
|
||||
# Labels
|
||||
##
|
||||
|
|
|
|||
|
|
@ -2087,3 +2087,24 @@ func TestJSONToInterfaceUser(t *testing.T) {
|
|||
assert.Equal(t, json.Number("42"), subject["teams"].([]interface{})[1].(map[string]interface{})["id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostHealth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hostHealth := &fleet.HostHealth{TeamID: ptr.Uint(1)}
|
||||
runTestCases(t, []authTestCase{
|
||||
{user: nil, object: hostHealth, action: read, allow: false},
|
||||
{user: test.UserGitOps, object: hostHealth, action: read, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam1, object: hostHealth, action: read, allow: false},
|
||||
{user: test.UserTeamGitOpsTeam2, object: hostHealth, action: read, allow: false},
|
||||
{user: test.UserAdmin, object: hostHealth, action: read, allow: true},
|
||||
{user: test.UserTeamAdminTeam1, object: hostHealth, action: read, allow: true},
|
||||
{user: test.UserTeamAdminTeam2, object: hostHealth, action: read, allow: false},
|
||||
{user: test.UserObserver, object: hostHealth, action: read, allow: true},
|
||||
{user: test.UserTeamObserverTeam1, object: hostHealth, action: read, allow: true},
|
||||
{user: test.UserTeamObserverTeam2, object: hostHealth, action: read, allow: false},
|
||||
{user: test.UserMaintainer, object: hostHealth, action: read, allow: true},
|
||||
{user: test.UserTeamMaintainerTeam1, object: hostHealth, action: read, allow: true},
|
||||
{user: test.UserTeamMaintainerTeam2, object: hostHealth, action: read, allow: false},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4536,3 +4536,44 @@ func (ds *Datastore) GetMatchingHostSerials(ctx context.Context, serials []strin
|
|||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHealth, error) {
|
||||
sqlStmt := `
|
||||
SELECT h.os_version, h.updated_at, h.platform, h.team_id, hd.encrypted as disk_encryption_enabled FROM hosts h
|
||||
LEFT JOIN host_disks hd ON hd.host_id = h.id
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
var hh fleet.HostHealth
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &hh, sqlStmt, id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ctxerr.Wrap(ctx, notFound("Host Health").WithID(id))
|
||||
}
|
||||
|
||||
return nil, ctxerr.Wrap(ctx, err, "loading host health")
|
||||
}
|
||||
|
||||
host := &fleet.Host{ID: id, Platform: hh.Platform}
|
||||
if err := ds.LoadHostSoftware(ctx, host, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, s := range host.Software {
|
||||
if len(s.Vulnerabilities) > 0 {
|
||||
hh.VulnerableSoftware = append(hh.VulnerableSoftware, s)
|
||||
}
|
||||
}
|
||||
|
||||
policies, err := ds.ListPoliciesForHost(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range policies {
|
||||
if p.Response == "fail" {
|
||||
hh.FailingPolicies = append(hh.FailingPolicies, p)
|
||||
}
|
||||
}
|
||||
|
||||
return &hh, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ func TestHosts(t *testing.T) {
|
|||
{"ListHostsLiteByIDs", testHostsListHostsLiteByIDs},
|
||||
{"ListHostsWithPagination", testListHostsWithPagination},
|
||||
{"LastRestarted", testLastRestarted},
|
||||
{"HostHealth", testHostHealth},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -7690,3 +7691,137 @@ func testLastRestarted(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, time.Duration(uptimeVal), host.Uptime)
|
||||
require.Equal(t, hostsToVals[host.ID], host.LastRestartedAt)
|
||||
}
|
||||
|
||||
func testHostHealth(t *testing.T, ds *Datastore) {
|
||||
_, err := ds.GetHostHealth(context.Background(), 1)
|
||||
require.Error(t, err)
|
||||
var nfe fleet.NotFoundError
|
||||
require.True(t, errors.As(err, &nfe))
|
||||
|
||||
// We'll check TeamIDs because at this level they should still be populated
|
||||
team, err := ds.NewTeam(context.Background(), &fleet.Team{
|
||||
Name: "team1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
_, err = ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 1,
|
||||
OsqueryHostID: ptr.String("foobar"),
|
||||
NodeKey: ptr.String("nodekey"),
|
||||
Hostname: "foobar.local",
|
||||
UUID: "uuid",
|
||||
Platform: "darwin",
|
||||
DistributedInterval: 60,
|
||||
LoggerTLSPeriod: 50,
|
||||
ConfigTLSRefresh: 40,
|
||||
DetailUpdatedAt: now,
|
||||
LabelUpdatedAt: now,
|
||||
LastEnrolledAt: now,
|
||||
PolicyUpdatedAt: now,
|
||||
RefetchRequested: true,
|
||||
TeamID: ptr.Uint(team.ID),
|
||||
|
||||
SeenTime: now,
|
||||
|
||||
CPUType: "cpuType",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
h, err := ds.Host(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// set up policies
|
||||
u := test.NewUser(t, ds, "Jack", "jack@example.com", true)
|
||||
|
||||
q := test.NewQuery(t, ds, nil, "passing_query", "select 1", 0, true)
|
||||
passingPolicy, err := ds.NewGlobalPolicy(context.Background(), &u.ID, fleet.PolicyPayload{QueryID: &q.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
q = test.NewQuery(t, ds, nil, "failing_query", "select 1", 0, true)
|
||||
failingPolicy, err := ds.NewGlobalPolicy(context.Background(), &u.ID, fleet.PolicyPayload{QueryID: &q.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), h, map[uint]*bool{passingPolicy.ID: ptr.Bool(true)}, time.Now(), false))
|
||||
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), h, map[uint]*bool{failingPolicy.ID: ptr.Bool(false)}, time.Now(), false))
|
||||
|
||||
// set up vulnerable software
|
||||
software := []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
||||
{Name: "bar", Version: "0.0.3", Source: "apps"},
|
||||
{Name: "baz", Version: "0.0.4", Source: "apps"},
|
||||
}
|
||||
_, err = ds.UpdateHostSoftware(context.Background(), h.ID, software)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ds.LoadHostSoftware(context.Background(), h, false))
|
||||
|
||||
soft1 := h.Software[0]
|
||||
if soft1.Name != "bar" {
|
||||
soft1 = h.Software[1]
|
||||
}
|
||||
|
||||
cpes := []fleet.SoftwareCPE{{SoftwareID: soft1.ID, CPE: "somecpe"}}
|
||||
_, err = ds.UpsertSoftwareCPEs(context.Background(), cpes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reload software so that 'GeneratedCPEID is set.
|
||||
require.NoError(t, ds.LoadHostSoftware(context.Background(), h, false))
|
||||
soft1 = h.Software[0]
|
||||
if soft1.Name != "bar" {
|
||||
soft1 = h.Software[1]
|
||||
}
|
||||
|
||||
inserted, err := ds.InsertSoftwareVulnerability(
|
||||
context.Background(), fleet.SoftwareVulnerability{
|
||||
SoftwareID: soft1.ID,
|
||||
CVE: "cve-123-123-132",
|
||||
}, fleet.NVDSource,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.True(t, inserted)
|
||||
|
||||
hh, err := ds.GetHostHealth(context.Background(), h.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, h.Platform, hh.Platform)
|
||||
require.Equal(t, h.DiskEncryptionEnabled, hh.DiskEncryptionEnabled)
|
||||
require.Equal(t, h.OSVersion, hh.OsVersion)
|
||||
require.Equal(t, ptr.Uint(team.ID), hh.TeamID)
|
||||
require.Equal(t, h.UpdatedAt, hh.UpdatedAt)
|
||||
require.Len(t, hh.FailingPolicies, 1)
|
||||
require.Equal(t, failingPolicy.ID, hh.FailingPolicies[0].ID)
|
||||
require.Len(t, hh.VulnerableSoftware, 1)
|
||||
require.Equal(t, soft1.ID, hh.VulnerableSoftware[0].ID)
|
||||
|
||||
// Validate a host with no software or policies or team
|
||||
_, err = ds.NewHost(context.Background(), &fleet.Host{
|
||||
ID: 2,
|
||||
OsqueryHostID: ptr.String("empty"),
|
||||
NodeKey: ptr.String("empty_nodekey"),
|
||||
Hostname: "empty.local",
|
||||
UUID: "uuid123",
|
||||
Platform: "darwin",
|
||||
DistributedInterval: 60,
|
||||
LoggerTLSPeriod: 50,
|
||||
ConfigTLSRefresh: 40,
|
||||
DetailUpdatedAt: now,
|
||||
LabelUpdatedAt: now,
|
||||
LastEnrolledAt: now,
|
||||
PolicyUpdatedAt: now,
|
||||
RefetchRequested: true,
|
||||
|
||||
SeenTime: now,
|
||||
|
||||
CPUType: "cpuType",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
h, err = ds.Host(context.Background(), 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
hh, err = ds.GetHostHealth(context.Background(), h.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, h.Platform, hh.Platform)
|
||||
require.Equal(t, h.DiskEncryptionEnabled, hh.DiskEncryptionEnabled)
|
||||
require.Equal(t, h.OSVersion, hh.OsVersion)
|
||||
require.Empty(t, hh.FailingPolicies)
|
||||
require.Empty(t, hh.VulnerableSoftware)
|
||||
require.Equal(t, h.TeamID, hh.TeamID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ type Datastore interface {
|
|||
NewHost(ctx context.Context, host *Host) (*Host, error)
|
||||
DeleteHost(ctx context.Context, hid uint) error
|
||||
Host(ctx context.Context, id uint) (*Host, error)
|
||||
GetHostHealth(ctx context.Context, id uint) (*HostHealth, error)
|
||||
ListHosts(ctx context.Context, filter TeamFilter, opt HostListOptions) ([]*Host, error)
|
||||
|
||||
// ListHostsLiteByUUIDs returns the "lite" version of hosts corresponding to
|
||||
|
|
|
|||
|
|
@ -321,6 +321,22 @@ type Host struct {
|
|||
LastRestartedAt time.Time `json:"last_restarted_at" db:"last_restarted_at" csv:"last_restarted_at"`
|
||||
}
|
||||
|
||||
// HostHealth contains a subset of Host data that indicates how healthy a Host is. For fields with
|
||||
// the same name, see the comments/docs for the Host field above.
|
||||
type HostHealth struct {
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at"`
|
||||
OsVersion string `json:"os_version,omitempty" db:"os_version"`
|
||||
DiskEncryptionEnabled *bool `json:"disk_encryption_enabled,omitempty" db:"disk_encryption_enabled"`
|
||||
VulnerableSoftware []HostSoftwareEntry `json:"vulnerable_software,omitempty"`
|
||||
FailingPolicies []*HostPolicy `json:"failing_policies,omitempty"`
|
||||
Platform string `json:"-" db:"platform"` // Needed to fetch failing policies. Not returned in HTTP responses.
|
||||
TeamID *uint `json:"team_id,omitempty" db:"team_id"` // Needed to verify that user can access this host's health data. Not returned in HTTP responses.
|
||||
}
|
||||
|
||||
func (hh HostHealth) AuthzType() string {
|
||||
return "host_health"
|
||||
}
|
||||
|
||||
type MDMHostData struct {
|
||||
// For CSV columns, since the CSV is flattened, we keep the "mdm." prefix
|
||||
// along with the column name.
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ type Service interface {
|
|||
// The return value can also include policy information and CVE scores based
|
||||
// on the values provided to `opts`
|
||||
GetHost(ctx context.Context, id uint, opts HostDetailOptions) (host *HostDetail, err error)
|
||||
GetHostHealth(ctx context.Context, id uint) (hostHealth *HostHealth, err error)
|
||||
GetHostSummary(ctx context.Context, teamID *uint, platform *string, lowDiskSpace *int) (summary *HostSummary, err error)
|
||||
DeleteHost(ctx context.Context, id uint) (err error)
|
||||
// HostByIdentifier returns one host matching the provided identifier.
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ type DeleteHostFunc func(ctx context.Context, hid uint) error
|
|||
|
||||
type HostFunc func(ctx context.Context, id uint) (*fleet.Host, error)
|
||||
|
||||
type GetHostHealthFunc func(ctx context.Context, id uint) (*fleet.HostHealth, error)
|
||||
|
||||
type ListHostsFunc func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error)
|
||||
|
||||
type ListHostsLiteByUUIDsFunc func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error)
|
||||
|
|
@ -973,6 +975,9 @@ type DataStore struct {
|
|||
HostFunc HostFunc
|
||||
HostFuncInvoked bool
|
||||
|
||||
GetHostHealthFunc GetHostHealthFunc
|
||||
GetHostHealthFuncInvoked bool
|
||||
|
||||
ListHostsFunc ListHostsFunc
|
||||
ListHostsFuncInvoked bool
|
||||
|
||||
|
|
@ -2373,6 +2378,13 @@ func (s *DataStore) Host(ctx context.Context, id uint) (*fleet.Host, error) {
|
|||
return s.HostFunc(ctx, id)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHealth, error) {
|
||||
s.mu.Lock()
|
||||
s.GetHostHealthFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetHostHealthFunc(ctx, id)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||
s.mu.Lock()
|
||||
s.ListHostsFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -385,6 +385,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/device_mapping", listHostDeviceMappingEndpoint, listHostDeviceMappingRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/report", hostsReportEndpoint, hostsReportRequest{})
|
||||
ue.GET("/api/_version_/fleet/os_versions", osVersionsEndpoint, osVersionsRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/health", getHostHealthEndpoint, getHostHealthRequest{})
|
||||
|
||||
ue.GET("/api/_version_/fleet/hosts/summary/mdm", getHostMDMSummary, getHostMDMSummaryRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/mdm", getHostMDM, getHostMDMRequest{})
|
||||
|
|
|
|||
|
|
@ -1704,3 +1704,46 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host
|
|||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Host Health
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type getHostHealthRequest struct {
|
||||
ID uint `url:"id"`
|
||||
}
|
||||
|
||||
type getHostHealthResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
HostID uint `json:"host_id,omitempty"`
|
||||
HostHealth *fleet.HostHealth `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
func (r getHostHealthResponse) error() error { return r.Err }
|
||||
|
||||
func getHostHealthEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*getHostHealthRequest)
|
||||
hh, err := svc.GetHostHealth(ctx, req.ID)
|
||||
if err != nil {
|
||||
return getHostHealthResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// remove TeamID as it's needed for authorization internally but is not part of the external API
|
||||
hh.TeamID = nil
|
||||
|
||||
return getHostHealthResponse{HostID: req.ID, HostHealth: hh}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHealth, error) {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
hh, err := svc.ds.GetHostHealth(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, hh, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hh, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -692,6 +692,7 @@ func (s *integrationTestSuite) TestVulnerableSoftware() {
|
|||
Hostname: t.Name() + "foo.local",
|
||||
PrimaryIP: "192.168.1.1",
|
||||
PrimaryMac: "30-65-EC-6F-C4-58",
|
||||
OSVersion: "Mac OS X 10.14.6",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host)
|
||||
|
|
@ -8795,3 +8796,128 @@ func results(num int, hostID string) string {
|
|||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestHostHealth() {
|
||||
t := s.T()
|
||||
|
||||
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
|
||||
Name: "team1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
OsqueryHostID: ptr.String(t.Name() + "hostid1"),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String(t.Name() + "nodekey1"),
|
||||
UUID: t.Name() + "uuid1",
|
||||
Hostname: t.Name() + "foo.local",
|
||||
PrimaryIP: "192.168.1.1",
|
||||
PrimaryMac: "30-65-EC-6F-C4-58",
|
||||
OSVersion: "Mac OS X 10.14.6",
|
||||
Platform: "darwin",
|
||||
CPUType: "cpuType",
|
||||
TeamID: ptr.Uint(team.ID),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host)
|
||||
|
||||
software := []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
||||
{Name: "bar", Version: "0.0.3", Source: "apps"},
|
||||
{Name: "baz", Version: "0.0.4", Source: "apps"},
|
||||
}
|
||||
_, err = s.ds.UpdateHostSoftware(context.Background(), host.ID, software)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.ds.LoadHostSoftware(context.Background(), host, false))
|
||||
|
||||
soft1 := host.Software[0]
|
||||
if soft1.Name != "bar" {
|
||||
soft1 = host.Software[1]
|
||||
}
|
||||
|
||||
cpes := []fleet.SoftwareCPE{{SoftwareID: soft1.ID, CPE: "somecpe"}}
|
||||
_, err = s.ds.UpsertSoftwareCPEs(context.Background(), cpes)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reload software so that 'GeneratedCPEID is set.
|
||||
require.NoError(t, s.ds.LoadHostSoftware(context.Background(), host, false))
|
||||
soft1 = host.Software[0]
|
||||
if soft1.Name != "bar" {
|
||||
soft1 = host.Software[1]
|
||||
}
|
||||
|
||||
inserted, err := s.ds.InsertSoftwareVulnerability(
|
||||
context.Background(), fleet.SoftwareVulnerability{
|
||||
SoftwareID: soft1.ID,
|
||||
CVE: "cve-123-123-132",
|
||||
}, fleet.NVDSource,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.True(t, inserted)
|
||||
|
||||
user1 := test.NewUser(t, s.ds, "Joe", "joe@example.com", true)
|
||||
|
||||
q1 := test.NewQuery(t, s.ds, nil, "passing_query", "select 1", 0, true)
|
||||
defer cleanupQuery(s, q1.ID)
|
||||
passingPolicy, err := s.ds.NewTeamPolicy(context.Background(), team.ID, &user1.ID, fleet.PolicyPayload{
|
||||
QueryID: &q1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
q2 := test.NewQuery(t, s.ds, nil, "failing_query", "select 0", 0, true)
|
||||
defer cleanupQuery(s, q2.ID)
|
||||
failingPolicy, err := s.ds.NewTeamPolicy(context.Background(), team.ID, &user1.ID, fleet.PolicyPayload{
|
||||
QueryID: &q2.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{failingPolicy.ID: ptr.Bool(false)}, time.Now(), false))
|
||||
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{passingPolicy.ID: ptr.Bool(true)}, time.Now(), false))
|
||||
|
||||
require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), host.ID, true))
|
||||
|
||||
// Get host health
|
||||
hh := getHostHealthResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/health", host.ID), nil, http.StatusOK, &hh)
|
||||
assert.Equal(t, host.ID, hh.HostID)
|
||||
assert.NotNil(t, hh.HostHealth)
|
||||
assert.Equal(t, host.OSVersion, hh.HostHealth.OsVersion)
|
||||
assert.Len(t, hh.HostHealth.VulnerableSoftware, 1)
|
||||
assert.Len(t, hh.HostHealth.FailingPolicies, 1)
|
||||
assert.True(t, *hh.HostHealth.DiskEncryptionEnabled)
|
||||
// Check that the TeamID didn't make it into the response
|
||||
assert.Nil(t, hh.HostHealth.TeamID)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/health", 0), nil, http.StatusNotFound, &hh)
|
||||
|
||||
resp := getHostHealthResponse{}
|
||||
host1, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
OsqueryHostID: ptr.String(t.Name() + "hostid2"),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String(t.Name() + "nodekey2"),
|
||||
UUID: t.Name() + "uuid2",
|
||||
Hostname: t.Name() + "foo2.local",
|
||||
PrimaryIP: "192.168.2.2",
|
||||
PrimaryMac: "32-62-E2-62-C2-52",
|
||||
OSVersion: "Mac OS X 10.14.2",
|
||||
Platform: "darwin",
|
||||
CPUType: "cpuType",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/health", host1.ID), nil, http.StatusOK, &resp)
|
||||
assert.Equal(t, host1.ID, resp.HostID)
|
||||
assert.NotNil(t, resp.HostHealth)
|
||||
assert.Equal(t, host1.OSVersion, resp.HostHealth.OsVersion)
|
||||
assert.Nil(t, resp.HostHealth.DiskEncryptionEnabled)
|
||||
assert.Empty(t, resp.HostHealth.VulnerableSoftware)
|
||||
assert.Empty(t, resp.HostHealth.FailingPolicies)
|
||||
assert.Nil(t, resp.HostHealth.TeamID)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue