mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
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.
295 lines
9.6 KiB
Go
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)
|
|
}
|