mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Add Failing Policy Counts to Health API (#17758)
This commit is contained in:
parent
775fa70c53
commit
46f7b6b043
5 changed files with 159 additions and 26 deletions
1
changes/16205-health-failing-counts
Normal file
1
changes/16205-health-failing-counts
Normal file
|
|
@ -0,0 +1 @@
|
|||
- The Host Health API now includes failing policy counts
|
||||
|
|
@ -15,7 +15,9 @@ import (
|
|||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
|
@ -4973,7 +4975,11 @@ func (ds *Datastore) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHea
|
|||
|
||||
for _, s := range host.Software {
|
||||
if len(s.Vulnerabilities) > 0 {
|
||||
hh.VulnerableSoftware = append(hh.VulnerableSoftware, s)
|
||||
hh.VulnerableSoftware = append(hh.VulnerableSoftware, fleet.HostHealthVulnerableSoftware{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Version: s.Version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4982,12 +4988,34 @@ func (ds *Datastore) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHea
|
|||
return nil, err
|
||||
}
|
||||
|
||||
isPremium := license.IsPremium(ctx)
|
||||
for _, p := range policies {
|
||||
if p.Response == "fail" {
|
||||
hh.FailingPolicies = append(hh.FailingPolicies, p)
|
||||
var critical *bool
|
||||
if isPremium {
|
||||
critical = &p.Critical
|
||||
}
|
||||
hh.FailingPolicies = append(hh.FailingPolicies, &fleet.HostHealthFailingPolicy{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Resolution: p.Resolution,
|
||||
Critical: critical,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hh.FailingPoliciesCount = len(hh.FailingPolicies)
|
||||
|
||||
if license.IsPremium(ctx) {
|
||||
var count int
|
||||
for _, p := range hh.FailingPolicies {
|
||||
if p.Critical != nil && *p.Critical {
|
||||
count++
|
||||
}
|
||||
}
|
||||
hh.FailingCriticalPoliciesCount = ptr.Int(count)
|
||||
}
|
||||
|
||||
return &hh, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -367,13 +367,28 @@ type HostOrbitInfo struct {
|
|||
// 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.
|
||||
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"`
|
||||
FailingPoliciesCount int `json:"failing_policies_count"`
|
||||
FailingCriticalPoliciesCount *int `json:"failing_critical_policies_count,omitempty"` // Fleet Premium Only
|
||||
VulnerableSoftware []HostHealthVulnerableSoftware `json:"vulnerable_software,omitempty"`
|
||||
FailingPolicies []*HostHealthFailingPolicy `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.
|
||||
}
|
||||
|
||||
type HostHealthVulnerableSoftware struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type HostHealthFailingPolicy struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Critical *bool `json:"critical,omitempty"` // Fleet Premium Only
|
||||
Resolution *string `json:"resolution"`
|
||||
}
|
||||
|
||||
func (hh HostHealth) AuthzType() string {
|
||||
|
|
|
|||
|
|
@ -10612,11 +10612,6 @@ func results(num int, hostID string) 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"),
|
||||
|
|
@ -10631,7 +10626,7 @@ func (s *integrationTestSuite) TestHostHealth() {
|
|||
OSVersion: "Mac OS X 10.14.6",
|
||||
Platform: "darwin",
|
||||
CPUType: "cpuType",
|
||||
TeamID: ptr.Uint(team.ID),
|
||||
TeamID: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host)
|
||||
|
|
@ -10670,19 +10665,17 @@ func (s *integrationTestSuite) TestHostHealth() {
|
|||
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,
|
||||
passingPolicy, err := s.ds.NewGlobalPolicy(context.Background(), nil, fleet.PolicyPayload{
|
||||
Name: "passing_policy",
|
||||
Query: "select 1",
|
||||
Resolution: "Run this command to fix it",
|
||||
})
|
||||
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,
|
||||
failingPolicy, err := s.ds.NewGlobalPolicy(context.Background(), nil, fleet.PolicyPayload{
|
||||
Name: "failing_policy",
|
||||
Query: "select 0",
|
||||
Resolution: "Run this command to fix it",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -10698,7 +10691,20 @@ func (s *integrationTestSuite) TestHostHealth() {
|
|||
assert.NotNil(t, hh.HostHealth)
|
||||
assert.Equal(t, host.OSVersion, hh.HostHealth.OsVersion)
|
||||
assert.Len(t, hh.HostHealth.VulnerableSoftware, 1)
|
||||
assert.Equal(t, hh.HostHealth.VulnerableSoftware[0], fleet.HostHealthVulnerableSoftware{
|
||||
ID: soft1.ID,
|
||||
Name: soft1.Name,
|
||||
Version: soft1.Version,
|
||||
})
|
||||
assert.Equal(t, 1, hh.HostHealth.FailingPoliciesCount)
|
||||
assert.Nil(t, hh.HostHealth.FailingCriticalPoliciesCount)
|
||||
assert.Len(t, hh.HostHealth.FailingPolicies, 1)
|
||||
assert.Equal(t, hh.HostHealth.FailingPolicies[0], &fleet.HostHealthFailingPolicy{
|
||||
ID: failingPolicy.ID,
|
||||
Name: failingPolicy.Name,
|
||||
Resolution: failingPolicy.Resolution,
|
||||
Critical: nil,
|
||||
})
|
||||
assert.True(t, *hh.HostHealth.DiskEncryptionEnabled)
|
||||
// Check that the TeamID didn't make it into the response
|
||||
assert.Nil(t, hh.HostHealth.TeamID)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/pubsub"
|
||||
"github.com/fleetdm/fleet/v4/server/service/schedule"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/log"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -3279,6 +3279,89 @@ func (s *integrationEnterpriseTestSuite) TestListHosts() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) 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)
|
||||
|
||||
passingTeamPolicy, err := s.ds.NewTeamPolicy(context.Background(), team.ID, nil, fleet.PolicyPayload{
|
||||
Name: "Passing Global Policy",
|
||||
Query: "select 1",
|
||||
Resolution: "Run this command to fix it",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
failingTeamPolicy, err := s.ds.NewTeamPolicy(context.Background(), team.ID, nil, fleet.PolicyPayload{
|
||||
Name: "Failing Global Policy",
|
||||
Query: "select 1",
|
||||
Resolution: "Run this command to fix it",
|
||||
Critical: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
passingGlobalPolicy, err := s.ds.NewGlobalPolicy(context.Background(), nil, fleet.PolicyPayload{
|
||||
Name: "Passing Global Policy",
|
||||
Query: "select 1",
|
||||
Resolution: "Run this command to fix it",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
failingGlobalPolicy, err := s.ds.NewGlobalPolicy(context.Background(), nil, fleet.PolicyPayload{
|
||||
Name: "Failing Global Policy",
|
||||
Query: "select 1",
|
||||
Resolution: "Run this command to fix it",
|
||||
Critical: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{failingGlobalPolicy.ID: ptr.Bool(false)}, time.Now(), false))
|
||||
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{passingGlobalPolicy.ID: ptr.Bool(true)}, time.Now(), false))
|
||||
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{failingTeamPolicy.ID: ptr.Bool(false)}, time.Now(), false))
|
||||
require.NoError(t, s.ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{passingTeamPolicy.ID: ptr.Bool(true)}, time.Now(), false))
|
||||
|
||||
hh := getHostHealthResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/health", host.ID), nil, http.StatusOK, &hh)
|
||||
require.Equal(t, host.ID, hh.HostID)
|
||||
assert.NotNil(t, hh.HostHealth)
|
||||
assert.Equal(t, host.OSVersion, hh.HostHealth.OsVersion)
|
||||
assert.Equal(t, 2, hh.HostHealth.FailingPoliciesCount)
|
||||
assert.Equal(t, ptr.Int(1), hh.HostHealth.FailingCriticalPoliciesCount)
|
||||
assert.Contains(t, hh.HostHealth.FailingPolicies, &fleet.HostHealthFailingPolicy{
|
||||
ID: failingTeamPolicy.ID,
|
||||
Name: failingTeamPolicy.Name,
|
||||
Resolution: failingTeamPolicy.Resolution,
|
||||
Critical: ptr.Bool(true),
|
||||
})
|
||||
assert.Contains(t, hh.HostHealth.FailingPolicies, &fleet.HostHealthFailingPolicy{
|
||||
ID: failingGlobalPolicy.ID,
|
||||
Name: failingGlobalPolicy.Name,
|
||||
Resolution: failingGlobalPolicy.Resolution,
|
||||
Critical: ptr.Bool(false),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestListVulnerabilities() {
|
||||
t := s.T()
|
||||
var resp listVulnerabilitiesResponse
|
||||
|
|
|
|||
Loading…
Reference in a new issue