Add ability to mark policy as critical. (#8896)

This commit is contained in:
Frank Sievertsen
2022-12-06 15:59:20 +01:00
committed by GitHub
parent 8dee862136
commit e0766d21a3
16 changed files with 209 additions and 107 deletions
@@ -0,0 +1 @@
* Add ability to mark policy as critical.
+4 -2
View File
@@ -62,7 +62,8 @@
"resolution": "Some resolution",
"team_id": 1,
"updated_at": "0001-01-01T00:00:00Z",
"created_at": "0001-01-01T00:00:00Z"
"created_at": "0001-01-01T00:00:00Z",
"critical": false
},
{
"id": 2,
@@ -76,7 +77,8 @@
"response": "fails",
"team_id": null,
"updated_at": "0001-01-01T00:00:00Z",
"created_at": "0001-01-01T00:00:00Z"
"created_at": "0001-01-01T00:00:00Z",
"critical": false
}
],
"status": "offline",
@@ -51,6 +51,7 @@ spec:
team_id: 1
created_at: "0001-01-01T00:00:00Z"
updated_at: "0001-01-01T00:00:00Z"
critical: false
- author_email: "alice@example.com"
author_id: 1
author_name: Alice
@@ -63,6 +64,7 @@ spec:
team_id: null
created_at: "0001-01-01T00:00:00Z"
updated_at: "0001-01-01T00:00:00Z"
critical: false
policy_updated_at: "0001-01-01T00:00:00Z"
public_ip: ""
primary_ip: ""
+31 -7
View File
@@ -2128,7 +2128,8 @@ Returns the information of the specified host.
"description": "this is a query",
"resolution": "fix with these steps...",
"platform": "windows,linux",
"response": "pass"
"response": "pass",
"critical": false
},
{
"id": 2,
@@ -2137,7 +2138,8 @@ Returns the information of the specified host.
"description": "this is another query",
"resolution": "fix with these other steps...",
"platform": "darwin",
"response": "fail"
"response": "fail",
"critical": false
},
{
"id": 3,
@@ -2146,7 +2148,8 @@ Returns the information of the specified host.
"description": "",
"resolution": "",
"platform": "",
"response": ""
"response": "",
"critical": false
}
],
"issues": {
@@ -2319,8 +2322,9 @@ Returns the information of the host specified using the `uuid`, `osquery_host_id
"platform": "darwin,linux",
"created_at": "2022-09-02T18:52:19Z",
"updated_at": "2022-09-02T18:52:19Z",
"response": "fail"
},
"response": "fail",
"critical": false
}
],
"batteries": [
{
@@ -3370,6 +3374,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"critical": false,
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
@@ -3386,6 +3391,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
"name": "Windows machines with encrypted hard disks",
"query": "SELECT 1 FROM bitlocker_info WHERE protection_status = 1;",
"description": "Checks if the hard disk is encrypted on Windows devices",
"critical": true,
"author_id": 43,
"author_name": "Alice",
"author_email": "alice@example.com",
@@ -3426,6 +3432,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"critical": false,
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
@@ -3461,6 +3468,7 @@ An error is returned if both "query" and "query_id" are set on the request.
| resolution | string | body | The resolution steps for the policy. |
| query_id | integer | body | An existing query's ID (legacy). |
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | Mark policy as critical/high impact. |
Either `query` or `query_id` must be provided.
@@ -3476,7 +3484,8 @@ Either `query` or `query_id` must be provided.
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"resolution": "Resolution steps",
"platform": "darwin"
"platform": "darwin",
"critical": true
}
```
@@ -3491,6 +3500,7 @@ 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",
"critical": true,
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
@@ -3530,6 +3540,7 @@ 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",
"critical": true,
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
@@ -3590,6 +3601,7 @@ Where `query_id` references an existing `query`.
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | Mark policy as critical/high impact. |
#### Example Edit Policy
@@ -3602,6 +3614,7 @@ 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",
"critical": true,
"resolution": "Resolution steps",
"platform": "darwin"
}
@@ -3618,6 +3631,7 @@ 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",
"critical": true,
"author_id": 43,
"author_name": "John",
"author_email": "john@example.com",
@@ -3654,7 +3668,7 @@ Team policies work the same as policies, but at the team level.
| Name | Type | In | Description |
| ------------------ | ------- | ---- | ------------------------------------------------------------------------------------------------------------- |
| id | integer | url | Required. Defines what team id to operate on |
| id | integer | url | Required. Defines what team id to operate on |
#### Example
@@ -3672,6 +3686,7 @@ Team policies work the same as policies, but at the team level.
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"critical": true,
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
@@ -3688,6 +3703,7 @@ Team policies work the same as policies, but at the team level.
"name": "Windows machines with encrypted hard disks",
"query": "SELECT 1 FROM bitlocker_info WHERE protection_status = 1;",
"description": "Checks if the hard disk is encrypted on Windows devices",
"critical": false,
"author_id": 43,
"author_name": "Alice",
"author_email": "alice@example.com",
@@ -3706,6 +3722,7 @@ Team policies work the same as policies, but at the team level.
"name": "Arbitrary Test Policy (all platforms) (all teams)",
"query": "SELECT 1 FROM osquery_info WHERE 1=1;",
"description": "If you're seeing this, mostly likely this is because someone is testing out failing policies in dogfood. You can ignore this.",
"critical": true,
"author_id": 77,
"author_name": "Test Admin",
"author_email": "test@admin.com",
@@ -3747,6 +3764,7 @@ Team policies work the same as policies, but at the team level.
"name": "Gatekeeper enabled",
"query": "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;",
"description": "Checks if gatekeeper is enabled on macOS devices",
"critical": true,
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
@@ -3778,6 +3796,7 @@ The semantics for creating a team policy are the same as for global policies, se
| resolution | string | body | The resolution steps for the policy. |
| query_id | integer | body | An existing query's ID (legacy). |
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | Mark policy as critical/high impact. |
Either `query` or `query_id` must be provided.
@@ -3792,6 +3811,7 @@ 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",
"critical": true,
"resolution": "Resolution steps",
"platform": "darwin"
}
@@ -3808,6 +3828,7 @@ 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",
"critical": true,
"author_id": 42,
"author_name": "John",
"author_email": "john@example.com",
@@ -3870,6 +3891,7 @@ Either `query` or `query_id` must be provided.
| description | string | body | The query's description. |
| resolution | string | body | The resolution steps for the policy. |
| platform | string | body | Comma-separated target platforms, currently supported values are "windows", "linux", "darwin". The default, an empty string means target all platforms. |
| critical | boolean | body | Mark policy as critical/high impact. |
#### Example Edit Policy
@@ -3882,6 +3904,7 @@ 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",
"critical": true,
"resolution": "Resolution steps",
"platform": "darwin"
}
@@ -3898,6 +3921,7 @@ 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",
"critical": true,
"author_id": 43,
"author_name": "John",
"author_email": "john@example.com",
@@ -0,0 +1,26 @@
package tables
import (
"database/sql"
"github.com/pkg/errors"
)
func init() {
MigrationClient.AddMigration(Up_20221130114928, Down_20221130114928)
}
func Up_20221130114928(tx *sql.Tx) error {
_, err := tx.Exec(`
ALTER TABLE policies ADD COLUMN critical TINYINT(1) NOT NULL DEFAULT FALSE;
`)
if err != nil {
return errors.Wrapf(err, "adding column critical")
}
return nil
}
func Down_20221130114928(*sql.Tx) error {
return nil
}
+20 -31
View File
@@ -16,6 +16,11 @@ import (
"github.com/jmoiron/sqlx"
)
const policyCols = `
p.id, p.team_id, p.resolution, p.name, p.query, p.description,
p.author_id, p.platforms, p.created_at, p.updated_at, p.critical
`
func (ds *Datastore) NewGlobalPolicy(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
if args.QueryID != nil {
q, err := ds.Query(ctx, *args.QueryID)
@@ -27,8 +32,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, platforms) VALUES (?, ?, ?, ?, ?, ?)`,
args.Name, args.Query, args.Description, args.Resolution, authorID, args.Platform,
`INSERT INTO policies (name, query, description, resolution, author_id, platforms, critical) VALUES (?, ?, ?, ?, ?, ?, ?)`,
args.Name, args.Query, args.Description, args.Resolution, authorID, args.Platform, args.Critical,
)
switch {
case err == nil:
@@ -59,7 +64,7 @@ func policyDB(ctx context.Context, q sqlx.QueryerContext, id uint, teamID *uint)
var policy fleet.Policy
err := sqlx.GetContext(ctx, q, &policy,
fmt.Sprintf(`SELECT p.*,
fmt.Sprintf(`SELECT `+policyCols+`,
COALESCE(u.name, '<deleted>') AS author_name,
COALESCE(u.email, '') AS author_email,
(select count(*) from policy_membership where policy_id=p.id and passes=true) as passing_host_count,
@@ -83,10 +88,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 = ?, platforms = ?
SET name = ?, query = ?, description = ?, resolution = ?, platforms = ?, critical = ?
WHERE id = ?
`
result, err := ds.writer.ExecContext(ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.ID)
result, err := ds.writer.ExecContext(ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.Critical, p.ID)
if err != nil {
return ctxerr.Wrap(ctx, err, "updating policy")
}
@@ -292,16 +297,7 @@ func listPoliciesDB(ctx context.Context, q sqlx.QueryerContext, teamID, countsFo
ctx,
q,
&policies,
fmt.Sprintf(`SELECT p.id,
p.team_id,
p.resolution,
p.name,
p.query,
p.description,
p.author_id,
p.platforms,
p.created_at,
p.updated_at,
fmt.Sprintf(`SELECT `+policyCols+`,
COALESCE(u.name, '<deleted>') AS author_name,
COALESCE(u.email, '') AS author_email,
%s
@@ -316,16 +312,7 @@ func listPoliciesDB(ctx context.Context, q sqlx.QueryerContext, teamID, countsFo
}
func (ds *Datastore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) {
sql := `SELECT p.id,
p.team_id,
p.resolution,
p.name,
p.query,
p.description,
p.author_id,
p.platforms,
p.created_at,
p.updated_at,
sql := `SELECT ` + policyCols + `,
COALESCE(u.name, '<deleted>') AS author_name,
COALESCE(u.email, '') AS author_email,
(select count(*) from policy_membership where policy_id=p.id and passes=true) as passing_host_count,
@@ -440,8 +427,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, platforms) VALUES (?, ?, ?, ?, ?, ?, ?)`,
args.Name, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platform)
`INSERT INTO policies (name, query, description, team_id, resolution, author_id, platforms, critical) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
args.Name, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platform, args.Critical)
switch {
case err == nil:
// OK
@@ -495,19 +482,21 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
author_id,
resolution,
team_id,
platforms
) VALUES ( ?, ?, ?, ?, ?, (SELECT IFNULL(MIN(id), NULL) FROM teams WHERE name = ?), ? )
platforms,
critical
) 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),
platforms = VALUES(platforms)
platforms = VALUES(platforms),
critical = VALUES(critical)
`
for _, spec := range specs {
res, err := tx.ExecContext(ctx,
sql, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team, spec.Platform,
sql, spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, spec.Team, spec.Platform, spec.Critical,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
+32 -25
View File
@@ -1296,52 +1296,59 @@ func testPoliciesSave(t *testing.T, ds *Datastore) {
var nfe *notFoundError
require.True(t, errors.As(err, &nfe))
gp, err := ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
payload := fleet.PolicyPayload{
Name: "global query",
Query: "select 1;",
Description: "global query desc",
Resolution: "global query resolution",
})
}
gp, err := ds.NewGlobalPolicy(ctx, &user1.ID, payload)
require.NoError(t, err)
require.Equal(t, gp.Name, payload.Name)
require.Equal(t, gp.Query, payload.Query)
require.Equal(t, gp.Description, payload.Description)
require.Equal(t, *gp.Resolution, payload.Resolution)
require.Equal(t, gp.Critical, payload.Critical)
tp1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
payload = fleet.PolicyPayload{
Name: "team1 query",
Query: "select 2;",
Description: "team1 query desc",
Resolution: "team1 query resolution",
})
Critical: true,
}
tp1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, payload)
require.NoError(t, err)
require.Equal(t, tp1.Name, payload.Name)
require.Equal(t, tp1.Query, payload.Query)
require.Equal(t, tp1.Description, payload.Description)
require.Equal(t, *tp1.Resolution, payload.Resolution)
require.Equal(t, tp1.Critical, payload.Critical)
// Change name only of a global query.
gp.Name = "global query updated"
err = ds.SavePolicy(ctx, gp)
gp2 := *gp
gp2.Name = "global query updated"
gp2.Critical = true
err = ds.SavePolicy(ctx, &gp2)
require.NoError(t, err)
gp, err = ds.Policy(ctx, gp.ID)
require.NoError(t, err)
assert.Equal(t, "global query updated", gp.Name)
assert.Equal(t, "select 1;", gp.Query)
assert.Equal(t, "global query desc", gp.Description)
require.NotNil(t, gp.Resolution)
assert.Equal(t, "global query resolution", *gp.Resolution)
require.NotNil(t, gp.AuthorID)
assert.Equal(t, user1.ID, *gp.AuthorID)
gp2.UpdateCreateTimestamps = gp.UpdateCreateTimestamps
require.Equal(t, &gp2, gp)
// Change name, query, description and resolution of a team policy.
tp1.Name = "team1 query updated"
tp1.Query = "select 12;"
tp1.Description = "team1 query desc updated"
tp1.Resolution = ptr.String("team1 query resolution updated")
err = ds.SavePolicy(ctx, tp1)
tp2 := *tp1
tp2.Name = "team1 query updated"
tp2.Query = "select 12;"
tp2.Description = "team1 query desc updated"
tp2.Resolution = ptr.String("team1 query resolution updated")
tp2.Critical = false
err = ds.SavePolicy(ctx, &tp2)
require.NoError(t, err)
tp1, err = ds.Policy(ctx, tp1.ID)
tp2.UpdateCreateTimestamps = tp1.UpdateCreateTimestamps
require.NoError(t, err)
assert.Equal(t, "team1 query updated", tp1.Name)
assert.Equal(t, "select 12;", tp1.Query)
assert.Equal(t, "team1 query desc updated", tp1.Description)
require.NotNil(t, tp1.Resolution)
assert.Equal(t, "team1 query resolution updated", *tp1.Resolution)
require.NotNil(t, tp1.AuthorID)
assert.Equal(t, user1.ID, *tp1.AuthorID)
require.Equal(t, tp1, &tp2)
}
func testPoliciesDelUser(t *testing.T, ds *Datastore) {
File diff suppressed because one or more lines are too long
+8
View File
@@ -19,6 +19,8 @@ type PolicyPayload struct {
Name string
// Query is the policy query (ignored if QueryID != nil).
Query string
// Critical marks the policy as high impact.
Critical bool
// Description is the policy description text (ignored if QueryID != nil).
Description string
// Resolution indicates the steps needed to solve a failing policy.
@@ -106,6 +108,8 @@ type ModifyPolicyPayload struct {
// Platform is a comma-separated string to indicate the target platforms.
// If non-nil, empty string targets all platforms.
Platform *string `json:"platform"`
// Critical marks the policy as high impact.
Critical *bool `json:"critical"`
}
// Verify verifies the policy payload is valid.
@@ -136,6 +140,8 @@ type PolicyData struct {
Name string `json:"name" db:"name"`
// Query is the actual query to run on the osquery agents.
Query string `json:"query" db:"query"`
// Critical marks the policy as high impact.
Critical bool `json:"critical" db:"critical"`
// Description describes the policy.
Description string `json:"description" db:"description"`
// AuthorID is the ID of the author of the policy.
@@ -198,6 +204,8 @@ type PolicySpec struct {
Query string `json:"query"`
// Description describes the policy.
Description string `json:"description"`
// Critical marks the policy as high impact.
Critical bool `json:"critical"`
// Resolution describes how to solve a failing policy.
Resolution string `json:"resolution,omitempty"`
// Team is the name of the team.
+3
View File
@@ -321,6 +321,9 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
if p.Platform != nil {
policy.Platform = *p.Platform
}
if p.Critical != nil {
policy.Critical = *p.Critical
}
logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query)
err = svc.ds.SavePolicy(ctx, policy)
+5 -2
View File
@@ -43,6 +43,7 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) {
TeamID: nil,
Resolution: ptr.String("policy1 resolution"),
Platform: "darwin",
Critical: true,
},
}, nil
}
@@ -121,7 +122,8 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) {
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "0001-01-01T00:00:00Z",
"passing_host_count": 0,
"failing_host_count": 0
"failing_host_count": 0,
"critical": true
},
"hosts": [
{
@@ -304,7 +306,8 @@ func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) {
"created_at": "0001-01-01T00:00:00Z",
"updated_at": "0001-01-01T00:00:00Z",
"passing_host_count": 0,
"failing_host_count": 0
"failing_host_count": 0,
"critical": false
},
"hosts": [
{
+20 -15
View File
@@ -73,7 +73,9 @@ This issue was created automatically by your Fleet Jira integration.
)),
FailingPolicyDescription: template.Must(template.New("").Parse(
`Hosts:
`{{ if .PolicyCritical }}This policy is marked as *Critical* in Fleet.
{{ end }}Hosts:
{{ $end := len .Hosts }}{{ if gt $end 50 }}{{ $end = 50 }}{{ end }}
{{ range slice .Hosts 0 $end }}
* [{{ .DisplayName }}|{{ $.FleetURL }}/hosts/{{ .ID }}]
@@ -102,11 +104,12 @@ type jiraVulnTplArgs struct {
}
type jiraFailingPoliciesTplArgs struct {
FleetURL string
PolicyID uint
PolicyName string
TeamID *uint
Hosts []fleet.PolicySetHost
FleetURL string
PolicyID uint
PolicyName string
PolicyCritical bool
TeamID *uint
Hosts []fleet.PolicySetHost
}
// JiraClient defines the method required for the client that makes API calls
@@ -304,11 +307,12 @@ func (j *Jira) runVuln(ctx context.Context, cli JiraClient, args jiraArgs) error
func (j *Jira) runFailingPolicy(ctx context.Context, cli JiraClient, args jiraArgs) error {
tplArgs := &jiraFailingPoliciesTplArgs{
FleetURL: j.FleetURL,
PolicyName: args.FailingPolicy.PolicyName,
PolicyID: args.FailingPolicy.PolicyID,
TeamID: args.FailingPolicy.TeamID,
Hosts: args.FailingPolicy.Hosts,
FleetURL: j.FleetURL,
PolicyName: args.FailingPolicy.PolicyName,
PolicyID: args.FailingPolicy.PolicyID,
PolicyCritical: args.FailingPolicy.PolicyCritical,
TeamID: args.FailingPolicy.TeamID,
Hosts: args.FailingPolicy.Hosts,
}
createdIssue, err := j.createTemplatedIssue(ctx, cli, jiraTemplates.FailingPolicySummary, jiraTemplates.FailingPolicyDescription, tplArgs)
@@ -424,10 +428,11 @@ func QueueJiraFailingPolicyJob(ctx context.Context, ds fleet.Datastore, logger k
level.Info(logger).Log(attrs...)
args := &failingPolicyArgs{
PolicyID: policy.ID,
PolicyName: policy.Name,
Hosts: hosts,
TeamID: policy.TeamID,
PolicyID: policy.ID,
PolicyName: policy.Name,
PolicyCritical: policy.Critical,
Hosts: hosts,
TeamID: policy.TeamID,
}
job, err := QueueJob(ctx, ds, jiraName, jiraArgs{FailingPolicy: args})
if err != nil {
+19 -3
View File
@@ -311,10 +311,12 @@ func TestJiraQueueFailingPolicyJob(t *testing.T) {
}
type mockJiraClient struct {
opts externalsvc.JiraOptions
opts externalsvc.JiraOptions
issues []jira.Issue
}
func (c *mockJiraClient) CreateJiraIssue(ctx context.Context, issue *jira.Issue) (*jira.Issue, error) {
c.issues = append(c.issues, *issue)
return &jira.Issue{}, nil
}
@@ -377,6 +379,7 @@ func TestJiraRunClientUpdate(t *testing.T) {
}
var projectKeys []string
var clients []*mockJiraClient
jiraJob := &Jira{
FleetURL: "http://example.com",
Datastore: ds,
@@ -384,7 +387,9 @@ func TestJiraRunClientUpdate(t *testing.T) {
NewClientFunc: func(opts *externalsvc.JiraOptions) (JiraClient, error) {
// keep track of project keys received in calls to NewClientFunc
projectKeys = append(projectKeys, opts.ProjectKey)
return &mockJiraClient{opts: *opts}, nil
client := &mockJiraClient{opts: *opts}
clients = append(clients, client)
return client, nil
},
}
@@ -398,7 +403,7 @@ func TestJiraRunClientUpdate(t *testing.T) {
require.NoError(t, err)
// run it globally again - it will reuse the cached client
err = jiraJob.Run(ctx, json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": []}}`))
err = jiraJob.Run(ctx, json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": [], "policy_critical": true}}`))
require.NoError(t, err)
// run it for team 123 a second time
@@ -413,4 +418,15 @@ func TestJiraRunClientUpdate(t *testing.T) {
require.Equal(t, []string{"0", "1", "2"}, projectKeys)
require.Equal(t, 5, globalCount) // app config is requested every time
require.Equal(t, 3, teamCount)
require.Len(t, clients, 3)
require.Len(t, clients[0].issues, 2)
require.NotContains(t, clients[0].issues[0].Fields.Description, "Critical")
require.Contains(t, clients[0].issues[1].Fields.Description, "Critical")
require.Len(t, clients[1].issues, 1)
require.NotContains(t, clients[1].issues[0].Fields.Description, "Critical")
require.Len(t, clients[2].issues, 1)
require.NotContains(t, clients[2].issues[0].Fields.Description, "Critical")
}
+5 -4
View File
@@ -37,10 +37,11 @@ type Job interface {
// failingPolicyArgs are the args common to all integrations that can process
// failing policies.
type failingPolicyArgs struct {
PolicyID uint `json:"policy_id"`
PolicyName string `json:"policy_name"`
Hosts []fleet.PolicySetHost `json:"hosts"`
TeamID *uint `json:"team_id,omitempty"`
PolicyID uint `json:"policy_id"`
PolicyName string `json:"policy_name"`
PolicyCritical bool `json:"policy_critical"`
Hosts []fleet.PolicySetHost `json:"hosts"`
TeamID *uint `json:"team_id,omitempty"`
}
// vulnArgs are the args common to all integrations that can process
+10 -13
View File
@@ -74,7 +74,9 @@ This ticket was created automatically by your Fleet Zendesk integration.
)),
FailingPolicyDescription: template.Must(template.New("").Parse(
`Hosts:
`{{ if .PolicyCritical }}This policy is marked as **Critical** in Fleet.
{{ end }}Hosts:
{{ $end := len .Hosts }}{{ if gt $end 50 }}{{ $end = 50 }}{{ end }}
{{ range slice .Hosts 0 $end }}
* [{{ .DisplayName }}]({{ $.FleetURL }}/hosts/{{ .ID }})
@@ -102,13 +104,7 @@ type zendeskVulnTplArgs struct {
CISAKnownExploit *bool
}
type zendeskFailingPoliciesTplArgs struct {
FleetURL string
PolicyID uint
PolicyName string
TeamID *uint
Hosts []fleet.PolicySetHost
}
type zendeskFailingPoliciesTplArgs jiraFailingPoliciesTplArgs
// ZendeskClient defines the method required for the client that makes API calls
// to Zendesk.
@@ -305,11 +301,12 @@ func (z *Zendesk) runVuln(ctx context.Context, cli ZendeskClient, args zendeskAr
func (z *Zendesk) runFailingPolicy(ctx context.Context, cli ZendeskClient, args zendeskArgs) error {
tplArgs := &zendeskFailingPoliciesTplArgs{
FleetURL: z.FleetURL,
PolicyName: args.FailingPolicy.PolicyName,
PolicyID: args.FailingPolicy.PolicyID,
TeamID: args.FailingPolicy.TeamID,
Hosts: args.FailingPolicy.Hosts,
FleetURL: z.FleetURL,
PolicyName: args.FailingPolicy.PolicyName,
PolicyID: args.FailingPolicy.PolicyID,
PolicyCritical: args.FailingPolicy.PolicyCritical,
TeamID: args.FailingPolicy.TeamID,
Hosts: args.FailingPolicy.Hosts,
}
createdTicket, err := z.createTemplatedTicket(ctx, cli, zendeskTemplates.FailingPolicySummary, zendeskTemplates.FailingPolicyDescription, tplArgs)
+20 -3
View File
@@ -296,10 +296,12 @@ func TestZendeskQueueFailingPolicyJob(t *testing.T) {
}
type mockZendeskClient struct {
opts externalsvc.ZendeskOptions
opts externalsvc.ZendeskOptions
tickets []zendesk.Ticket
}
func (c *mockZendeskClient) CreateZendeskTicket(ctx context.Context, ticket *zendesk.Ticket) (*zendesk.Ticket, error) {
c.tickets = append(c.tickets, *ticket)
return &zendesk.Ticket{}, nil
}
@@ -362,6 +364,7 @@ func TestZendeskRunClientUpdate(t *testing.T) {
}
var groupIDs []int64
var clients []*mockZendeskClient
zendeskJob := &Zendesk{
FleetURL: "http://example.com",
Datastore: ds,
@@ -369,7 +372,9 @@ func TestZendeskRunClientUpdate(t *testing.T) {
NewClientFunc: func(opts *externalsvc.ZendeskOptions) (ZendeskClient, error) {
// keep track of group IDs received in calls to NewClientFunc
groupIDs = append(groupIDs, opts.GroupID)
return &mockZendeskClient{opts: *opts}, nil
c := &mockZendeskClient{opts: *opts}
clients = append(clients, c)
return c, nil
},
}
@@ -384,7 +389,7 @@ func TestZendeskRunClientUpdate(t *testing.T) {
require.NoError(t, err)
// run it globally again - it will reuse the cached client
err = zendeskJob.Run(ctx, json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": []}}`))
err = zendeskJob.Run(ctx, json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": [], "policy_critical": true}}`))
require.NoError(t, err)
// run it for team 123 a second time
@@ -399,4 +404,16 @@ func TestZendeskRunClientUpdate(t *testing.T) {
require.Equal(t, []int64{0, 1, 2}, groupIDs)
require.Equal(t, 5, globalCount) // app config is requested every time
require.Equal(t, 3, teamCount)
require.Len(t, clients, 3)
require.Len(t, clients[0].tickets, 2)
require.NotContains(t, clients[0].tickets[0].Comment.Body, "Critical")
require.Contains(t, clients[0].tickets[1].Comment.Body, "Critical")
require.Len(t, clients[1].tickets, 1)
require.NotContains(t, clients[1].tickets[0].Comment.Body, "Critical")
require.Len(t, clients[2].tickets, 1)
require.NotContains(t, clients[2].tickets[0].Comment.Body, "Critical")
}