2022-05-02 20:58:34 +00:00
package worker
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sort"
2022-06-06 14:41:51 +00:00
"sync"
"text/template"
2022-05-02 20:58:34 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2022-11-15 14:08:05 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/license"
2022-05-02 20:58:34 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2022-06-06 14:41:51 +00:00
"github.com/fleetdm/fleet/v4/server/service/externalsvc"
2022-05-02 20:58:34 +00:00
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
zendesk "github.com/nukosuke/go-zendesk/zendesk"
)
// zendeskName is the name of the job as registered in the worker.
const zendeskName = "zendesk"
2022-06-06 14:41:51 +00:00
var zendeskTemplates = struct {
VulnSummary * template . Template
VulnDescription * template . Template
FailingPolicySummary * template . Template
FailingPolicyDescription * template . Template
} {
VulnSummary : template . Must ( template . New ( "" ) . Parse (
` Vulnerability {{ .CVE }} detected on {{ len .Hosts }} host(s) ` ,
) ) ,
2022-05-02 20:58:34 +00:00
2022-10-26 14:42:09 +00:00
// 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 (
2022-06-06 14:41:51 +00:00
` See vulnerability ( CVE ) details in National Vulnerability Database ( NVD ) here : [ { { . CVE } } ] ( { { . NVDURL } } { { . CVE } } ) .
2022-05-02 20:58:34 +00:00
2022-10-26 14:42:09 +00:00
{ { if . IsPremium } } { { if . EPSSProbability } }
& nbsp ;
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 }}
& nbsp ;
{ { end } } { { end } }
2022-05-02 20:58:34 +00:00
Affected hosts :
{ { $ end := len . Hosts } } { { if gt $ end 50 } } { { $ end = 50 } } { { end } }
{ { range slice . Hosts 0 $ end } }
2022-10-08 12:57:46 +00:00
* [ { { . DisplayName } } ] ( { { $ . FleetURL } } / hosts / { { . ID } } )
2022-05-02 20:58:34 +00:00
{ { end } }
View the affected software and more affected hosts :
1. Go to the [ Software ] ( { { . FleetURL } } / software / manage ) page in Fleet .
2022-10-26 14:42:09 +00:00
2. Above the list of software , in the * * Search software * * box , enter "{{ .CVE }}" .
3. Hover over the affected software and select * * View all hosts * * .
2022-05-02 20:58:34 +00:00
-- --
This ticket was created automatically by your Fleet Zendesk integration .
2022-06-06 14:41:51 +00:00
` ) ) ,
2022-05-02 20:58:34 +00:00
2022-06-06 14:41:51 +00:00
FailingPolicySummary : template . Must ( template . New ( "" ) . Parse (
` {{ .PolicyName }} policy failed on {{ len .Hosts }} host(s) ` ,
) ) ,
FailingPolicyDescription : template . Must ( template . New ( "" ) . Parse (
` Hosts :
{ { $ end := len . Hosts } } { { if gt $ end 50 } } { { $ end = 50 } } { { end } }
{ { range slice . Hosts 0 $ end } }
2022-10-08 12:57:46 +00:00
* [ { { . DisplayName } } ] ( { { $ . FleetURL } } / hosts / { { . ID } } )
2022-06-06 14:41:51 +00:00
{ { end } }
View hosts that failed { { . PolicyName } } on the [ * * Hosts * * ] ( { { . FleetURL } } / hosts / manage / ? order_key = hostname & order_direction = asc & { { if . TeamID } } team_id = { { . TeamID } } & { { end } } policy_id = { { . PolicyID } } & policy_response = failing ) page in Fleet .
-- --
This issue was created automatically by your Fleet Zendesk integration .
` ) ) ,
}
type zendeskVulnTplArgs struct {
2022-05-02 20:58:34 +00:00
NVDURL string
FleetURL string
CVE string
Hosts [ ] * fleet . HostShort
2022-10-26 14:42:09 +00:00
IsPremium bool
// the following fields are only included in the ticket for premium licenses.
EPSSProbability * float64
CVSSScore * float64
CISAKnownExploit * bool
2022-05-02 20:58:34 +00:00
}
2022-06-06 14:41:51 +00:00
type zendeskFailingPoliciesTplArgs struct {
FleetURL string
PolicyID uint
PolicyName string
TeamID * uint
Hosts [ ] fleet . PolicySetHost
}
2022-05-02 20:58:34 +00:00
// ZendeskClient defines the method required for the client that makes API calls
// to Zendesk.
type ZendeskClient interface {
2022-06-06 14:41:51 +00:00
CreateZendeskTicket ( ctx context . Context , ticket * zendesk . Ticket ) ( * zendesk . Ticket , error )
ZendeskConfigMatches ( opts * externalsvc . ZendeskOptions ) bool
2022-05-02 20:58:34 +00:00
}
// Zendesk is the job processor for zendesk integrations.
type Zendesk struct {
FleetURL string
Datastore fleet . Datastore
Log kitlog . Logger
2022-06-06 14:41:51 +00:00
NewClientFunc func ( * externalsvc . ZendeskOptions ) ( ZendeskClient , error )
// mu protects concurrent access to clientsCache, so that the job processor
// can potentially be run concurrently.
mu sync . Mutex
// map of integration type + team ID to Zendesk client (empty team ID for
// global), e.g. "vuln:123", "failingPolicy:", etc.
clientsCache map [ string ] ZendeskClient
}
// returns nil, nil if there is no integration enabled for that message.
func ( z * Zendesk ) getClient ( ctx context . Context , args zendeskArgs ) ( ZendeskClient , error ) {
var teamID uint
var useTeamCfg bool
intgType := args . integrationType ( )
key := intgType + ":"
if intgType == intgTypeFailingPolicy && args . FailingPolicy . TeamID != nil {
teamID = * args . FailingPolicy . TeamID
useTeamCfg = true
key += fmt . Sprint ( teamID )
}
2022-06-13 14:04:47 +00:00
ac , err := z . Datastore . AppConfig ( ctx )
if err != nil {
return nil , err
}
2022-06-06 14:41:51 +00:00
// load the config that would be used to create the client first - it is
// needed to check if an existing client is configured the same or if its
// configuration has changed since it was created.
var opts * externalsvc . ZendeskOptions
if useTeamCfg {
tm , err := z . Datastore . Team ( ctx , teamID )
if err != nil {
return nil , err
}
2022-06-13 14:04:47 +00:00
intgs , err := tm . Config . Integrations . MatchWithIntegrations ( ac . Integrations )
if err != nil {
return nil , err
}
for _ , intg := range intgs . Zendesk {
2022-06-06 14:41:51 +00:00
if intgType == intgTypeFailingPolicy && intg . EnableFailingPolicies {
opts = & externalsvc . ZendeskOptions {
URL : intg . URL ,
Email : intg . Email ,
APIToken : intg . APIToken ,
GroupID : intg . GroupID ,
}
break
}
}
} else {
for _ , intg := range ac . Integrations . Zendesk {
if ( intgType == intgTypeVuln && intg . EnableSoftwareVulnerabilities ) ||
( intgType == intgTypeFailingPolicy && intg . EnableFailingPolicies ) {
opts = & externalsvc . ZendeskOptions {
URL : intg . URL ,
Email : intg . Email ,
APIToken : intg . APIToken ,
GroupID : intg . GroupID ,
}
break
}
}
}
z . mu . Lock ( )
defer z . mu . Unlock ( )
if z . clientsCache == nil {
z . clientsCache = make ( map [ string ] ZendeskClient )
}
if opts == nil {
// no integration configured, clear any existing one
delete ( z . clientsCache , key )
return nil , nil
}
// check if the existing one can be reused
if cli := z . clientsCache [ key ] ; cli != nil && cli . ZendeskConfigMatches ( opts ) {
return cli , nil
}
// otherwise create a new one
cli , err := z . NewClientFunc ( opts )
if err != nil {
return nil , err
}
z . clientsCache [ key ] = cli
return cli , nil
2022-05-02 20:58:34 +00:00
}
// Name returns the name of the job.
func ( z * Zendesk ) Name ( ) string {
return zendeskName
}
2022-06-06 14:41:51 +00:00
// zendeskArgs are the arguments for the Zendesk integration job.
type zendeskArgs struct {
2022-10-26 14:42:09 +00:00
// CVE is deprecated but kept for backwards compatibility (there may be jobs
// enqueued in that format to process).
2022-06-06 14:41:51 +00:00
CVE string ` json:"cve,omitempty" `
2022-10-26 14:42:09 +00:00
Vulnerability * vulnArgs ` json:"vulnerability,omitempty" `
2022-06-06 14:41:51 +00:00
FailingPolicy * failingPolicyArgs ` json:"failing_policy,omitempty" `
}
func ( a * zendeskArgs ) integrationType ( ) string {
if a . FailingPolicy == nil {
return intgTypeVuln
}
return intgTypeFailingPolicy
2022-05-02 20:58:34 +00:00
}
// Run executes the zendesk job.
func ( z * Zendesk ) Run ( ctx context . Context , argsJSON json . RawMessage ) error {
2022-06-06 14:41:51 +00:00
var args zendeskArgs
2022-05-02 20:58:34 +00:00
if err := json . Unmarshal ( argsJSON , & args ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "unmarshal args" )
}
2022-06-06 14:41:51 +00:00
cli , err := z . getClient ( ctx , args )
if err != nil {
return ctxerr . Wrap ( ctx , err , "get Zendesk client" )
}
if cli == nil {
// this message was queued when an integration was enabled, but since
// then it has been disabled, so return success to mark the message
// as processed.
return nil
}
switch intgType := args . integrationType ( ) ; intgType {
case intgTypeVuln :
return z . runVuln ( ctx , cli , args )
case intgTypeFailingPolicy :
return z . runFailingPolicy ( ctx , cli , args )
default :
return ctxerr . Errorf ( ctx , "unknown integration type: %v" , intgType )
}
}
func ( z * Zendesk ) runVuln ( ctx context . Context , cli ZendeskClient , args zendeskArgs ) error {
2022-10-26 14:42:09 +00:00
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 )
2022-05-02 20:58:34 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "find hosts by cve" )
}
2022-06-06 14:41:51 +00:00
tplArgs := & zendeskVulnTplArgs {
2022-10-26 14:42:09 +00:00
NVDURL : nvdCVEURL ,
FleetURL : z . FleetURL ,
CVE : vargs . CVE ,
Hosts : hosts ,
2022-11-15 14:08:05 +00:00
IsPremium : license . IsPremium ( ctx ) ,
2022-10-26 14:42:09 +00:00
EPSSProbability : vargs . EPSSProbability ,
CVSSScore : vargs . CVSSScore ,
CISAKnownExploit : vargs . CISAKnownExploit ,
2022-05-02 20:58:34 +00:00
}
2022-06-06 14:41:51 +00:00
createdTicket , err := z . createTemplatedTicket ( ctx , cli , zendeskTemplates . VulnSummary , zendeskTemplates . VulnDescription , tplArgs )
if err != nil {
return err
}
level . Debug ( z . Log ) . Log (
"msg" , "created zendesk ticket for cve" ,
2022-10-26 14:42:09 +00:00
"cve" , vargs . CVE ,
2022-06-06 14:41:51 +00:00
"ticket_id" , createdTicket . ID ,
)
return nil
}
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 ,
}
createdTicket , err := z . createTemplatedTicket ( ctx , cli , zendeskTemplates . FailingPolicySummary , zendeskTemplates . FailingPolicyDescription , tplArgs )
if err != nil {
return err
}
attrs := [ ] interface { } {
"msg" , "created zendesk ticket for failing policy" ,
"policy_id" , args . FailingPolicy . PolicyID ,
"policy_name" , args . FailingPolicy . PolicyName ,
"ticket_id" , createdTicket . ID ,
}
if args . FailingPolicy . TeamID != nil {
attrs = append ( attrs , "team_id" , * args . FailingPolicy . TeamID )
}
level . Debug ( z . Log ) . Log ( attrs ... )
return nil
}
func ( z * Zendesk ) createTemplatedTicket ( ctx context . Context , cli ZendeskClient , summaryTpl , descTpl * template . Template , args interface { } ) ( * zendesk . Ticket , error ) {
2022-05-02 20:58:34 +00:00
var buf bytes . Buffer
2022-06-06 14:41:51 +00:00
if err := summaryTpl . Execute ( & buf , args ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "execute summary template" )
2022-05-02 20:58:34 +00:00
}
summary := buf . String ( )
buf . Reset ( ) // reuse buffer
2022-06-06 14:41:51 +00:00
if err := descTpl . Execute ( & buf , args ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "execute description template" )
2022-05-02 20:58:34 +00:00
}
description := buf . String ( )
ticket := & zendesk . Ticket {
Subject : summary ,
Comment : & zendesk . TicketComment { Body : description } ,
}
2022-06-06 14:41:51 +00:00
createdTicket , err := cli . CreateZendeskTicket ( ctx , ticket )
2022-05-02 20:58:34 +00:00
if err != nil {
2022-06-06 14:41:51 +00:00
return nil , ctxerr . Wrap ( ctx , err , "create ticket" )
2022-05-02 20:58:34 +00:00
}
2022-06-06 14:41:51 +00:00
return createdTicket , nil
2022-05-02 20:58:34 +00:00
}
2022-06-06 14:41:51 +00:00
// QueueZendeskVulnJobs queues the Zendesk vulnerability jobs to process asynchronously
2022-05-02 20:58:34 +00:00
// via the worker.
2022-10-26 14:42:09 +00:00
func QueueZendeskVulnJobs (
ctx context . Context ,
ds fleet . Datastore ,
logger kitlog . Logger ,
recentVulns [ ] fleet . SoftwareVulnerability ,
cveMeta map [ string ] fleet . CVEMeta ,
) error {
2022-05-02 20:58:34 +00:00
level . Info ( logger ) . Log ( "enabled" , "true" , "recentVulns" , len ( recentVulns ) )
// for troubleshooting, log in debug level the CVEs that we will process
// (cannot be done in the loop below as we want to add the debug log
// _before_ we start processing them).
cves := make ( [ ] string , 0 , len ( recentVulns ) )
2022-06-08 01:09:47 +00:00
for _ , vuln := range recentVulns {
2022-10-28 15:12:21 +00:00
cves = append ( cves , vuln . GetCVE ( ) )
2022-05-02 20:58:34 +00:00
}
sort . Strings ( cves )
level . Debug ( logger ) . Log ( "recent_cves" , fmt . Sprintf ( "%v" , cves ) )
2022-09-13 14:41:52 +00:00
uniqCVEs := make ( map [ string ] bool )
for _ , v := range recentVulns {
2022-10-28 15:12:21 +00:00
uniqCVEs [ v . GetCVE ( ) ] = true
2022-09-13 14:41:52 +00:00
}
for cve := range uniqCVEs {
2022-10-26 14:42:09 +00:00
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 } )
2022-05-02 20:58:34 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "queueing job" )
}
level . Debug ( logger ) . Log ( "job_id" , job . ID )
}
return nil
}
2022-06-06 14:41:51 +00:00
// QueueZendeskFailingPolicyJob queues a Zendesk job for a failing policy to
// process asynchronously via the worker.
func QueueZendeskFailingPolicyJob ( ctx context . Context , ds fleet . Datastore , logger kitlog . Logger ,
2022-09-13 14:41:52 +00:00
policy * fleet . Policy , hosts [ ] fleet . PolicySetHost ,
) error {
2022-06-06 14:41:51 +00:00
attrs := [ ] interface { } {
"enabled" , "true" ,
"failing_policy" , policy . ID ,
"hosts_count" , len ( hosts ) ,
}
if policy . TeamID != nil {
attrs = append ( attrs , "team_id" , * policy . TeamID )
}
2022-06-20 15:41:45 +00:00
if len ( hosts ) == 0 {
attrs = append ( attrs , "msg" , "skipping, no host" )
level . Debug ( logger ) . Log ( attrs ... )
return nil
}
2022-06-06 14:41:51 +00:00
level . Info ( logger ) . Log ( attrs ... )
args := & failingPolicyArgs {
PolicyID : policy . ID ,
PolicyName : policy . Name ,
TeamID : policy . TeamID ,
Hosts : hosts ,
}
job , err := QueueJob ( ctx , ds , zendeskName , zendeskArgs { FailingPolicy : args } )
if err != nil {
return ctxerr . Wrap ( ctx , err , "queueing job" )
}
level . Debug ( logger ) . Log ( "job_id" , job . ID )
return nil
}