mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Add platforms
field to policies (#3181)
* Add platforms field to policies * Fix fleetctl tests * PR review changes * Add missing tests * Add changes for ListPoliciesForHost
This commit is contained in:
parent
142006cbdd
commit
b9a408704e
1
changes/issue-3010-add-platform-to-policies
Normal file
1
changes/issue-3010-add-platform-to-policies
Normal file
@ -0,0 +1 @@
|
||||
* Add "platforms" field to policies.
|
@ -261,7 +261,7 @@ func TestGetHosts(t *testing.T) {
|
||||
return make([]*fleet.Pack, 0), nil
|
||||
}
|
||||
defaultPolicyQuery := "select 1 from osquery_info where start_time > 1;"
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.HostPolicy, error) {
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return []*fleet.HostPolicy{
|
||||
{
|
||||
PolicyData: fleet.PolicyData{
|
||||
|
@ -56,6 +56,7 @@
|
||||
"id":1,
|
||||
"query":"select 1 from osquery_info where start_time > 1;",
|
||||
"name":"query1",
|
||||
"platforms": "",
|
||||
"description":"Some description",
|
||||
"author_email":"alice@example.com",
|
||||
"author_id":1,
|
||||
@ -70,6 +71,7 @@
|
||||
"id":2,
|
||||
"query":"select 1 from osquery_info where start_time > 1;",
|
||||
"name":"query2",
|
||||
"platforms": "",
|
||||
"description":"",
|
||||
"author_email":"alice@example.com",
|
||||
"author_id":1,
|
||||
|
@ -43,6 +43,7 @@ spec:
|
||||
id: 1
|
||||
description: "Some description"
|
||||
name: query1
|
||||
platforms: ""
|
||||
query: select 1 from osquery_info where start_time > 1;
|
||||
resolution: "Some resolution"
|
||||
response: passes
|
||||
@ -55,6 +56,7 @@ spec:
|
||||
id: 2
|
||||
description: ""
|
||||
name: query2
|
||||
platforms: ""
|
||||
query: select 1 from osquery_info where start_time > 1;
|
||||
response: fails
|
||||
team_id: null
|
||||
|
@ -802,6 +802,7 @@ If the scheduled queries haven't run on the host yet, the stats have zero values
|
||||
"query": "select * from foo;",
|
||||
"description": "this is a query",
|
||||
"resolution": "fix with these steps...",
|
||||
"platforms": "windows,linux",
|
||||
"response": "pass"
|
||||
},
|
||||
{
|
||||
@ -810,6 +811,7 @@ If the scheduled queries haven't run on the host yet, the stats have zero values
|
||||
"query": "select * from bar;",
|
||||
"description": "this is another query",
|
||||
"resolution": "fix with these other steps...",
|
||||
"platforms": "darwin",
|
||||
"response": "fail"
|
||||
},
|
||||
{
|
||||
@ -818,6 +820,7 @@ If the scheduled queries haven't run on the host yet, the stats have zero values
|
||||
"query": "select * from baz;",
|
||||
"description": "",
|
||||
"resolution": "",
|
||||
"platforms": "",
|
||||
"response": ""
|
||||
}
|
||||
],
|
||||
@ -3444,6 +3447,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
|
||||
"author_name": "John",
|
||||
"author_email": "john@example.com",
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 2000,
|
||||
"failing_host_count": 300
|
||||
},
|
||||
@ -3455,6 +3459,8 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
|
||||
"author_id": 43,
|
||||
"author_name": "Alice",
|
||||
"author_email": "alice@example.com",
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "windows",
|
||||
"passing_host_count": 2300,
|
||||
"failing_host_count": 0
|
||||
}
|
||||
@ -3491,6 +3497,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
|
||||
"author_name": "John",
|
||||
"author_email": "john@example.com",
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 2000,
|
||||
"failing_host_count": 300
|
||||
}
|
||||
@ -3517,6 +3524,7 @@ An error is returned if both "query" and "query_id" are set on the request.
|
||||
| description | string | body | The query's description. |
|
||||
| resolution | string | body | The resolution steps for the policy. |
|
||||
| query_id | integer | body | An existing query's ID (legacy). |
|
||||
| platforms | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms |
|
||||
|
||||
Either `query` or `query_id` must be provided.
|
||||
|
||||
@ -3531,7 +3539,8 @@ Either `query` or `query_id` must be provided.
|
||||
"name": "Gatekeeper enabled",
|
||||
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
|
||||
"description": "Checks if gatekeeper is enabled on macOS devices",
|
||||
"resolution": "Resolution steps"
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin"
|
||||
}
|
||||
```
|
||||
|
||||
@ -3550,6 +3559,7 @@ Either `query` or `query_id` must be provided.
|
||||
"author_name": "John",
|
||||
"author_email": "john@example.com",
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0
|
||||
}
|
||||
@ -3585,6 +3595,7 @@ Where `query_id` references an existing `query`.
|
||||
"author_name": "John",
|
||||
"author_email": "john@example.com",
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0
|
||||
}
|
||||
@ -3636,6 +3647,8 @@ Where `query_id` references an existing `query`.
|
||||
| query | string | body | The query in SQL. |
|
||||
| description | string | body | The query's description. |
|
||||
| resolution | string | body | The resolution steps for the policy. |
|
||||
| platforms | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms |
|
||||
|
||||
|
||||
#### Example Edit Policy
|
||||
|
||||
@ -3648,7 +3661,8 @@ Where `query_id` references an existing `query`.
|
||||
"name": "Gatekeeper enabled",
|
||||
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
|
||||
"description": "Checks if gatekeeper is enabled on macOS devices",
|
||||
"resolution": "Resolution steps"
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin"
|
||||
}
|
||||
```
|
||||
|
||||
@ -3667,6 +3681,7 @@ Where `query_id` references an existing `query`.
|
||||
"author_name": "John",
|
||||
"author_email": "john@example.com",
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0
|
||||
}
|
||||
@ -3718,6 +3733,7 @@ Team policies work the same as policies, but at the team level.
|
||||
"author_email": "john@example.com",
|
||||
"team_id": 1,
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 2000,
|
||||
"failing_host_count": 300
|
||||
},
|
||||
@ -3730,6 +3746,8 @@ Team policies work the same as policies, but at the team level.
|
||||
"author_name": "Alice",
|
||||
"author_email": "alice@example.com",
|
||||
"team_id": 1,
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "windows",
|
||||
"passing_host_count": 2300,
|
||||
"failing_host_count": 0
|
||||
}
|
||||
@ -3768,6 +3786,7 @@ Team policies work the same as policies, but at the team level.
|
||||
"author_email": "john@example.com",
|
||||
"team_id": 1,
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0
|
||||
}
|
||||
@ -3790,6 +3809,7 @@ The semantics for creating a team policy are the same as for global policies, se
|
||||
| description | string | body | The query's description. |
|
||||
| resolution | string | body | The resolution steps for the policy. |
|
||||
| query_id | integer | body | An existing query's ID (legacy). |
|
||||
| platforms | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms |
|
||||
|
||||
Either `query` or `query_id` must be provided.
|
||||
|
||||
@ -3804,7 +3824,8 @@ Either `query` or `query_id` must be provided.
|
||||
"name": "Gatekeeper enabled",
|
||||
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
|
||||
"description": "Checks if gatekeeper is enabled on macOS devices",
|
||||
"resolution": "Resolution steps"
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin"
|
||||
}
|
||||
```
|
||||
|
||||
@ -3824,6 +3845,7 @@ Either `query` or `query_id` must be provided.
|
||||
"author_email": "john@example.com",
|
||||
"team_id": 1,
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0
|
||||
}
|
||||
@ -3877,6 +3899,8 @@ Either `query` or `query_id` must be provided.
|
||||
| query | string | body | The query in SQL. |
|
||||
| description | string | body | The query's description. |
|
||||
| resolution | string | body | The resolution steps for the policy. |
|
||||
| platforms | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms |
|
||||
|
||||
|
||||
#### Example Edit Policy
|
||||
|
||||
@ -3889,7 +3913,8 @@ Either `query` or `query_id` must be provided.
|
||||
"name": "Gatekeeper enabled",
|
||||
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
|
||||
"description": "Checks if gatekeeper is enabled on macOS devices",
|
||||
"resolution": "Resolution steps"
|
||||
"resolution": "Resolution steps",
|
||||
"plarforms": "darwin"
|
||||
}
|
||||
```
|
||||
|
||||
@ -3908,6 +3933,7 @@ Either `query` or `query_id` must be provided.
|
||||
"author_name": "John",
|
||||
"author_email": "john@example.com",
|
||||
"resolution": "Resolution steps",
|
||||
"platforms": "darwin",
|
||||
"team_id": 2,
|
||||
"passing_host_count": 0,
|
||||
"failing_host_count": 0
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
@ -263,17 +264,6 @@ func saveHostPackStatsDB(ctx context.Context, db sqlx.ExecerContext, host *fleet
|
||||
return nil
|
||||
}
|
||||
|
||||
// schQueriesPlatformFromHost converts the platform from a Host.Platform
|
||||
// string to a scheduled query platform string.
|
||||
func schQueryPlatformFromHost(hostPlatform string) string {
|
||||
switch hostPlatform {
|
||||
case "ubuntu", "rhel", "debian":
|
||||
return "linux"
|
||||
default: // darwin, windows
|
||||
return hostPlatform
|
||||
}
|
||||
}
|
||||
|
||||
// loadhostPacksStatsDB will load all the pack stats for the given host. The scheduled
|
||||
// queries that haven't run yet are returned with zero values.
|
||||
func loadHostPackStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint, hostPlatform string) ([]fleet.PackStats, error) {
|
||||
@ -328,7 +318,7 @@ func loadHostPackStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint,
|
||||
goqu.I("sq.platform").IsNull(),
|
||||
// scheduled_queries.platform can be a comma-separated list of
|
||||
// platforms, e.g. "darwin,windows".
|
||||
goqu.L("FIND_IN_SET(?, sq.platform)", schQueryPlatformFromHost(hostPlatform)).Neq(0),
|
||||
goqu.L("FIND_IN_SET(?, sq.platform)", fleet.PlatformFromHost(hostPlatform)).Neq(0),
|
||||
),
|
||||
)
|
||||
sql, args, err := ds.ToSQL()
|
||||
@ -1004,7 +994,11 @@ func (d *Datastore) DeleteHosts(ctx context.Context, ids []uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Datastore) ListPoliciesForHost(ctx context.Context, hid uint) (packs []*fleet.HostPolicy, err error) {
|
||||
func (d *Datastore) ListPoliciesForHost(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
if host.FleetPlatform() == "" {
|
||||
// We log to help troubleshooting in case this happens.
|
||||
level.Error(d.logger).Log("err", fmt.Sprintf("host %d with empty platform", host.ID))
|
||||
}
|
||||
query := `SELECT p.*,
|
||||
COALESCE(u.name, '<deleted>') AS author_name,
|
||||
COALESCE(u.email, '') AS author_email,
|
||||
@ -1017,10 +1011,11 @@ func (d *Datastore) ListPoliciesForHost(ctx context.Context, hid uint) (packs []
|
||||
FROM policies p
|
||||
LEFT JOIN policy_membership pm ON (p.id=pm.policy_id AND host_id=?)
|
||||
LEFT JOIN users u ON p.author_id = u.id
|
||||
WHERE p.team_id IS NULL OR p.team_id = (select team_id from hosts WHERE id = ?)`
|
||||
WHERE (p.team_id IS NULL OR p.team_id = (select team_id from hosts WHERE id = ?))
|
||||
AND (p.platforms IS NULL OR p.platforms = "" OR FIND_IN_SET(?, p.platforms) != 0)`
|
||||
|
||||
var policies []*fleet.HostPolicy
|
||||
if err := sqlx.SelectContext(ctx, d.reader, &policies, query, hid, hid); err != nil {
|
||||
if err := sqlx.SelectContext(ctx, d.reader, &policies, query, host.ID, host.ID, host.FleetPlatform()); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get host policies")
|
||||
}
|
||||
return policies, nil
|
||||
|
@ -18,6 +18,7 @@ func Up_20211116184030(tx *sql.Tx) error {
|
||||
ADD COLUMN query mediumtext NOT NULL,
|
||||
ADD COLUMN description mediumtext NOT NULL,
|
||||
ADD COLUMN author_id int(10) unsigned DEFAULT NULL,
|
||||
ADD COLUMN platforms VARCHAR(255) NOT NULL DEFAULT '',
|
||||
|
||||
ADD KEY idx_policies_author_id (author_id),
|
||||
ADD KEY idx_policies_team_id (team_id),
|
||||
|
@ -7,8 +7,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
@ -23,8 +25,8 @@ func (ds *Datastore) NewGlobalPolicy(ctx context.Context, authorID *uint, args f
|
||||
args.Description = q.Description
|
||||
}
|
||||
res, err := ds.writer.ExecContext(ctx,
|
||||
`INSERT INTO policies (name, query, description, resolution, author_id) VALUES (?, ?, ?, ?, ?)`,
|
||||
args.Name, args.Query, args.Description, args.Resolution, authorID,
|
||||
`INSERT INTO policies (name, query, description, resolution, author_id, platforms) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
args.Name, args.Query, args.Description, args.Resolution, authorID, args.Platforms,
|
||||
)
|
||||
switch {
|
||||
case err == nil:
|
||||
@ -74,10 +76,10 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint)
|
||||
func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy) error {
|
||||
sql := `
|
||||
UPDATE policies
|
||||
SET name = ?, query = ?, description = ?, resolution = ?
|
||||
SET name = ?, query = ?, description = ?, resolution = ?, platforms = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
result, err := ds.writer.ExecContext(ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.ID)
|
||||
result, err := ds.writer.ExecContext(ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.Platforms, p.ID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating policy")
|
||||
}
|
||||
@ -212,44 +214,46 @@ func deletePolicyDB(ctx context.Context, q sqlx.ExtContext, ids []uint, teamID *
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// PolicyQueriesForHost returns the policy queries that are to be executed on the given host.
|
||||
func (ds *Datastore) PolicyQueriesForHost(ctx context.Context, host *fleet.Host) (map[string]string, error) {
|
||||
var globalRows, teamRows []struct {
|
||||
Id string `db:"id"`
|
||||
var rows []struct {
|
||||
ID string `db:"id"`
|
||||
Query string `db:"query"`
|
||||
}
|
||||
err := sqlx.SelectContext(
|
||||
ctx,
|
||||
ds.reader,
|
||||
&globalRows,
|
||||
`SELECT p.id, p.query FROM policies p WHERE team_id is NULL`,
|
||||
if host.FleetPlatform() == "" {
|
||||
// We log to help troubleshooting in case this happens, as the host
|
||||
// won't be receiving any policies targeted for specific platforms.
|
||||
level.Error(ds.logger).Log("err", fmt.Sprintf("host %d with empty platform", host.ID))
|
||||
}
|
||||
q := dialect.From("policies").Select(
|
||||
goqu.I("id"),
|
||||
goqu.I("query"),
|
||||
).Where(
|
||||
goqu.And(
|
||||
goqu.Or(
|
||||
goqu.I("platforms").Eq(""),
|
||||
goqu.L("FIND_IN_SET(?, ?)",
|
||||
host.FleetPlatform(),
|
||||
goqu.I("platforms"),
|
||||
).Neq(0),
|
||||
),
|
||||
goqu.Or(
|
||||
goqu.I("team_id").IsNull(), // global policies
|
||||
goqu.I("team_id").Eq(host.TeamID), // team policies
|
||||
),
|
||||
),
|
||||
)
|
||||
sql, args, err := q.ToSQL()
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "selecting policies sql build")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, ds.reader, &rows, sql, args...); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "selecting policies for host")
|
||||
}
|
||||
|
||||
results := map[string]string{}
|
||||
|
||||
if host.TeamID != nil {
|
||||
err := sqlx.SelectContext(
|
||||
ctx,
|
||||
ds.reader,
|
||||
&teamRows,
|
||||
`SELECT p.id, p.query FROM policies p WHERE team_id = ?`,
|
||||
*host.TeamID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "selecting policies for host in team")
|
||||
}
|
||||
results := make(map[string]string)
|
||||
for _, row := range rows {
|
||||
results[row.ID] = row.Query
|
||||
}
|
||||
|
||||
for _, row := range globalRows {
|
||||
results[row.Id] = row.Query
|
||||
}
|
||||
|
||||
for _, row := range teamRows {
|
||||
results[row.Id] = row.Query
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@ -264,8 +268,8 @@ func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, authorID *u
|
||||
args.Description = q.Description
|
||||
}
|
||||
res, err := ds.writer.ExecContext(ctx,
|
||||
`INSERT INTO policies (name, query, description, team_id, resolution, author_id) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
args.Name, args.Query, args.Description, teamID, args.Resolution, authorID)
|
||||
`INSERT INTO policies (name, query, description, team_id, resolution, author_id, platforms) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
args.Name, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platforms)
|
||||
switch {
|
||||
case err == nil:
|
||||
// OK
|
||||
@ -307,19 +311,21 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
|
||||
description,
|
||||
author_id,
|
||||
resolution,
|
||||
team_id
|
||||
) VALUES ( ?, ?, ?, ?, ?, (SELECT IFNULL(MIN(id), NULL) FROM teams WHERE name = ?) )
|
||||
team_id,
|
||||
platforms
|
||||
) VALUES ( ?, ?, ?, ?, ?, (SELECT IFNULL(MIN(id), NULL) FROM teams WHERE name = ?), ? )
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
query = VALUES(query),
|
||||
description = VALUES(description),
|
||||
author_id = VALUES(author_id),
|
||||
resolution = VALUES(resolution),
|
||||
team_id = VALUES(team_id)
|
||||
team_id = VALUES(team_id),
|
||||
platforms = VALUES(platforms)
|
||||
`
|
||||
for _, spec := range specs {
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
sql, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team,
|
||||
sql, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team, spec.Platforms,
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
|
||||
}
|
||||
|
@ -4,12 +4,16 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -28,6 +32,7 @@ func TestPolicies(t *testing.T) {
|
||||
{"TeamPolicyLegacy", testTeamPolicyLegacy},
|
||||
{"TeamPolicyProprietary", testTeamPolicyProprietary},
|
||||
{"PolicyQueriesForHost", testPolicyQueriesForHost},
|
||||
{"PolicyQueriesForHostPlatforms", testPolicyQueriesForHostPlatforms},
|
||||
{"TeamPolicyTransfer", testTeamPolicyTransfer},
|
||||
{"ApplyPolicySpec", testApplyPolicySpec},
|
||||
{"Save", testPoliciesSave},
|
||||
@ -518,6 +523,244 @@ func testTeamPolicyProprietary(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, user1.ID, *team2Policies[0].AuthorID)
|
||||
}
|
||||
|
||||
func newTestHostWithPlatform(t *testing.T, ds *Datastore, hostname, platform string, teamID *uint) *fleet.Host {
|
||||
nodeKey, err := server.GenerateRandomText(32)
|
||||
require.NoError(t, err)
|
||||
host, err := ds.NewHost(context.Background(), &fleet.Host{
|
||||
OsqueryHostID: uuid.NewString(),
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: nodeKey,
|
||||
UUID: uuid.NewString(),
|
||||
Hostname: hostname,
|
||||
Platform: platform,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if teamID != nil {
|
||||
err := ds.AddHostsToTeam(context.Background(), teamID, []uint{host.ID})
|
||||
require.NoError(t, err)
|
||||
host, err = ds.Host(context.Background(), host.ID, false)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func newTestPolicy(t *testing.T, ds *Datastore, user *fleet.User, name, platforms string, teamID *uint) *fleet.Policy {
|
||||
query := fmt.Sprintf("select %s;", name)
|
||||
if teamID == nil {
|
||||
gp, err := ds.NewGlobalPolicy(context.Background(), &user.ID, fleet.PolicyPayload{
|
||||
Name: name,
|
||||
Query: query,
|
||||
Platforms: platforms,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return gp
|
||||
}
|
||||
tp, err := ds.NewTeamPolicy(context.Background(), *teamID, &user.ID, fleet.PolicyPayload{
|
||||
Name: name,
|
||||
Query: query,
|
||||
Platforms: platforms,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return tp
|
||||
}
|
||||
|
||||
type expectedPolicyResults struct {
|
||||
policyQueries map[string]string
|
||||
hostPolicies []*fleet.HostPolicy
|
||||
}
|
||||
|
||||
func expectedPolicyQueries(policies ...*fleet.Policy) expectedPolicyResults {
|
||||
queries := make(map[string]string)
|
||||
for _, policy := range policies {
|
||||
queries[strconv.Itoa(int(policy.ID))] = policy.Query
|
||||
}
|
||||
hostPolicies := make([]*fleet.HostPolicy, len(policies))
|
||||
for i := range policies {
|
||||
hostPolicies[i] = &fleet.HostPolicy{
|
||||
PolicyData: policies[i].PolicyData,
|
||||
}
|
||||
}
|
||||
return expectedPolicyResults{
|
||||
policyQueries: queries,
|
||||
hostPolicies: hostPolicies,
|
||||
}
|
||||
}
|
||||
|
||||
func testPolicyQueriesForHostPlatforms(t *testing.T, ds *Datastore) {
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Global hosts:
|
||||
var global *uint
|
||||
host1GlobalUbuntu := newTestHostWithPlatform(t, ds, "host1_global_ubuntu", "ubuntu", global)
|
||||
host2GlobalDarwin := newTestHostWithPlatform(t, ds, "host2_global_darwin", "darwin", global)
|
||||
host3GlobalWindows := newTestHostWithPlatform(t, ds, "host3_global_windows", "windows", global)
|
||||
host4GlobalEmpty := newTestHostWithPlatform(t, ds, "host4_global_empty_platform", "", global)
|
||||
|
||||
// team1 hosts:
|
||||
host1t1Rhel := newTestHostWithPlatform(t, ds, "host1_team1_ubuntu", "rhel", &team1.ID)
|
||||
host2t1Darwin := newTestHostWithPlatform(t, ds, "host2_team1_darwin", "darwin", &team1.ID)
|
||||
host3t1Windows := newTestHostWithPlatform(t, ds, "host3_team1_windows", "windows", &team1.ID)
|
||||
host4t1Empty := newTestHostWithPlatform(t, ds, "host4_team1_empty_platform", "", &team1.ID)
|
||||
|
||||
// team2 hosts
|
||||
host1t2Debian := newTestHostWithPlatform(t, ds, "host1_team2_ubuntu", "debian", &team2.ID)
|
||||
host2t2Darwin := newTestHostWithPlatform(t, ds, "host2_team2_darwin", "darwin", &team2.ID)
|
||||
host3t2Windows := newTestHostWithPlatform(t, ds, "host3_team2_windows", "windows", &team2.ID)
|
||||
host4t2Empty := newTestHostWithPlatform(t, ds, "host4_team2_empty_platform", "", &team2.ID)
|
||||
|
||||
// Global policies:
|
||||
policy1GlobalLinuxDarwin := newTestPolicy(t, ds, user1, "policy1_global_linux_darwin", "linux,darwin", global)
|
||||
policy2GlobalWindows := newTestPolicy(t, ds, user1, "policy2_global_windows", "windows", global)
|
||||
policy3GlobalAll := newTestPolicy(t, ds, user1, "policy3_global_all", "", global)
|
||||
|
||||
// Team1 policies:
|
||||
policy1t1Darwin := newTestPolicy(t, ds, user1, "policy1_team1_darwin", "darwin", &team1.ID)
|
||||
policy2t1Windows := newTestPolicy(t, ds, user1, "policy2_team1_windows", "windows", &team1.ID)
|
||||
policy3t1All := newTestPolicy(t, ds, user1, "policy3_team1_all", "", &team1.ID)
|
||||
|
||||
// Team2 policies:
|
||||
policy1t2LinuxDarwin := newTestPolicy(t, ds, user1, "policy1_team2_linux_darwin", "linux,darwin", &team2.ID)
|
||||
policy2t2Windows := newTestPolicy(t, ds, user1, "policy2_team2_windows", "windows", &team2.ID)
|
||||
policy3t2All1 := newTestPolicy(t, ds, user1, "policy3_team2_all1", "linux,darwin,windows", &team2.ID)
|
||||
policy4t2All2 := newTestPolicy(t, ds, user1, "policy4_team2_all2", "", &team2.ID)
|
||||
|
||||
for _, tc := range []struct {
|
||||
host *fleet.Host
|
||||
expectedPolicies expectedPolicyResults
|
||||
}{
|
||||
{
|
||||
host: host1GlobalUbuntu,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy1GlobalLinuxDarwin,
|
||||
policy3GlobalAll,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host2GlobalDarwin,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy1GlobalLinuxDarwin,
|
||||
policy3GlobalAll,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host3GlobalWindows,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy2GlobalWindows,
|
||||
policy3GlobalAll,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host4GlobalEmpty,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy3GlobalAll,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host1t1Rhel,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy1GlobalLinuxDarwin,
|
||||
policy3GlobalAll,
|
||||
|
||||
policy3t1All,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host2t1Darwin,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy1GlobalLinuxDarwin,
|
||||
policy3GlobalAll,
|
||||
|
||||
policy3t1All,
|
||||
policy1t1Darwin,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host3t1Windows,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy2GlobalWindows,
|
||||
policy3GlobalAll,
|
||||
|
||||
policy3t1All,
|
||||
policy2t1Windows,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host4t1Empty,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy3GlobalAll,
|
||||
|
||||
policy3t1All,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host1t2Debian,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy1GlobalLinuxDarwin,
|
||||
policy3GlobalAll,
|
||||
|
||||
policy1t2LinuxDarwin,
|
||||
policy3t2All1,
|
||||
policy4t2All2,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host2t2Darwin,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy1GlobalLinuxDarwin,
|
||||
policy3GlobalAll,
|
||||
|
||||
policy1t2LinuxDarwin,
|
||||
policy3t2All1,
|
||||
policy4t2All2,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host3t2Windows,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy2GlobalWindows,
|
||||
policy3GlobalAll,
|
||||
|
||||
policy2t2Windows,
|
||||
policy3t2All1,
|
||||
policy4t2All2,
|
||||
),
|
||||
},
|
||||
{
|
||||
host: host4t2Empty,
|
||||
expectedPolicies: expectedPolicyQueries(
|
||||
policy3GlobalAll,
|
||||
|
||||
policy4t2All2,
|
||||
),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.host.Hostname, func(t *testing.T) {
|
||||
// PolicyQueriesForHost is the endpoint used by osquery agents when they check in.
|
||||
queries, err := ds.PolicyQueriesForHost(context.Background(), tc.host)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedPolicies.policyQueries, queries)
|
||||
// ListPoliciesForHost is the endpoint used by fleet UI/API clients.
|
||||
hostPolicies, err := ds.ListPoliciesForHost(context.Background(), tc.host)
|
||||
require.NoError(t, err)
|
||||
sort.Slice(hostPolicies, func(i, j int) bool {
|
||||
return hostPolicies[i].ID < hostPolicies[j].ID
|
||||
})
|
||||
sort.Slice(tc.expectedPolicies.hostPolicies, func(i, j int) bool {
|
||||
return tc.expectedPolicies.hostPolicies[i].ID < tc.expectedPolicies.hostPolicies[j].ID
|
||||
})
|
||||
require.Equal(t, tc.expectedPolicies.hostPolicies, hostPolicies)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
||||
@ -590,7 +833,7 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
|
||||
|
||||
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host1, map[uint]*bool{tp.ID: ptr.Bool(false), gp.ID: nil}, time.Now(), false))
|
||||
|
||||
policies, err := ds.ListPoliciesForHost(context.Background(), host1.ID)
|
||||
policies, err := ds.ListPoliciesForHost(context.Background(), host1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policies, 2)
|
||||
|
||||
@ -617,7 +860,7 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
|
||||
assert.NotNil(t, policies[1].Resolution)
|
||||
assert.Equal(t, "some other gp resolution", *policies[1].Resolution)
|
||||
|
||||
policies, err = ds.ListPoliciesForHost(context.Background(), host2.ID)
|
||||
policies, err = ds.ListPoliciesForHost(context.Background(), host2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policies, 1)
|
||||
|
||||
@ -627,7 +870,7 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
|
||||
|
||||
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host2, map[uint]*bool{gp.ID: ptr.Bool(true)}, time.Now(), false))
|
||||
|
||||
policies, err = ds.ListPoliciesForHost(context.Background(), host2.ID)
|
||||
policies, err = ds.ListPoliciesForHost(context.Background(), host2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policies, 1)
|
||||
|
||||
@ -642,7 +885,7 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host2, map[uint]*bool{uint(id): nil}, time.Now(), false))
|
||||
|
||||
policies, err = ds.ListPoliciesForHost(context.Background(), host2.ID)
|
||||
policies, err = ds.ListPoliciesForHost(context.Background(), host2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policies, 2)
|
||||
|
||||
@ -742,6 +985,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query1 desc",
|
||||
Resolution: "some resolution",
|
||||
Team: "",
|
||||
Platforms: "",
|
||||
},
|
||||
{
|
||||
Name: "query2",
|
||||
@ -749,6 +993,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query2 desc",
|
||||
Resolution: "some other resolution",
|
||||
Team: "team1",
|
||||
Platforms: "darwin",
|
||||
},
|
||||
{
|
||||
Name: "query3",
|
||||
@ -756,6 +1001,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query3 desc",
|
||||
Resolution: "some other good resolution",
|
||||
Team: "team1",
|
||||
Platforms: "windows,linux",
|
||||
},
|
||||
}))
|
||||
|
||||
@ -769,6 +1015,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, user1.ID, *policies[0].AuthorID)
|
||||
require.NotNil(t, policies[0].Resolution)
|
||||
assert.Equal(t, "some resolution", *policies[0].Resolution)
|
||||
assert.Equal(t, "", policies[0].Platforms)
|
||||
|
||||
teamPolicies, err := ds.ListTeamPolicies(ctx, team1.ID)
|
||||
require.NoError(t, err)
|
||||
@ -780,6 +1027,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, user1.ID, *teamPolicies[0].AuthorID)
|
||||
require.NotNil(t, teamPolicies[0].Resolution)
|
||||
assert.Equal(t, "some other resolution", *teamPolicies[0].Resolution)
|
||||
assert.Equal(t, "darwin", teamPolicies[0].Platforms)
|
||||
|
||||
assert.Equal(t, "query3", teamPolicies[1].Name)
|
||||
assert.Equal(t, "select 3;", teamPolicies[1].Query)
|
||||
@ -788,6 +1036,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, user1.ID, *teamPolicies[1].AuthorID)
|
||||
require.NotNil(t, teamPolicies[1].Resolution)
|
||||
assert.Equal(t, "some other good resolution", *teamPolicies[1].Resolution)
|
||||
assert.Equal(t, "windows,linux", teamPolicies[1].Platforms)
|
||||
|
||||
// Make sure apply is idempotent
|
||||
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
|
||||
@ -797,6 +1046,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query1 desc",
|
||||
Resolution: "some resolution",
|
||||
Team: "",
|
||||
Platforms: "",
|
||||
},
|
||||
{
|
||||
Name: "query2",
|
||||
@ -804,6 +1054,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query2 desc",
|
||||
Resolution: "some other resolution",
|
||||
Team: "team1",
|
||||
Platforms: "darwin",
|
||||
},
|
||||
{
|
||||
Name: "query3",
|
||||
@ -811,6 +1062,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query3 desc",
|
||||
Resolution: "some other good resolution",
|
||||
Team: "team1",
|
||||
Platforms: "windows,linux",
|
||||
},
|
||||
}))
|
||||
|
||||
@ -829,6 +1081,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query1 desc updated",
|
||||
Resolution: "some resolution updated",
|
||||
Team: "",
|
||||
Platforms: "linux",
|
||||
},
|
||||
{
|
||||
Name: "query2",
|
||||
@ -836,6 +1089,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query2 desc updated",
|
||||
Resolution: "some other resolution updated",
|
||||
Team: "team1",
|
||||
Platforms: "windows",
|
||||
},
|
||||
}))
|
||||
policies, err = ds.ListGlobalPolicies(ctx)
|
||||
@ -849,6 +1103,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, user1.ID, *policies[0].AuthorID)
|
||||
require.NotNil(t, policies[0].Resolution)
|
||||
assert.Equal(t, "some resolution updated", *policies[0].Resolution)
|
||||
assert.Equal(t, "linux", policies[0].Platforms)
|
||||
|
||||
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
|
||||
require.NoError(t, err)
|
||||
@ -862,6 +1117,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, team1.ID, *teamPolicies[0].TeamID)
|
||||
require.NotNil(t, teamPolicies[0].Resolution)
|
||||
assert.Equal(t, "some other resolution updated", *teamPolicies[0].Resolution)
|
||||
assert.Equal(t, "windows", teamPolicies[0].Platforms)
|
||||
|
||||
// The following will "move" the policy from global to a team.
|
||||
require.NoError(t, ds.ApplyPolicySpecs(ctx, user1.ID, []*fleet.PolicySpec{
|
||||
@ -871,6 +1127,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Description: "query1 desc team1",
|
||||
Resolution: "some resolution team1",
|
||||
Team: "team1",
|
||||
Platforms: "linux",
|
||||
},
|
||||
}))
|
||||
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
|
||||
@ -887,6 +1144,7 @@ func testApplyPolicySpec(t *testing.T, ds *Datastore) {
|
||||
Query: "select 53;",
|
||||
Description: "query2 desc global",
|
||||
Resolution: "some resolution global",
|
||||
Platforms: "windows",
|
||||
},
|
||||
}))
|
||||
teamPolicies, err = ds.ListTeamPolicies(ctx, team1.ID)
|
||||
|
@ -381,6 +381,7 @@ CREATE TABLE `policies` (
|
||||
`query` mediumtext NOT NULL,
|
||||
`description` mediumtext NOT NULL,
|
||||
`author_id` int(10) unsigned DEFAULT NULL,
|
||||
`platforms` varchar(255) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_policies_unique_name` (`name`),
|
||||
KEY `idx_policies_author_id` (`author_id`),
|
||||
|
@ -213,7 +213,7 @@ type Datastore interface {
|
||||
CountHostsInLabel(ctx context.Context, filter TeamFilter, lid uint, opt HostListOptions) (int, error)
|
||||
|
||||
// ListPoliciesForHost lists the policies that a host will check and whether they are passing
|
||||
ListPoliciesForHost(ctx context.Context, hid uint) ([]*HostPolicy, error)
|
||||
ListPoliciesForHost(ctx context.Context, host *Host) ([]*HostPolicy, error)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// TargetStore
|
||||
|
@ -74,24 +74,25 @@ type Host struct {
|
||||
// OsqueryHostID is the key used in the request context that is
|
||||
// used to retrieve host information. It is sent from osquery and may currently be
|
||||
// a GUID or a Host Name, but in either case, it MUST be unique
|
||||
OsqueryHostID string `json:"-" db:"osquery_host_id"`
|
||||
DetailUpdatedAt time.Time `json:"detail_updated_at" db:"detail_updated_at"` // Time that the host details were last updated
|
||||
LabelUpdatedAt time.Time `json:"label_updated_at" db:"label_updated_at"` // Time that the host labels were last updated
|
||||
PolicyUpdatedAt time.Time `json:"policy_updated_at" db:"policy_updated_at"` // Time that the host policies were last updated
|
||||
LastEnrolledAt time.Time `json:"last_enrolled_at" db:"last_enrolled_at"` // Time that the host last enrolled
|
||||
SeenTime time.Time `json:"seen_time" db:"seen_time"` // Time that the host was last "seen"
|
||||
RefetchRequested bool `json:"refetch_requested" db:"refetch_requested"`
|
||||
NodeKey string `json:"-" db:"node_key"`
|
||||
Hostname string `json:"hostname" db:"hostname"` // there is a fulltext index on this field
|
||||
UUID string `json:"uuid" db:"uuid"` // there is a fulltext index on this field
|
||||
Platform string `json:"platform"`
|
||||
OsqueryVersion string `json:"osquery_version" db:"osquery_version"`
|
||||
OSVersion string `json:"os_version" db:"os_version"`
|
||||
Build string `json:"build"`
|
||||
PlatformLike string `json:"platform_like" db:"platform_like"`
|
||||
CodeName string `json:"code_name" db:"code_name"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
Memory int64 `json:"memory" sql:"type:bigint" db:"memory"`
|
||||
OsqueryHostID string `json:"-" db:"osquery_host_id"`
|
||||
DetailUpdatedAt time.Time `json:"detail_updated_at" db:"detail_updated_at"` // Time that the host details were last updated
|
||||
LabelUpdatedAt time.Time `json:"label_updated_at" db:"label_updated_at"` // Time that the host labels were last updated
|
||||
PolicyUpdatedAt time.Time `json:"policy_updated_at" db:"policy_updated_at"` // Time that the host policies were last updated
|
||||
LastEnrolledAt time.Time `json:"last_enrolled_at" db:"last_enrolled_at"` // Time that the host last enrolled
|
||||
SeenTime time.Time `json:"seen_time" db:"seen_time"` // Time that the host was last "seen"
|
||||
RefetchRequested bool `json:"refetch_requested" db:"refetch_requested"`
|
||||
NodeKey string `json:"-" db:"node_key"`
|
||||
Hostname string `json:"hostname" db:"hostname"` // there is a fulltext index on this field
|
||||
UUID string `json:"uuid" db:"uuid"` // there is a fulltext index on this field
|
||||
// Platform is the host's platform as defined by osquery's os_version.platform.
|
||||
Platform string `json:"platform"`
|
||||
OsqueryVersion string `json:"osquery_version" db:"osquery_version"`
|
||||
OSVersion string `json:"os_version" db:"os_version"`
|
||||
Build string `json:"build"`
|
||||
PlatformLike string `json:"platform_like" db:"platform_like"`
|
||||
CodeName string `json:"code_name" db:"code_name"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
Memory int64 `json:"memory" sql:"type:bigint" db:"memory"`
|
||||
// system_info fields
|
||||
CPUType string `json:"cpu_type" db:"cpu_type"`
|
||||
CPUSubtype string `json:"cpu_subtype" db:"cpu_subtype"`
|
||||
@ -209,3 +210,25 @@ func (h *Host) IsNew(now time.Time) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FleetPlatform returns the host's generic platform as supported by Fleet.
|
||||
func (h *Host) FleetPlatform() string {
|
||||
return PlatformFromHost(h.Platform)
|
||||
}
|
||||
|
||||
// PlatformFromHost converts the given host platform into
|
||||
// the generic platforms known by osquery
|
||||
// https://osquery.readthedocs.io/en/stable/deployment/configuration/
|
||||
// and supported by Fleet.
|
||||
//
|
||||
// Returns empty string if hostPlatform is unknnown.
|
||||
func PlatformFromHost(hostPlatform string) string {
|
||||
switch hostPlatform {
|
||||
case "ubuntu", "rhel", "debian":
|
||||
return "linux"
|
||||
case "darwin", "windows":
|
||||
return hostPlatform
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package fleet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PolicyPayload holds data for policy creation.
|
||||
@ -20,15 +21,20 @@ type PolicyPayload struct {
|
||||
Query string
|
||||
// Description is the policy description text (ignored if QueryID != nil).
|
||||
Description string
|
||||
// Resolution indicate the steps needed to solve a failing policy.
|
||||
// Resolution indicates the steps needed to solve a failing policy.
|
||||
Resolution string
|
||||
// Platforms is a comma-separated string to indicate the target platforms.
|
||||
//
|
||||
// Empty string targets all platforms.
|
||||
Platforms string
|
||||
}
|
||||
|
||||
var (
|
||||
errPolicyEmptyName = errors.New("policy name cannot be empty")
|
||||
errPolicyEmptyQuery = errors.New("policy query cannot be empty")
|
||||
errPolicyIDAndQuerySet = errors.New("both fields \"queryID\" and \"query\" cannot be set")
|
||||
errPolicyInvalidQuery = errors.New("invalid policy query")
|
||||
errPolicyEmptyName = errors.New("policy name cannot be empty")
|
||||
errPolicyEmptyQuery = errors.New("policy query cannot be empty")
|
||||
errPolicyIDAndQuerySet = errors.New("both fields \"queryID\" and \"query\" cannot be set")
|
||||
errPolicyInvalidQuery = errors.New("invalid policy query")
|
||||
errPolicyInvalidPlatform = errors.New("invalid policy platform")
|
||||
)
|
||||
|
||||
// Verify verifies the policy payload is valid.
|
||||
@ -45,6 +51,9 @@ func (p PolicyPayload) Verify() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := verifyPolicyPlatforms(p.Platforms); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -65,6 +74,21 @@ func verifyPolicyQuery(query string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyPolicyPlatforms(platforms string) error {
|
||||
if platforms == "" {
|
||||
return nil
|
||||
}
|
||||
for _, s := range strings.Split(platforms, ",") {
|
||||
switch strings.TrimSpace(s) {
|
||||
case "windows", "linux", "darwin":
|
||||
// OK
|
||||
default:
|
||||
return errPolicyInvalidPlatform
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModifyPolicyPayload holds data for policy modification.
|
||||
type ModifyPolicyPayload struct {
|
||||
// Name is the name of the policy.
|
||||
@ -75,6 +99,10 @@ type ModifyPolicyPayload struct {
|
||||
Description *string `json:"description"`
|
||||
// Resolution indicate the steps needed to solve a failing policy.
|
||||
Resolution *string `json:"resolution"`
|
||||
// Platforms is a comma-separated string to indicate the target platforms.
|
||||
//
|
||||
// Empty string targets all platforms.
|
||||
Platforms *string `json:"platforms"`
|
||||
}
|
||||
|
||||
// Verify verifies the policy payload is valid.
|
||||
@ -89,6 +117,11 @@ func (p ModifyPolicyPayload) Verify() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if p.Platforms != nil {
|
||||
if err := verifyPolicyPlatforms(*p.Platforms); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -115,6 +148,10 @@ type PolicyData struct {
|
||||
TeamID *uint `json:"team_id" db:"team_id"`
|
||||
// Resolution describes how to solve a failing policy.
|
||||
Resolution *string `json:"resolution,omitempty" db:"resolution"`
|
||||
// Platforms is a comma-separated string to indicate the target platforms.
|
||||
//
|
||||
// Empty string targets all platforms.
|
||||
Platforms string `json:"platforms" db:"platforms"`
|
||||
|
||||
UpdateCreateTimestamps
|
||||
}
|
||||
@ -149,12 +186,23 @@ type HostPolicy struct {
|
||||
}
|
||||
|
||||
// PolicySpec is used to hold policy data to apply policy specs.
|
||||
//
|
||||
// Policies are currently identified by name (unique).
|
||||
type PolicySpec struct {
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
// Name is the name of the policy.
|
||||
Name string `json:"name"`
|
||||
// Query is the policy's SQL query.
|
||||
Query string `json:"query"`
|
||||
// Description describes the policy.
|
||||
Description string `json:"description"`
|
||||
Resolution string `json:"resolution,omitempty"`
|
||||
Team string `json:"team,omitempty"`
|
||||
// Resolution describes how to solve a failing policy.
|
||||
Resolution string `json:"resolution,omitempty"`
|
||||
// Team is the name of the team.
|
||||
Team string `json:"team,omitempty"`
|
||||
// Platforms is a comma-separated string to indicate the target platforms.
|
||||
//
|
||||
// Empty string targets all platforms.
|
||||
Platforms string `json:"platforms,omitempty"`
|
||||
}
|
||||
|
||||
// Verify verifies the policy data is valid.
|
||||
@ -165,5 +213,8 @@ func (p PolicySpec) Verify() error {
|
||||
if err := verifyPolicyQuery(p.Query); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := verifyPolicyPlatforms(p.Platforms); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ type CountHostsFunc func(ctx context.Context, filter fleet.TeamFilter, opt fleet
|
||||
|
||||
type CountHostsInLabelFunc func(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) (int, error)
|
||||
|
||||
type ListPoliciesForHostFunc func(ctx context.Context, hid uint) ([]*fleet.HostPolicy, error)
|
||||
type ListPoliciesForHostFunc func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error)
|
||||
|
||||
type CountHostsInTargetsFunc func(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error)
|
||||
|
||||
@ -1203,9 +1203,9 @@ func (s *DataStore) CountHostsInLabel(ctx context.Context, filter fleet.TeamFilt
|
||||
return s.CountHostsInLabelFunc(ctx, filter, lid, opt)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListPoliciesForHost(ctx context.Context, hid uint) ([]*fleet.HostPolicy, error) {
|
||||
func (s *DataStore) ListPoliciesForHost(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
s.ListPoliciesForHostFuncInvoked = true
|
||||
return s.ListPoliciesForHostFunc(ctx, hid)
|
||||
return s.ListPoliciesForHostFunc(ctx, host)
|
||||
}
|
||||
|
||||
func (s *DataStore) CountHostsInTargets(ctx context.Context, filter fleet.TeamFilter, targets fleet.HostTargets, now time.Time) (fleet.TargetMetrics, error) {
|
||||
|
@ -21,6 +21,7 @@ type globalPolicyRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Resolution string `json:"resolution"`
|
||||
Platforms string `json:"platforms"`
|
||||
}
|
||||
|
||||
type globalPolicyResponse struct {
|
||||
@ -38,6 +39,7 @@ func globalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Se
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Resolution: req.Resolution,
|
||||
Platforms: req.Platforms,
|
||||
})
|
||||
if err != nil {
|
||||
return globalPolicyResponse{Err: err}, nil
|
||||
|
@ -767,6 +767,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
|
||||
NodeKey: fmt.Sprintf("%s%d", t.Name(), i),
|
||||
UUID: fmt.Sprintf("%s%d", t.Name(), i),
|
||||
Hostname: fmt.Sprintf("%sfoo.local%d", t.Name(), i),
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@ -792,6 +793,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
|
||||
Query: "select * from osquery;",
|
||||
Description: "Some description",
|
||||
Resolution: "some global resolution",
|
||||
Platforms: "darwin",
|
||||
}
|
||||
gpResp := globalPolicyResponse{}
|
||||
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpParams, http.StatusOK, &gpResp)
|
||||
@ -805,6 +807,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
|
||||
assert.NotNil(t, gpResp.Policy.AuthorID)
|
||||
assert.Equal(t, "Test Name admin1@example.com", gpResp.Policy.AuthorName)
|
||||
assert.Equal(t, "admin1@example.com", gpResp.Policy.AuthorEmail)
|
||||
assert.Equal(t, "darwin", gpResp.Policy.Platforms)
|
||||
|
||||
mgpParams := modifyGlobalPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
@ -812,6 +815,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
|
||||
Query: ptr.String("select * from osquery_info;"),
|
||||
Description: ptr.String("Some description updated"),
|
||||
Resolution: ptr.String("some global resolution updated"),
|
||||
Platforms: ptr.String(""),
|
||||
},
|
||||
}
|
||||
mgpResp := modifyGlobalPolicyResponse{}
|
||||
@ -822,6 +826,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
|
||||
assert.Equal(t, "Some description updated", mgpResp.Policy.Description)
|
||||
require.NotNil(t, mgpResp.Policy.Resolution)
|
||||
assert.Equal(t, "some global resolution updated", *mgpResp.Policy.Resolution)
|
||||
assert.Equal(t, "", mgpResp.Policy.Platforms)
|
||||
|
||||
ggpResp := getPolicyByIDResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/global/policies/%d", gpResp.Policy.ID), getPolicyByIDRequest{}, http.StatusOK, &ggpResp)
|
||||
@ -831,6 +836,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
|
||||
assert.Equal(t, "Some description updated", ggpResp.Policy.Description)
|
||||
require.NotNil(t, ggpResp.Policy.Resolution)
|
||||
assert.Equal(t, "some global resolution updated", *ggpResp.Policy.Resolution)
|
||||
assert.Equal(t, "", mgpResp.Policy.Platforms)
|
||||
|
||||
policiesResponse := listGlobalPoliciesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/global/policies", nil, http.StatusOK, &policiesResponse)
|
||||
@ -840,6 +846,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesProprietary() {
|
||||
assert.Equal(t, "Some description updated", policiesResponse.Policies[0].Description)
|
||||
require.NotNil(t, policiesResponse.Policies[0].Resolution)
|
||||
assert.Equal(t, "some global resolution updated", *policiesResponse.Policies[0].Resolution)
|
||||
assert.Equal(t, "", policiesResponse.Policies[0].Platforms)
|
||||
|
||||
listHostsURL := fmt.Sprintf("/api/v1/fleet/hosts?policy_id=%d", policiesResponse.Policies[0].ID)
|
||||
listHostsResp := listHostsResponse{}
|
||||
@ -890,6 +897,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
|
||||
NodeKey: fmt.Sprintf("%s%d", t.Name(), i),
|
||||
UUID: fmt.Sprintf("%s%d", t.Name(), i),
|
||||
Hostname: fmt.Sprintf("%sfoo.local%d", t.Name(), i),
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hosts[i] = h.ID
|
||||
@ -902,6 +910,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
|
||||
Query: "select * from osquery;",
|
||||
Description: "Some description",
|
||||
Resolution: "some team resolution",
|
||||
Platforms: "darwin",
|
||||
}
|
||||
tpResp := teamPolicyResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpParams, http.StatusOK, &tpResp)
|
||||
@ -922,6 +931,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
|
||||
Query: ptr.String("select * from osquery_info;"),
|
||||
Description: ptr.String("Some description updated"),
|
||||
Resolution: ptr.String("some team resolution updated"),
|
||||
Platforms: ptr.String("darwin,windows"),
|
||||
},
|
||||
}
|
||||
mtpResp := modifyTeamPolicyResponse{}
|
||||
@ -932,6 +942,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
|
||||
assert.Equal(t, "Some description updated", mtpResp.Policy.Description)
|
||||
require.NotNil(t, mtpResp.Policy.Resolution)
|
||||
assert.Equal(t, "some team resolution updated", *mtpResp.Policy.Resolution)
|
||||
assert.Equal(t, "darwin,windows", mtpResp.Policy.Platforms)
|
||||
|
||||
gtpResp := getPolicyByIDResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d/policies/%d", team1.ID, tpResp.Policy.ID), getPolicyByIDRequest{}, http.StatusOK, >pResp)
|
||||
@ -941,6 +952,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
|
||||
assert.Equal(t, "Some description updated", gtpResp.Policy.Description)
|
||||
require.NotNil(t, gtpResp.Policy.Resolution)
|
||||
assert.Equal(t, "some team resolution updated", *gtpResp.Policy.Resolution)
|
||||
assert.Equal(t, "darwin,windows", gtpResp.Policy.Platforms)
|
||||
|
||||
policiesResponse := listTeamPoliciesResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &policiesResponse)
|
||||
@ -950,6 +962,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietary() {
|
||||
assert.Equal(t, "Some description updated", policiesResponse.Policies[0].Description)
|
||||
require.NotNil(t, policiesResponse.Policies[0].Resolution)
|
||||
assert.Equal(t, "some team resolution updated", *policiesResponse.Policies[0].Resolution)
|
||||
assert.Equal(t, "darwin,windows", policiesResponse.Policies[0].Platforms)
|
||||
|
||||
listHostsURL := fmt.Sprintf("/api/v1/fleet/hosts?policy_id=%d", policiesResponse.Policies[0].ID)
|
||||
listHostsResp := listHostsResponse{}
|
||||
@ -1019,6 +1032,7 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietaryInvalid() {
|
||||
queryID *uint
|
||||
name string
|
||||
query string
|
||||
platforms string
|
||||
}{
|
||||
{
|
||||
tname: "set both QueryID and Query",
|
||||
@ -1045,12 +1059,20 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietaryInvalid() {
|
||||
name: "Invalid query",
|
||||
query: "ATTACH 'foo' AS bar;",
|
||||
},
|
||||
{
|
||||
tname: "Invalid platforms",
|
||||
testUpdate: true,
|
||||
name: "Some query",
|
||||
query: "select 42;",
|
||||
platforms: "linux1",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.tname, func(t *testing.T) {
|
||||
tpReq := teamPolicyRequest{
|
||||
QueryID: tc.queryID,
|
||||
Name: tc.name,
|
||||
Query: tc.query,
|
||||
QueryID: tc.queryID,
|
||||
Name: tc.name,
|
||||
Query: tc.query,
|
||||
Platforms: tc.platforms,
|
||||
}
|
||||
tpResp := teamPolicyResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/teams/%d/policies", team1.ID), tpReq, http.StatusBadRequest, &tpResp)
|
||||
@ -1061,8 +1083,9 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietaryInvalid() {
|
||||
if testUpdate {
|
||||
tpReq := modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
Name: ptr.String(tc.name),
|
||||
Query: ptr.String(tc.query),
|
||||
Name: ptr.String(tc.name),
|
||||
Query: ptr.String(tc.query),
|
||||
Platforms: ptr.String(tc.platforms),
|
||||
},
|
||||
}
|
||||
tpResp := modifyTeamPolicyResponse{}
|
||||
@ -1071,9 +1094,10 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietaryInvalid() {
|
||||
}
|
||||
|
||||
gpReq := globalPolicyRequest{
|
||||
QueryID: tc.queryID,
|
||||
Name: tc.name,
|
||||
Query: tc.query,
|
||||
QueryID: tc.queryID,
|
||||
Name: tc.name,
|
||||
Query: tc.query,
|
||||
Platforms: tc.platforms,
|
||||
}
|
||||
gpResp := globalPolicyResponse{}
|
||||
s.DoJSON("POST", "/api/v1/fleet/global/policies", gpReq, http.StatusBadRequest, &gpResp)
|
||||
@ -1082,8 +1106,9 @@ func (s *integrationTestSuite) TestTeamPoliciesProprietaryInvalid() {
|
||||
if testUpdate {
|
||||
gpReq := modifyGlobalPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
Name: ptr.String(tc.name),
|
||||
Query: ptr.String(tc.query),
|
||||
Name: ptr.String(tc.name),
|
||||
Query: ptr.String(tc.query),
|
||||
Platforms: ptr.String(tc.platforms),
|
||||
},
|
||||
}
|
||||
gpResp := modifyGlobalPolicyResponse{}
|
||||
|
@ -61,7 +61,7 @@ func (svc Service) getHostDetails(ctx context.Context, host *fleet.Host) (*fleet
|
||||
return nil, ctxerr.Wrap(ctx, err, "get packs for host")
|
||||
}
|
||||
|
||||
policies, err := svc.ds.ListPoliciesForHost(ctx, host.ID)
|
||||
policies, err := svc.ds.ListPoliciesForHost(ctx, host)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get policies for host")
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ func TestHostDetails(t *testing.T) {
|
||||
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.HostPolicy, error) {
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ func TestHostAuth(t *testing.T) {
|
||||
ds.SaveHostFunc = func(ctx context.Context, host *fleet.Host) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.HostPolicy, error) {
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.DeleteHostsFunc = func(ctx context.Context, ids []uint) error {
|
||||
|
@ -23,6 +23,7 @@ type teamPolicyRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Resolution string `json:"resolution"`
|
||||
Platforms string `json:"platforms"`
|
||||
}
|
||||
|
||||
type teamPolicyResponse struct {
|
||||
@ -40,6 +41,7 @@ func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
|
||||
Query: req.Query,
|
||||
Description: req.Description,
|
||||
Resolution: req.Resolution,
|
||||
Platforms: req.Platforms,
|
||||
})
|
||||
if err != nil {
|
||||
return teamPolicyResponse{Err: err}, nil
|
||||
@ -260,6 +262,9 @@ func (svc Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p fl
|
||||
if p.Resolution != nil {
|
||||
policy.Resolution = p.Resolution
|
||||
}
|
||||
if p.Platforms != nil {
|
||||
policy.Platforms = *p.Platforms
|
||||
}
|
||||
logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query)
|
||||
|
||||
err = svc.ds.SavePolicy(ctx, policy)
|
||||
|
Loading…
Reference in New Issue
Block a user