fleet/server/worker/jira_test.go
Juan Fernandez ef73039559
Improve vulnerability detection for Ubuntu (#6102)
Feature: Improve our capability to detect vulnerable software on Ubuntu hosts

To improve the capability of detecting vulnerable software on Ubuntu, we are now using OVAL definitions to detect vulnerable software on Ubuntu hosts. If data sync is enabled (disable_data_sync=false) OVAL definitions are automatically kept up to date (they are 'refreshed' once per day) - there's also the option to manually download the OVAL definitions using the 'fleetctl vulnerability-data-stream' command. Downloaded definitions are then parsed into an intermediary format and then used to identify vulnerable software on Ubuntu hosts. Finally, any 'recent' detected vulnerabilities are sent to any third-party integrations.
2022-06-07 21:09:47 -04:00

295 lines
9.6 KiB
Go

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, TeamJiraIntegration: fleet.TeamJiraIntegration{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{
{TeamJiraIntegration: fleet.TeamJiraIntegration{ProjectKey: "0", EnableFailingPolicies: true}},
},
}}, 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, 2, globalCount)
require.Equal(t, 3, teamCount)
}