Add vulnerability scores to Jira/Zendesk tickets for premium users (#8346)

This commit is contained in:
Martin Angers 2022-10-26 10:42:09 -04:00 committed by GitHub
parent acfd154150
commit 49e910270b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 435 additions and 86 deletions

View file

@ -0,0 +1 @@
* Added vulnerability scores to Jira and Zendesk integrations for Fleet Premium users.

View file

@ -202,6 +202,7 @@ func scanVulnerabilities(
ds,
kitlog.With(logger, "jira", "vulnerabilities"),
vulns,
meta,
); err != nil {
errHandler(ctx, logger, "queueing vulnerabilities to jira", err)
}
@ -213,6 +214,7 @@ func scanVulnerabilities(
ds,
kitlog.With(logger, "zendesk", "vulnerabilities"),
vulns,
meta,
); err != nil {
errHandler(ctx, logger, "queueing vulnerabilities to Zendesk", err)
}
@ -468,6 +470,7 @@ func startIntegrationsSchedule(
instanceID string,
ds fleet.Datastore,
logger kitlog.Logger,
license *fleet.LicenseInfo,
) (*schedule.Schedule, error) {
const (
name = "integrations"
@ -484,11 +487,13 @@ func startIntegrationsSchedule(
Datastore: ds,
Log: logger,
NewClientFunc: newJiraClient,
License: license,
}
zendesk := &worker.Zendesk{
Datastore: ds,
Log: logger,
NewClientFunc: newZendeskClient,
License: license,
}
// leave the url empty for now, will be filled when the lock is acquired with
// the up-to-date config.

View file

@ -471,7 +471,7 @@ the way that the Fleet server works.
if _, err := startAutomationsSchedule(ctx, instanceID, ds, logger, 5*time.Minute, failingPolicySet); err != nil {
initFatal(err, "failed to register automations schedule")
}
if _, err := startIntegrationsSchedule(ctx, instanceID, ds, logger); err != nil {
if _, err := startIntegrationsSchedule(ctx, instanceID, ds, logger, license); err != nil {
initFatal(err, "failed to register integrations schedule")
}
if config.MDMApple.Enable {

View file

@ -62,6 +62,7 @@ func TestJiraFailer(t *testing.T) {
FleetURL: "http://example.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
License: &fleet.LicenseInfo{Tier: fleet.TierFree},
NewClientFunc: func(opts *externalsvc.JiraOptions) (JiraClient, error) {
return failer, nil
},
@ -120,6 +121,7 @@ func TestZendeskFailer(t *testing.T) {
FleetURL: "http://example.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
License: &fleet.LicenseInfo{Tier: fleet.TierFree},
NewClientFunc: func(opts *externalsvc.ZendeskOptions) (ZendeskClient, error) {
return failer, nil
},

View file

@ -30,10 +30,25 @@ var jiraTemplates = struct {
`Vulnerability {{ .CVE }} detected on {{ len .Hosts }} host(s)`,
)),
// Jira uses wiki markup in the v2 api.
VulnDescription: template.Must(template.New("").Parse(
// Jira uses wiki markup in the v2 api. See
// https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all
// for some reference. The `\\` marks force a newline to have the desired spacing
// around the scores, when present.
VulnDescription: template.Must(template.New("").Funcs(template.FuncMap{
// CISAKnownExploit is *bool, so any condition check on it in the template
// will test if nil or not, and not its actual boolean value. Hence, "deref".
"deref": func(b *bool) bool { return *b },
}).Parse(
`See vulnerability (CVE) details in National Vulnerability Database (NVD) here: [{{ .CVE }}|{{ .NVDURL }}{{ .CVE }}].
{{ if .IsPremium }}{{ if .EPSSProbability }}\\Probability of exploit (reported by [FIRST.org/epss|https://www.first.org/epss/]): {{ .EPSSProbability }}
{{ end }}
{{ if .CVSSScore }}CVSS score (reported by [NVD|https://nvd.nist.gov/]): {{ .CVSSScore }}
{{ end }}
{{ if .CISAKnownExploit }}Known exploits (reported by [CISA|https://www.cisa.gov/known-exploited-vulnerabilities-catalog]): {{ if deref .CISAKnownExploit }}Yes{{ else }}No{{ end }}
\\
{{ end }}{{ end }}
Affected hosts:
{{ $end := len .Hosts }}{{ if gt $end 50 }}{{ $end = 50 }}{{ end }}
@ -76,6 +91,13 @@ type jiraVulnTplArgs struct {
FleetURL string
CVE string
Hosts []*fleet.HostShort
IsPremium bool
// the following fields are only included in the ticket for premium licenses.
EPSSProbability *float64
CVSSScore *float64
CISAKnownExploit *bool
}
type jiraFailingPoliciesTplArgs struct {
@ -98,6 +120,7 @@ type Jira struct {
FleetURL string
Datastore fleet.Datastore
Log kitlog.Logger
License *fleet.LicenseInfo
NewClientFunc func(*externalsvc.JiraOptions) (JiraClient, error)
// mu protects concurrent access to clientsCache, so that the job processor
@ -199,7 +222,10 @@ func (j *Jira) getClient(ctx context.Context, args jiraArgs) (JiraClient, error)
// jiraArgs are the arguments for the Jira integration job.
type jiraArgs struct {
// CVE is deprecated but kept for backwards compatibility (there may be jobs
// enqueued in that format to process).
CVE string `json:"cve,omitempty"`
Vulnerability *vulnArgs `json:"vulnerability,omitempty"`
FailingPolicy *failingPolicyArgs `json:"failing_policy,omitempty"`
}
@ -239,16 +265,28 @@ func (j *Jira) Run(ctx context.Context, argsJSON json.RawMessage) error {
}
func (j *Jira) runVuln(ctx context.Context, cli JiraClient, args jiraArgs) error {
hosts, err := j.Datastore.HostsByCVE(ctx, args.CVE)
vargs := args.Vulnerability
if vargs == nil {
// support the old format of vulnerability args, where only the CVE
// is provided.
vargs = &vulnArgs{
CVE: args.CVE,
}
}
hosts, err := j.Datastore.HostsByCVE(ctx, vargs.CVE)
if err != nil {
return ctxerr.Wrap(ctx, err, "find hosts by cve")
}
tplArgs := &jiraVulnTplArgs{
NVDURL: nvdCVEURL,
FleetURL: j.FleetURL,
CVE: args.CVE,
Hosts: hosts,
NVDURL: nvdCVEURL,
FleetURL: j.FleetURL,
CVE: vargs.CVE,
Hosts: hosts,
IsPremium: j.License.IsPremium(),
EPSSProbability: vargs.EPSSProbability,
CVSSScore: vargs.CVSSScore,
CISAKnownExploit: vargs.CISAKnownExploit,
}
createdIssue, err := j.createTemplatedIssue(ctx, cli, jiraTemplates.VulnSummary, jiraTemplates.VulnDescription, tplArgs)
@ -257,7 +295,7 @@ func (j *Jira) runVuln(ctx context.Context, cli JiraClient, args jiraArgs) error
}
level.Debug(j.Log).Log(
"msg", "created jira issue for cve",
"cve", args.CVE,
"cve", vargs.CVE,
"issue_id", createdIssue.ID,
"issue_key", createdIssue.Key,
)
@ -324,7 +362,13 @@ func (j *Jira) createTemplatedIssue(ctx context.Context, cli JiraClient, summary
// QueueJiraVulnJobs queues the Jira vulnerability jobs to process asynchronously
// via the worker.
func QueueJiraVulnJobs(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, recentVulns []fleet.SoftwareVulnerability) error {
func QueueJiraVulnJobs(
ctx context.Context,
ds fleet.Datastore,
logger kitlog.Logger,
recentVulns []fleet.SoftwareVulnerability,
cveMeta map[string]fleet.CVEMeta,
) error {
level.Info(logger).Log("enabled", "true", "recentVulns", len(recentVulns))
// for troubleshooting, log in debug level the CVEs that we will process
@ -343,7 +387,13 @@ func QueueJiraVulnJobs(ctx context.Context, ds fleet.Datastore, logger kitlog.Lo
}
for cve := range uniqCVEs {
job, err := QueueJob(ctx, ds, jiraName, jiraArgs{CVE: cve})
args := vulnArgs{CVE: cve}
if meta, ok := cveMeta[cve]; ok {
args.EPSSProbability = meta.EPSSProbability
args.CVSSScore = meta.CVSSScore
args.CISAKnownExploit = meta.CISAKnownExploit
}
job, err := QueueJob(ctx, ds, jiraName, jiraArgs{Vulnerability: &args})
if err != nil {
return ctxerr.Wrap(ctx, err, "queueing job")
}

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
@ -73,6 +74,7 @@ func TestJiraRun(t *testing.T) {
require.Contains(t, string(body), expectedDescription)
}
if expectedNotInDescription != "" {
fmt.Println(string(body))
require.NotContains(t, string(body), expectedNotInDescription)
}
@ -96,37 +98,100 @@ func TestJiraRun(t *testing.T) {
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
cases := []struct {
desc string
licenseTier string
payload string
expectedSummary string
expectedDescription string
expectedNotInDescription string
}{
{
"old vuln format free",
fleet.TierFree,
`{"cve":"CVE-1234-5678"}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
"Probability of exploit",
},
{
"vuln free",
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
"Probability of exploit",
},
{
"vuln with scores free",
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
"Probability of exploit",
},
{
"failing global policy",
fleet.TierFree,
`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": []}}`,
`"summary":"test-policy policy failed on 0 host(s)"`,
"\\u0026policy_id=1\\u0026policy_response=failing",
"\\u0026team_id=",
},
{
"failing team policy",
fleet.TierPremium,
`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": [{"id": 1, "hostname": "test-1"}, {"id": 2, "hostname": "test-2"}]}}`,
`"summary":"test-policy-2 policy failed on 2 host(s)"`,
"\\u0026team_id=123\\u0026policy_id=2\\u0026policy_response=failing",
"",
},
{
"old vuln format premium",
fleet.TierPremium,
`{"cve":"CVE-1234-5678"}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
"Probability of exploit",
},
{
"vuln premium",
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
"Probability of exploit",
},
{
"vuln with scores premium",
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Probability of exploit",
"",
},
}
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)
})
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
jira := &Jira{
FleetURL: "https://fleetdm.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
License: &fleet.LicenseInfo{Tier: c.licenseTier},
NewClientFunc: func(opts *externalsvc.JiraOptions) (JiraClient, error) {
return client, nil
},
}
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)
})
expectedSummary = c.expectedSummary
expectedDescription = c.expectedDescription
expectedNotInDescription = c.expectedNotInDescription
err = jira.Run(context.Background(), json.RawMessage(c.payload))
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) {
@ -153,8 +218,12 @@ func TestJiraQueueVulnJobs(t *testing.T) {
CVE: "CVE-1234-5678",
SoftwareID: 3,
}}
meta := make(map[string]fleet.CVEMeta, len(vulns))
for _, v := range vulns {
meta[v.CVE] = fleet.CVEMeta{CVE: v.CVE}
}
err := QueueJiraVulnJobs(ctx, ds, logger, vulns)
err := QueueJiraVulnJobs(ctx, ds, logger, vulns, meta)
require.NoError(t, err)
require.True(t, ds.NewJobFuncInvoked)
require.Equal(t, 1, count)
@ -164,7 +233,11 @@ func TestJiraQueueVulnJobs(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"}})
theCVE := "CVE-1234-5678"
meta := map[string]fleet.CVEMeta{
theCVE: {CVE: theCVE},
}
err := QueueJiraVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: theCVE}}, meta)
require.NoError(t, err)
require.True(t, ds.NewJobFuncInvoked)
})
@ -173,7 +246,11 @@ func TestJiraQueueVulnJobs(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"}})
theCVE := "CVE-1234-5678"
meta := map[string]fleet.CVEMeta{
theCVE: {CVE: theCVE},
}
err := QueueJiraVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: theCVE}}, meta)
require.Error(t, err)
require.ErrorIs(t, err, io.EOF)
require.True(t, ds.NewJobFuncInvoked)
@ -304,6 +381,7 @@ func TestJiraRunClientUpdate(t *testing.T) {
FleetURL: "http://example.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
License: &fleet.LicenseInfo{Tier: fleet.TierFree},
NewClientFunc: func(opts *externalsvc.JiraOptions) (JiraClient, error) {
// keep track of project keys received in calls to NewClientFunc
projectKeys = append(projectKeys, opts.ProjectKey)

View file

@ -43,6 +43,15 @@ type failingPolicyArgs struct {
TeamID *uint `json:"team_id,omitempty"`
}
// vulnArgs are the args common to all integrations that can process
// vulnerabilities.
type vulnArgs struct {
CVE string `json:"cve,omitempty"`
EPSSProbability *float64 `json:"epss_probability,omitempty"` // Premium feature only
CVSSScore *float64 `json:"cvss_score,omitempty"` // Premium feature only
CISAKnownExploit *bool `json:"cisa_known_exploit,omitempty"` // Premium feature only
}
// Worker runs jobs. NOT SAFE FOR CONCURRENT USE.
type Worker struct {
ds fleet.Datastore

View file

@ -30,9 +30,26 @@ var zendeskTemplates = struct {
`Vulnerability {{ .CVE }} detected on {{ len .Hosts }} host(s)`,
)),
VulnDescription: template.Must(template.New("").Parse(
// Zendesk uses markdown for formatting. Some reference documentation about
// it can be found here:
// https://support.zendesk.com/hc/en-us/articles/4408846544922-Formatting-text-with-Markdown
VulnDescription: template.Must(template.New("").Funcs(template.FuncMap{
// CISAKnownExploit is *bool, so any condition check on it in the template
// will test if nil or not, and not its actual boolean value. Hence, "deref".
"deref": func(b *bool) bool { return *b },
}).Parse(
`See vulnerability (CVE) details in National Vulnerability Database (NVD) here: [{{ .CVE }}]({{ .NVDURL }}{{ .CVE }}).
{{ if .IsPremium }}{{ if .EPSSProbability }}
 
Probability of exploit (reported by [FIRST.org/epss](https://www.first.org/epss/)): {{ .EPSSProbability }}
{{ end }}
{{ if .CVSSScore }}CVSS score (reported by [NVD](https://nvd.nist.gov/)): {{ .CVSSScore }}
{{ end }}
{{ if .CISAKnownExploit }}Known exploits (reported by [CISA](https://www.cisa.gov/known-exploited-vulnerabilities-catalog)): {{ if deref .CISAKnownExploit }}Yes{{ else }}No{{ end }}
 
{{ end }}{{ end }}
Affected hosts:
{{ $end := len .Hosts }}{{ if gt $end 50 }}{{ $end = 50 }}{{ end }}
@ -43,8 +60,8 @@ Affected hosts:
View the affected software and more affected hosts:
1. Go to the [Software]({{ .FleetURL }}/software/manage) page in Fleet.
2. Above the list of software, in the *Search software* box, enter "{{ .CVE }}".
3. Hover over the affected software and select *View all hosts*.
2. Above the list of software, in the **Search software** box, enter "{{ .CVE }}".
3. Hover over the affected software and select **View all hosts**.
----
@ -75,6 +92,13 @@ type zendeskVulnTplArgs struct {
FleetURL string
CVE string
Hosts []*fleet.HostShort
IsPremium bool
// the following fields are only included in the ticket for premium licenses.
EPSSProbability *float64
CVSSScore *float64
CISAKnownExploit *bool
}
type zendeskFailingPoliciesTplArgs struct {
@ -97,6 +121,7 @@ type Zendesk struct {
FleetURL string
Datastore fleet.Datastore
Log kitlog.Logger
License *fleet.LicenseInfo
NewClientFunc func(*externalsvc.ZendeskOptions) (ZendeskClient, error)
// mu protects concurrent access to clientsCache, so that the job processor
@ -199,7 +224,10 @@ func (z *Zendesk) Name() string {
// zendeskArgs are the arguments for the Zendesk integration job.
type zendeskArgs struct {
// CVE is deprecated but kept for backwards compatibility (there may be jobs
// enqueued in that format to process).
CVE string `json:"cve,omitempty"`
Vulnerability *vulnArgs `json:"vulnerability,omitempty"`
FailingPolicy *failingPolicyArgs `json:"failing_policy,omitempty"`
}
@ -239,16 +267,28 @@ func (z *Zendesk) Run(ctx context.Context, argsJSON json.RawMessage) error {
}
func (z *Zendesk) runVuln(ctx context.Context, cli ZendeskClient, args zendeskArgs) error {
hosts, err := z.Datastore.HostsByCVE(ctx, args.CVE)
vargs := args.Vulnerability
if vargs == nil {
// support the old format of vulnerability args, where only the CVE
// is provided.
vargs = &vulnArgs{
CVE: args.CVE,
}
}
hosts, err := z.Datastore.HostsByCVE(ctx, vargs.CVE)
if err != nil {
return ctxerr.Wrap(ctx, err, "find hosts by cve")
}
tplArgs := &zendeskVulnTplArgs{
NVDURL: nvdCVEURL,
FleetURL: z.FleetURL,
CVE: args.CVE,
Hosts: hosts,
NVDURL: nvdCVEURL,
FleetURL: z.FleetURL,
CVE: vargs.CVE,
Hosts: hosts,
IsPremium: z.License.IsPremium(),
EPSSProbability: vargs.EPSSProbability,
CVSSScore: vargs.CVSSScore,
CISAKnownExploit: vargs.CISAKnownExploit,
}
createdTicket, err := z.createTemplatedTicket(ctx, cli, zendeskTemplates.VulnSummary, zendeskTemplates.VulnDescription, tplArgs)
@ -257,7 +297,7 @@ func (z *Zendesk) runVuln(ctx context.Context, cli ZendeskClient, args zendeskAr
}
level.Debug(z.Log).Log(
"msg", "created zendesk ticket for cve",
"cve", args.CVE,
"cve", vargs.CVE,
"ticket_id", createdTicket.ID,
)
return nil
@ -317,7 +357,13 @@ func (z *Zendesk) createTemplatedTicket(ctx context.Context, cli ZendeskClient,
// QueueZendeskVulnJobs queues the Zendesk vulnerability jobs to process asynchronously
// via the worker.
func QueueZendeskVulnJobs(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, recentVulns []fleet.SoftwareVulnerability) error {
func QueueZendeskVulnJobs(
ctx context.Context,
ds fleet.Datastore,
logger kitlog.Logger,
recentVulns []fleet.SoftwareVulnerability,
cveMeta map[string]fleet.CVEMeta,
) error {
level.Info(logger).Log("enabled", "true", "recentVulns", len(recentVulns))
// for troubleshooting, log in debug level the CVEs that we will process
@ -336,7 +382,13 @@ func QueueZendeskVulnJobs(ctx context.Context, ds fleet.Datastore, logger kitlog
}
for cve := range uniqCVEs {
job, err := QueueJob(ctx, ds, zendeskName, zendeskArgs{CVE: cve})
args := vulnArgs{CVE: cve}
if meta, ok := cveMeta[cve]; ok {
args.EPSSProbability = meta.EPSSProbability
args.CVSSScore = meta.CVSSScore
args.CISAKnownExploit = meta.CISAKnownExploit
}
job, err := QueueJob(ctx, ds, zendeskName, zendeskArgs{Vulnerability: &args})
if err != nil {
return ctxerr.Wrap(ctx, err, "queueing job")
}

View file

@ -83,38 +83,99 @@ func TestZendeskRun(t *testing.T) {
client, err := externalsvc.NewZendeskTestClient(&externalsvc.ZendeskOptions{URL: srv.URL, GroupID: int64(123)})
require.NoError(t, err)
zendesk := &Zendesk{
FleetURL: "https://fleetdm.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
NewClientFunc: func(opts *externalsvc.ZendeskOptions) (ZendeskClient, error) {
return client, nil
cases := []struct {
desc string
licenseTier string
payload string
expectedSubject string
expectedDescription string
expectedNotInDescription string
}{
{
"old vuln format free",
fleet.TierFree,
`{"cve":"CVE-1234-5678"}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
"Probability of exploit",
},
{
"vuln free",
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
"Probability of exploit",
},
{
"vuln with scores free",
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
"Probability of exploit",
},
{
"failing global policy",
fleet.TierFree,
`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": [{"id": 123, "hostname": "host-123"}]}}`,
`"subject":"test-policy policy failed on 1 host(s)"`,
"\\u0026policy_id=1\\u0026policy_response=failing",
"\\u0026team_id=",
},
{
"failing team policy",
fleet.TierPremium,
`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": [{"id": 1, "hostname": "host-1"}, {"id": 2, "hostname": "host-2"}]}}`,
`"subject":"test-policy-2 policy failed on 2 host(s)"`,
"\\u0026team_id=123\\u0026policy_id=2\\u0026policy_response=failing",
"",
},
{
"old vuln format premium",
fleet.TierPremium,
`{"cve":"CVE-1234-5678"}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
"Probability of exploit",
},
{
"vuln premium",
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
"Probability of exploit",
},
{
"vuln with scores premium",
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Probability of exploit",
"",
},
}
t.Run("vuln", func(t *testing.T) {
expectedSubject = `"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`
expectedDescription = `"group_id":123`
expectedNotInDescription = ""
err = zendesk.Run(context.Background(), json.RawMessage(`{"cve":"CVE-1234-5678"}`))
require.NoError(t, err)
})
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
zendesk := &Zendesk{
FleetURL: "https://fleetdm.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
License: &fleet.LicenseInfo{Tier: c.licenseTier},
NewClientFunc: func(opts *externalsvc.ZendeskOptions) (ZendeskClient, error) {
return client, nil
},
}
t.Run("failing global policy", func(t *testing.T) {
expectedSubject = `"subject":"test-policy policy failed on 1 host(s)"`
expectedDescription = "\\u0026policy_id=1\\u0026policy_response=failing" // ampersand gets rendered as \u0026 in json string
expectedNotInDescription = "\\u0026team_id="
err = zendesk.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": [{"id": 123, "hostname": "host-123"}]}}`))
require.NoError(t, err)
})
t.Run("failing team policy", func(t *testing.T) {
expectedSubject = `"subject":"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 = zendesk.Run(context.Background(), json.RawMessage(`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": [{"id": 1, "hostname": "host-1"}, {"id": 2, "hostname": "host-2"}]}}`))
require.NoError(t, err)
})
expectedSubject = c.expectedSubject
expectedDescription = c.expectedDescription
expectedNotInDescription = c.expectedNotInDescription
err = zendesk.Run(context.Background(), json.RawMessage(c.payload))
require.NoError(t, err)
})
}
}
func TestZendeskQueueVulnJobs(t *testing.T) {
@ -141,8 +202,12 @@ func TestZendeskQueueVulnJobs(t *testing.T) {
CVE: "CVE-1234-5678",
SoftwareID: 3,
}}
meta := make(map[string]fleet.CVEMeta, len(vulns))
for _, v := range vulns {
meta[v.CVE] = fleet.CVEMeta{CVE: v.CVE}
}
err := QueueZendeskVulnJobs(ctx, ds, logger, vulns)
err := QueueZendeskVulnJobs(ctx, ds, logger, vulns, meta)
require.NoError(t, err)
require.True(t, ds.NewJobFuncInvoked)
require.Equal(t, 1, count)
@ -152,7 +217,11 @@ func TestZendeskQueueVulnJobs(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return job, nil
}
err := QueueZendeskVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: "CVE-1234-5678"}})
theCVE := "CVE-1234-5678"
meta := map[string]fleet.CVEMeta{
theCVE: {CVE: theCVE},
}
err := QueueZendeskVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: theCVE}}, meta)
require.NoError(t, err)
require.True(t, ds.NewJobFuncInvoked)
})
@ -161,7 +230,11 @@ func TestZendeskQueueVulnJobs(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return nil, io.EOF
}
err := QueueZendeskVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: "CVE-1234-5678"}})
theCVE := "CVE-1234-5678"
meta := map[string]fleet.CVEMeta{
theCVE: {CVE: theCVE},
}
err := QueueZendeskVulnJobs(ctx, ds, logger, []fleet.SoftwareVulnerability{{CVE: theCVE}}, meta)
require.Error(t, err)
require.ErrorIs(t, err, io.EOF)
require.True(t, ds.NewJobFuncInvoked)
@ -292,6 +365,7 @@ func TestZendeskRunClientUpdate(t *testing.T) {
FleetURL: "http://example.com",
Datastore: ds,
Log: kitlog.NewNopLogger(),
License: &fleet.LicenseInfo{Tier: fleet.TierFree},
NewClientFunc: func(opts *externalsvc.ZendeskOptions) (ZendeskClient, error) {
// keep track of group IDs received in calls to NewClientFunc
groupIDs = append(groupIDs, opts.GroupID)

View file

@ -28,13 +28,23 @@ func main() {
jiraProjectKey = flag.String("jira-project-key", "", "The Jira project key")
fleetURL = flag.String("fleet-url", "https://localhost:8080", "The Fleet server URL")
cve = flag.String("cve", "", "The CVE to create a Jira issue for")
epssProbability = flag.Float64("epss-probability", 0, "The EPSS Probability score of the CVE")
cvssScore = flag.Float64("cvss-score", 0, "The CVSS score of the CVE")
cisaKnownExploit = flag.Bool("cisa-known-exploit", false, "Whether CISA reported it as a known exploit")
hostsCount = flag.Int("hosts-count", 1, "The number of hosts to match the CVE or failing policy")
failingPolicyID = flag.Int("failing-policy-id", 0, "The failing policy ID")
failingPolicyTeamID = flag.Int("failing-policy-team-id", 0, "The Team ID of the failing policy")
premiumLicense = flag.Bool("premium", false, "Whether to simulate a premium user or not")
)
flag.Parse()
// keep set of flags that were provided, to handle those that can be absent
setFlags := make(map[string]bool)
flag.CommandLine.Visit(func(f *flag.Flag) {
setFlags[f.Name] = true
})
if *jiraURL == "" {
fmt.Fprintf(os.Stderr, "-jira-url is required")
os.Exit(1)
@ -72,7 +82,7 @@ func main() {
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
hosts := make([]*fleet.HostShort, *hostsCount)
for i := 0; i < *hostsCount; i++ {
hosts[i] = &fleet.HostShort{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1)}
hosts[i] = &fleet.HostShort{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1), DisplayName: fmt.Sprintf("host-test-%d", i+1)}
}
return hosts, nil
}
@ -110,10 +120,15 @@ func main() {
}, nil
}
license := &fleet.LicenseInfo{Tier: fleet.TierFree}
if *premiumLicense {
license.Tier = fleet.TierPremium
}
jira := &worker.Jira{
FleetURL: *fleetURL,
Datastore: ds,
Log: logger,
License: license,
NewClientFunc: func(opts *externalsvc.JiraOptions) (worker.JiraClient, error) {
return externalsvc.NewJiraClient(opts)
},
@ -121,7 +136,31 @@ func main() {
var argsJSON json.RawMessage
if *cve != "" {
argsJSON = json.RawMessage(fmt.Sprintf(`{"cve":%q}`, *cve))
vulnArgs := struct {
CVE string `json:"cve,omitempty"`
EPSSProbability *float64 `json:"epss_probability,omitempty"`
CVSSScore *float64 `json:"cvss_score,omitempty"`
CISAKnownExploit *bool `json:"cisa_known_exploit,omitempty"`
}{
CVE: *cve,
}
if setFlags["epss-probability"] {
vulnArgs.EPSSProbability = epssProbability
}
if setFlags["cvss-score"] {
vulnArgs.CVSSScore = cvssScore
}
if setFlags["cisa-known-exploit"] {
vulnArgs.CISAKnownExploit = cisaKnownExploit
}
b, err := json.Marshal(vulnArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to marshal vulnerability args: %v", err)
os.Exit(1)
}
argsJSON = json.RawMessage(fmt.Sprintf(`{"vulnerability":%s}`, string(b)))
} else if *failingPolicyID > 0 {
jsonStr := fmt.Sprintf(`{"failing_policy":{"policy_id": %d, "policy_name": "test-policy-%[1]d", `, *failingPolicyID)
if *failingPolicyTeamID > 0 {

View file

@ -28,13 +28,23 @@ func main() {
zendeskGroupID = flag.Int64("zendesk-group-id", 0, "The Zendesk group id")
fleetURL = flag.String("fleet-url", "https://localhost:8080", "The Fleet server URL")
cve = flag.String("cve", "", "The CVE to create a Zendesk issue for")
epssProbability = flag.Float64("epss-probability", 0, "The EPSS Probability score of the CVE")
cvssScore = flag.Float64("cvss-score", 0, "The CVSS score of the CVE")
cisaKnownExploit = flag.Bool("cisa-known-exploit", false, "Whether CISA reported it as a known exploit")
hostsCount = flag.Int("hosts-count", 1, "The number of hosts to match the CVE or failing policy")
failingPolicyID = flag.Int("failing-policy-id", 0, "The failing policy ID")
failingPolicyTeamID = flag.Int("failing-policy-team-id", 0, "The Team ID of the failing policy")
premiumLicense = flag.Bool("premium", false, "Whether to simulate a premium user or not")
)
flag.Parse()
// keep set of flags that were provided, to handle those that can be absent
setFlags := make(map[string]bool)
flag.CommandLine.Visit(func(f *flag.Flag) {
setFlags[f.Name] = true
})
if *zendeskURL == "" {
fmt.Fprintf(os.Stderr, "-zendesk-url is required")
os.Exit(1)
@ -72,7 +82,7 @@ func main() {
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
hosts := make([]*fleet.HostShort, *hostsCount)
for i := 0; i < *hostsCount; i++ {
hosts[i] = &fleet.HostShort{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1)}
hosts[i] = &fleet.HostShort{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1), DisplayName: fmt.Sprintf("host-test-%d", i+1)}
}
return hosts, nil
}
@ -110,10 +120,15 @@ func main() {
}, nil
}
license := &fleet.LicenseInfo{Tier: fleet.TierFree}
if *premiumLicense {
license.Tier = fleet.TierPremium
}
zendesk := &worker.Zendesk{
FleetURL: *fleetURL,
Datastore: ds,
Log: logger,
License: license,
NewClientFunc: func(opts *externalsvc.ZendeskOptions) (worker.ZendeskClient, error) {
return externalsvc.NewZendeskClient(opts)
},
@ -121,7 +136,31 @@ func main() {
var argsJSON json.RawMessage
if *cve != "" {
argsJSON = json.RawMessage(fmt.Sprintf(`{"cve":%q}`, *cve))
vulnArgs := struct {
CVE string `json:"cve,omitempty"`
EPSSProbability *float64 `json:"epss_probability,omitempty"`
CVSSScore *float64 `json:"cvss_score,omitempty"`
CISAKnownExploit *bool `json:"cisa_known_exploit,omitempty"`
}{
CVE: *cve,
}
if setFlags["epss-probability"] {
vulnArgs.EPSSProbability = epssProbability
}
if setFlags["cvss-score"] {
vulnArgs.CVSSScore = cvssScore
}
if setFlags["cisa-known-exploit"] {
vulnArgs.CISAKnownExploit = cisaKnownExploit
}
b, err := json.Marshal(vulnArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to marshal vulnerability args: %v", err)
os.Exit(1)
}
argsJSON = json.RawMessage(fmt.Sprintf(`{"vulnerability":%s}`, string(b)))
} else if *failingPolicyID > 0 {
jsonStr := fmt.Sprintf(`{"failing_policy":{"policy_id": %d, "policy_name": "test-policy-%[1]d", `, *failingPolicyID)
if *failingPolicyTeamID > 0 {