Add Failing Policy Counts to Health API (#17758)

This commit is contained in:
Tim Lee 2024-04-15 16:14:21 -06:00 committed by GitHub
parent 775fa70c53
commit 46f7b6b043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 159 additions and 26 deletions

View file

@ -0,0 +1 @@
- The Host Health API now includes failing policy counts

View file

@ -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
}

View file

@ -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 {

View file

@ -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)

View file

@ -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