From e0766d21a34367c7c7db62d49fa96c00c22c8b7b Mon Sep 17 00:00:00 2001 From: Frank Sievertsen Date: Tue, 6 Dec 2022 15:59:20 +0100 Subject: [PATCH] Add ability to mark policy as critical. (#8896) --- changes/issue-7805-mark-policy-as-critical | 1 + .../expectedHostDetailResponseJson.json | 6 +- .../expectedHostDetailResponseYaml.yml | 2 + docs/Using-Fleet/REST-API.md | 38 ++++++++++--- .../20221130114928_AddPolicyCritical.go | 26 +++++++++ server/datastore/mysql/policies.go | 51 +++++++---------- server/datastore/mysql/policies_test.go | 57 +++++++++++-------- server/datastore/mysql/schema.sql | 5 +- server/fleet/policies.go | 8 +++ server/service/team_policies.go | 3 + server/webhooks/failing_policies_test.go | 7 ++- server/worker/jira.go | 35 +++++++----- server/worker/jira_test.go | 22 ++++++- server/worker/worker.go | 9 +-- server/worker/zendesk.go | 23 ++++---- server/worker/zendesk_test.go | 23 +++++++- 16 files changed, 209 insertions(+), 107 deletions(-) create mode 100644 changes/issue-7805-mark-policy-as-critical create mode 100644 server/datastore/mysql/migrations/tables/20221130114928_AddPolicyCritical.go diff --git a/changes/issue-7805-mark-policy-as-critical b/changes/issue-7805-mark-policy-as-critical new file mode 100644 index 0000000000..745b066087 --- /dev/null +++ b/changes/issue-7805-mark-policy-as-critical @@ -0,0 +1 @@ +* Add ability to mark policy as critical. \ No newline at end of file diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json index 6686f07dca..a9ec1ae940 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json @@ -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", diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml index 54bf06efb8..1ae5ac5ce3 100644 --- a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml @@ -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: "" diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index 8f806531c0..82c5cb8cec 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -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", diff --git a/server/datastore/mysql/migrations/tables/20221130114928_AddPolicyCritical.go b/server/datastore/mysql/migrations/tables/20221130114928_AddPolicyCritical.go new file mode 100644 index 0000000000..13fa25a5e6 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20221130114928_AddPolicyCritical.go @@ -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 +} diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index 1cee43c126..4a7f26d7d7 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -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, '') 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, '') 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, '') 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") diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go index 462e4bed72..5ee6930a08 100644 --- a/server/datastore/mysql/policies_test.go +++ b/server/datastore/mysql/policies_test.go @@ -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) { diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 814d761c6a..a8aecc55a2 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -465,9 +465,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=159 DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB AUTO_INCREMENT=160 DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -775,6 +775,7 @@ CREATE TABLE `policies` ( `description` mediumtext NOT NULL, `author_id` int(10) unsigned DEFAULT NULL, `platforms` varchar(255) NOT NULL DEFAULT '', + `critical` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `idx_policies_unique_name` (`name`), KEY `idx_policies_author_id` (`author_id`), diff --git a/server/fleet/policies.go b/server/fleet/policies.go index ce40d9bde8..f4f55ad81d 100644 --- a/server/fleet/policies.go +++ b/server/fleet/policies.go @@ -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. diff --git a/server/service/team_policies.go b/server/service/team_policies.go index a039691a4c..4352b0873f 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -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) diff --git a/server/webhooks/failing_policies_test.go b/server/webhooks/failing_policies_test.go index 10b87a3b79..b8afff8b43 100644 --- a/server/webhooks/failing_policies_test.go +++ b/server/webhooks/failing_policies_test.go @@ -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": [ { diff --git a/server/worker/jira.go b/server/worker/jira.go index 343c7e731a..4c6a9cdc95 100644 --- a/server/worker/jira.go +++ b/server/worker/jira.go @@ -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 { diff --git a/server/worker/jira_test.go b/server/worker/jira_test.go index 4c5a73e7ab..1d9c79b6ef 100644 --- a/server/worker/jira_test.go +++ b/server/worker/jira_test.go @@ -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") } diff --git a/server/worker/worker.go b/server/worker/worker.go index d64e526ec5..52c7a1cbe0 100644 --- a/server/worker/worker.go +++ b/server/worker/worker.go @@ -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 diff --git a/server/worker/zendesk.go b/server/worker/zendesk.go index 3a542c2a10..a704bf0421 100644 --- a/server/worker/zendesk.go +++ b/server/worker/zendesk.go @@ -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) diff --git a/server/worker/zendesk_test.go b/server/worker/zendesk_test.go index 4bd848cccc..e6bed0d6f7 100644 --- a/server/worker/zendesk_test.go +++ b/server/worker/zendesk_test.go @@ -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") }