fleet/server/worker/jira_test.go

299 lines
9.8 KiB
Go
Raw Normal View History

package worker
import (
"context"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
jira "github.com/andygrunwald/go-jira"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
kitlog "github.com/go-kit/kit/log"
"github.com/stretchr/testify/require"
)
func TestJiraRun(t *testing.T) {
ds := new(mock.Store)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
return []*fleet.HostShort{
{
ID: 1,
Hostname: "test",
},
}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{Integrations: fleet.Integrations{
Jira: []*fleet.JiraIntegration{
{EnableSoftwareVulnerabilities: true, EnableFailingPolicies: true},
},
}}, nil
}
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
if tid != 123 {
return nil, errors.New("unexpected team id")
}
return &fleet.Team{
ID: 123,
Config: fleet.TeamConfig{
Integrations: fleet.TeamIntegrations{
Jira: []*fleet.TeamJiraIntegration{
{EnableFailingPolicies: true},
},
},
},
}, nil
}
var expectedSummary, expectedDescription, expectedNotInDescription string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(501)
return
}
if r.URL.Path != "/rest/api/2/issue" {
w.WriteHeader(502)
return
}
// the request body is the JSON payload sent to Jira, i.e. the rendered templates
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
if expectedSummary != "" {
require.Contains(t, string(body), expectedSummary)
}
if expectedDescription != "" {
require.Contains(t, string(body), expectedDescription)
}
if expectedNotInDescription != "" {
require.NotContains(t, string(body), expectedNotInDescription)
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`
{
"id": "10000",
"key": "ED-24",
"self": "https://your-domain.atlassian.net/rest/api/2/issue/10000",
"transition": {
"status": 200,
"errorCollection": {
"errorMessages": [],
"errors": {}
}
}
}`))
}))
defer srv.Close()
client, err := externalsvc.NewJiraClient(&externalsvc.JiraOptions{BaseURL: srv.URL})
require.NoError(t, err)
jira := &Jira{
FleetURL: "http://example.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
NewClientFunc: func(opts *externalsvc.JiraOptions) (JiraClient, error) {
return client, nil
},
}
t.Run("vuln", func(t *testing.T) {
expectedSummary = `"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`
expectedDescription, expectedNotInDescription = "", ""
err = jira.Run(context.Background(), json.RawMessage(`{"cve":"CVE-1234-5678"}`))
require.NoError(t, err)
})
t.Run("failing global policy", func(t *testing.T) {
expectedSummary = `"summary":"test-policy policy failed on 0 host(s)"`
expectedDescription = "\\u0026policy_id=1\\u0026policy_response=failing" // ampersand gets rendered as \u0026 in json string
expectedNotInDescription = "\\u0026team_id="
err = jira.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": []}}`))
require.NoError(t, err)
})
t.Run("failing team policy", func(t *testing.T) {
expectedSummary = `"summary":"test-policy-2 policy failed on 2 host(s)"`
expectedDescription = "\\u0026team_id=123\\u0026policy_id=2\\u0026policy_response=failing" // ampersand gets rendered as \u0026 in json string
expectedNotInDescription = ""
err = jira.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": [{"id": 1, "hostname": "test-1"}, {"id": 2, "hostname": "test-2"}]}}`))
require.NoError(t, err)
})
}
func TestJiraQueueVulnJobs(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
logger := kitlog.NewNopLogger()
t.Run("success", func(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
}
err := QueueJiraVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: "CVE-1234-5678"}})
require.NoError(t, err)
require.True(t, ds.NewJobFuncInvoked)
})
t.Run("failure", func(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return nil, io.EOF
}
err := QueueJiraVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: "CVE-1234-5678"}})
require.Error(t, err)
require.ErrorIs(t, err, io.EOF)
require.True(t, ds.NewJobFuncInvoked)
})
}
func TestJiraQueueFailingPolicyJob(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
logger := kitlog.NewNopLogger()
t.Run("success global", func(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
require.NotContains(t, string(*job.Args), `"team_id"`)
return job, nil
}
err := QueueJiraFailingPolicyJob(ctx, ds, logger,
&fleet.Policy{PolicyData: fleet.PolicyData{ID: 1, Name: "p1"}}, []fleet.PolicySetHost{{ID: 1, Hostname: "h1"}})
require.NoError(t, err)
require.True(t, ds.NewJobFuncInvoked)
})
t.Run("success team", func(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
require.Contains(t, string(*job.Args), `"team_id"`)
return job, nil
}
err := QueueJiraFailingPolicyJob(ctx, ds, logger,
&fleet.Policy{PolicyData: fleet.PolicyData{ID: 1, Name: "p1", TeamID: ptr.Uint(2)}}, []fleet.PolicySetHost{{ID: 1, Hostname: "h1"}})
require.NoError(t, err)
require.True(t, ds.NewJobFuncInvoked)
})
t.Run("failure", func(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return nil, io.EOF
}
err := QueueJiraFailingPolicyJob(ctx, ds, logger,
&fleet.Policy{PolicyData: fleet.PolicyData{ID: 1, Name: "p1"}}, []fleet.PolicySetHost{{ID: 1, Hostname: "h1"}})
require.Error(t, err)
require.ErrorIs(t, err, io.EOF)
require.True(t, ds.NewJobFuncInvoked)
})
}
type mockJiraClient struct {
opts externalsvc.JiraOptions
}
func (c *mockJiraClient) CreateJiraIssue(ctx context.Context, issue *jira.Issue) (*jira.Issue, error) {
return &jira.Issue{}, nil
}
func (c *mockJiraClient) JiraConfigMatches(opts *externalsvc.JiraOptions) bool {
return c.opts == *opts
}
func TestJiraRunClientUpdate(t *testing.T) {
// test creation of client when config changes between 2 uses, and when integration is disabled.
ds := new(mock.Store)
var globalCount int
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
// failing policies is globally enabled
globalCount++
return &fleet.AppConfig{Integrations: fleet.Integrations{
Jira: []*fleet.JiraIntegration{
{ProjectKey: "0", EnableFailingPolicies: true},
{ProjectKey: "1", EnableFailingPolicies: false}, // the team integration will use the project keys 1-3
{ProjectKey: "2", EnableFailingPolicies: false},
{ProjectKey: "3", EnableFailingPolicies: false},
},
}}, nil
}
teamCfg := &fleet.Team{
ID: 123,
Config: fleet.TeamConfig{
Integrations: fleet.TeamIntegrations{
Jira: []*fleet.TeamJiraIntegration{
{ProjectKey: "1", EnableFailingPolicies: true},
},
},
},
}
var teamCount int
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
teamCount++
if tid != 123 {
return nil, errors.New("unexpected team id")
}
curCfg := *teamCfg
jira0 := *teamCfg.Config.Integrations.Jira[0]
// failing policies is enabled for team 123 the first time
if jira0.ProjectKey == "1" {
// the second time we change the project key
jira0.ProjectKey = "2"
teamCfg.Config.Integrations.Jira = []*fleet.TeamJiraIntegration{&jira0}
} else if jira0.ProjectKey == "2" {
// the third time we disable it altogether
jira0.ProjectKey = "3"
jira0.EnableFailingPolicies = false
teamCfg.Config.Integrations.Jira = []*fleet.TeamJiraIntegration{&jira0}
}
return &curCfg, nil
}
var projectKeys []string
jiraJob := &Jira{
FleetURL: "http://example.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
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
},
}
// run it globally - it is enabled and will not change
err := jiraJob.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": []}}`))
require.NoError(t, err)
// run it for team 123 a first time
err = jiraJob.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": []}}`))
require.NoError(t, err)
// run it globally again - it will reuse the cached client
err = jiraJob.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": []}}`))
require.NoError(t, err)
// run it for team 123 a second time
err = jiraJob.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": []}}`))
require.NoError(t, err)
// run it for team 123 a third time, this time integration is disabled
err = jiraJob.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": []}}`))
require.NoError(t, err)
// it should've created 3 clients - the global one, and the first 2 calls with team 123
require.Equal(t, []string{"0", "1", "2"}, projectKeys)
require.Equal(t, 5, globalCount) // app config is requested every time
require.Equal(t, 3, teamCount)
}