2022-04-05 16:56:15 +00:00
package worker
import (
2022-04-11 20:42:16 +00:00
"bytes"
2022-04-05 16:56:15 +00:00
"context"
"encoding/json"
2023-05-17 20:53:15 +00:00
"errors"
2022-04-11 20:42:16 +00:00
"fmt"
"sort"
2022-06-06 14:41:51 +00:00
"sync"
"text/template"
2023-03-28 20:11:31 +00:00
"time"
2022-04-05 16:56:15 +00:00
2022-04-11 20:42:16 +00:00
jira "github.com/andygrunwald/go-jira"
"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-04-05 16:56:15 +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"
2024-06-17 13:27:31 +00:00
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
2022-04-05 16:56:15 +00:00
)
2022-05-02 20:58:34 +00:00
// jiraName is the name of the job as registered in the worker.
const jiraName = "jira"
2022-04-11 20:42:16 +00:00
2022-06-06 14:41:51 +00:00
var jiraTemplates = 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-04-11 20:42:16 +00:00
2022-10-26 14:42:09 +00:00
// 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 (
2022-06-06 14:41:51 +00:00
` See vulnerability ( CVE ) details in National Vulnerability Database ( NVD ) here : [ { { . CVE } } | { { . NVDURL } } { { . CVE } } ] .
2022-04-11 20:42:16 +00:00
2022-10-26 14:42:09 +00:00
{ { 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 } }
2023-03-28 20:11:31 +00:00
{ { if . CVEPublished } } Published ( reported by [ NVD | https : //nvd.nist.gov/]): {{ .CVEPublished }}
{ { end } }
2022-10-26 14:42:09 +00:00
{ { if . CISAKnownExploit } } Known exploits ( reported by [ CISA | https : //www.cisa.gov/known-exploited-vulnerabilities-catalog]): {{ if deref .CISAKnownExploit }}Yes{{ else }}No{{ end }}
\ \
{ { end } } { { end } }
2022-04-11 20:42:16 +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 } } ]
2023-05-17 20:53:15 +00:00
{ { range $ path := . SoftwareInstalledPaths } }
* * { { $ path } }
{ { end } }
2022-04-11 20:42:16 +00:00
{ { end } }
View the affected software and more affected hosts :
# Go to the [ Software | { { . FleetURL } } / software / manage ] page in Fleet .
# Above the list of software , in the * Search software * box , enter "{{ .CVE }}" .
# Hover over the affected software and select * View all hosts * .
-- --
This issue was created automatically by your Fleet Jira integration .
2022-06-06 14:41:51 +00:00
` ) ) ,
2022-04-11 20:42:16 +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 (
2022-12-06 14:59:20 +00:00
` { { if . PolicyCritical } } This policy is marked as * Critical * in Fleet .
{ { end } } Hosts :
2022-06-06 14:41:51 +00:00
{ { $ 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 Jira integration .
` ) ) ,
}
type jiraVulnTplArgs struct {
2022-04-11 20:42:16 +00:00
NVDURL string
FleetURL string
CVE string
2023-05-17 20:53:15 +00:00
Hosts [ ] fleet . HostVulnerabilitySummary
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
2023-03-28 20:11:31 +00:00
CVEPublished * time . Time
2022-04-05 16:56:15 +00:00
}
2022-04-11 20:42:16 +00:00
// JiraClient defines the method required for the client that makes API calls
// to Jira.
type JiraClient interface {
2022-06-06 14:41:51 +00:00
CreateJiraIssue ( ctx context . Context , issue * jira . Issue ) ( * jira . Issue , error )
JiraConfigMatches ( opts * externalsvc . JiraOptions ) bool
2022-04-11 20:42:16 +00:00
}
// Jira is the job processor for jira integrations.
type Jira struct {
2022-06-06 14:41:51 +00:00
FleetURL string
Datastore fleet . Datastore
Log kitlog . Logger
NewClientFunc func ( * externalsvc . JiraOptions ) ( JiraClient , 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 Jira client (empty team ID for
// global), e.g. "vuln:123", "failingPolicy:", etc.
clientsCache map [ string ] JiraClient
2022-04-05 16:56:15 +00:00
}
2022-04-11 20:42:16 +00:00
// Name returns the name of the job.
2022-04-05 16:56:15 +00:00
func ( j * Jira ) Name ( ) string {
2022-04-11 20:42:16 +00:00
return jiraName
2022-04-05 16:56:15 +00:00
}
2022-06-06 14:41:51 +00:00
// returns nil, nil if there is no integration enabled for that message.
func ( j * Jira ) getClient ( ctx context . Context , args jiraArgs ) ( JiraClient , 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 := j . 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 . JiraOptions
if useTeamCfg {
2025-09-02 23:02:34 +00:00
tm , err := j . Datastore . TeamWithoutExtras ( ctx , teamID )
2022-06-06 14:41:51 +00:00
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 . Jira {
2022-06-06 14:41:51 +00:00
if intgType == intgTypeFailingPolicy && intg . EnableFailingPolicies {
opts = & externalsvc . JiraOptions {
BaseURL : intg . URL ,
BasicAuthUsername : intg . Username ,
BasicAuthPassword : intg . APIToken ,
ProjectKey : intg . ProjectKey ,
}
break
}
}
} else {
for _ , intg := range ac . Integrations . Jira {
if ( intgType == intgTypeVuln && intg . EnableSoftwareVulnerabilities ) ||
( intgType == intgTypeFailingPolicy && intg . EnableFailingPolicies ) {
opts = & externalsvc . JiraOptions {
BaseURL : intg . URL ,
BasicAuthUsername : intg . Username ,
BasicAuthPassword : intg . APIToken ,
ProjectKey : intg . ProjectKey ,
}
break
}
}
}
j . mu . Lock ( )
defer j . mu . Unlock ( )
if j . clientsCache == nil {
j . clientsCache = make ( map [ string ] JiraClient )
}
if opts == nil {
// no integration configured, clear any existing one
delete ( j . clientsCache , key )
return nil , nil
}
// check if the existing one can be reused
if cli := j . clientsCache [ key ] ; cli != nil && cli . JiraConfigMatches ( opts ) {
return cli , nil
}
// otherwise create a new one
cli , err := j . NewClientFunc ( opts )
if err != nil {
return nil , err
}
j . clientsCache [ key ] = cli
return cli , nil
}
// jiraArgs are the arguments for the Jira integration job.
type jiraArgs struct {
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 * jiraArgs ) integrationType ( ) string {
if a . FailingPolicy == nil {
return intgTypeVuln
}
return intgTypeFailingPolicy
2022-04-11 20:42:16 +00:00
}
// Run executes the jira job.
2022-04-05 16:56:15 +00:00
func ( j * Jira ) Run ( ctx context . Context , argsJSON json . RawMessage ) error {
2022-06-06 14:41:51 +00:00
var args jiraArgs
2022-04-11 20:42:16 +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 := j . getClient ( ctx , args )
if err != nil {
return ctxerr . Wrap ( ctx , err , "get Jira 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 j . runVuln ( ctx , cli , args )
case intgTypeFailingPolicy :
return j . runFailingPolicy ( ctx , cli , args )
default :
return ctxerr . Errorf ( ctx , "unknown integration type: %v" , intgType )
}
}
func ( j * Jira ) runVuln ( ctx context . Context , cli JiraClient , args jiraArgs ) error {
2022-10-26 14:42:09 +00:00
vargs := args . Vulnerability
if vargs == nil {
2023-05-17 20:53:15 +00:00
return errors . New ( "invalid job args" )
}
var hosts [ ] fleet . HostVulnerabilitySummary
var err error
// Default to deprecated method in case we are processing an 'old' job payload
// we are deprecating this because of performance reasons - querying by software_id should be
// way more efficient than by CVE.
if len ( vargs . AffectedSoftwareIDs ) == 0 {
hosts , err = j . Datastore . HostsByCVE ( ctx , vargs . CVE )
} else {
hosts , err = j . Datastore . HostVulnSummariesBySoftwareIDs ( ctx , vargs . AffectedSoftwareIDs )
2022-10-26 14:42:09 +00:00
}
2022-04-11 20:42:16 +00:00
if err != nil {
2023-05-17 20:53:15 +00:00
return ctxerr . Wrap ( ctx , err , "fetching hosts" )
2022-04-11 20:42:16 +00:00
}
2022-06-06 14:41:51 +00:00
tplArgs := & jiraVulnTplArgs {
2022-10-26 14:42:09 +00:00
NVDURL : nvdCVEURL ,
FleetURL : j . 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 ,
2023-03-28 20:11:31 +00:00
CVEPublished : vargs . CVEPublished ,
2022-04-11 20:42:16 +00:00
}
2022-06-06 14:41:51 +00:00
createdIssue , err := j . createTemplatedIssue ( ctx , cli , jiraTemplates . VulnSummary , jiraTemplates . VulnDescription , tplArgs )
if err != nil {
return err
}
level . Debug ( j . Log ) . Log (
"msg" , "created jira issue for cve" ,
2022-10-26 14:42:09 +00:00
"cve" , vargs . CVE ,
2022-06-06 14:41:51 +00:00
"issue_id" , createdIssue . ID ,
"issue_key" , createdIssue . Key ,
)
return nil
}
func ( j * Jira ) runFailingPolicy ( ctx context . Context , cli JiraClient , args jiraArgs ) error {
2022-12-09 18:23:08 +00:00
tplArgs := newFailingPoliciesTplArgs ( j . FleetURL , args . FailingPolicy )
2022-06-06 14:41:51 +00:00
createdIssue , err := j . createTemplatedIssue ( ctx , cli , jiraTemplates . FailingPolicySummary , jiraTemplates . FailingPolicyDescription , tplArgs )
if err != nil {
return err
}
attrs := [ ] interface { } {
"msg" , "created jira issue for failing policy" ,
"policy_id" , args . FailingPolicy . PolicyID ,
"policy_name" , args . FailingPolicy . PolicyName ,
"issue_id" , createdIssue . ID ,
"issue_key" , createdIssue . Key ,
}
if args . FailingPolicy . TeamID != nil {
attrs = append ( attrs , "team_id" , * args . FailingPolicy . TeamID )
}
level . Debug ( j . Log ) . Log ( attrs ... )
return nil
}
func ( j * Jira ) createTemplatedIssue ( ctx context . Context , cli JiraClient , summaryTpl , descTpl * template . Template , args interface { } ) ( * jira . Issue , error ) {
2022-04-11 20:42:16 +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-04-11 20:42:16 +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-04-11 20:42:16 +00:00
}
description := buf . String ( )
issue := & jira . Issue {
Fields : & jira . IssueFields {
Type : jira . IssueType {
2022-05-02 12:56:42 +00:00
Name : "Task" ,
2022-04-11 20:42:16 +00:00
} ,
Summary : summary ,
Description : description ,
} ,
}
2022-06-06 14:41:51 +00:00
createdIssue , err := cli . CreateJiraIssue ( ctx , issue )
2022-04-11 20:42:16 +00:00
if err != nil {
2022-06-06 14:41:51 +00:00
return nil , ctxerr . Wrap ( ctx , err , "create issue" )
2022-04-11 20:42:16 +00:00
}
2022-06-06 14:41:51 +00:00
return createdIssue , nil
2022-04-11 20:42:16 +00:00
}
2022-06-06 14:41:51 +00:00
// QueueJiraVulnJobs queues the Jira vulnerability jobs to process asynchronously
2022-04-11 20:42:16 +00:00
// via the worker.
2022-10-26 14:42:09 +00:00
func QueueJiraVulnJobs (
ctx context . Context ,
ds fleet . Datastore ,
logger kitlog . Logger ,
recentVulns [ ] fleet . SoftwareVulnerability ,
cveMeta map [ string ] fleet . CVEMeta ,
) error {
2022-04-11 20:42:16 +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-04-11 20:42:16 +00:00
}
sort . Strings ( cves )
level . Debug ( logger ) . Log ( "recent_cves" , fmt . Sprintf ( "%v" , cves ) )
2023-05-17 20:53:15 +00:00
cveGrouped := make ( map [ string ] [ ] uint )
2022-09-13 14:41:52 +00:00
for _ , v := range recentVulns {
2023-05-17 20:53:15 +00:00
cveGrouped [ v . GetCVE ( ) ] = append ( cveGrouped [ v . GetCVE ( ) ] , v . Affected ( ) )
2022-09-13 14:41:52 +00:00
}
2023-05-17 20:53:15 +00:00
for cve , sIDs := range cveGrouped {
args := vulnArgs { CVE : cve , AffectedSoftwareIDs : sIDs }
2022-10-26 14:42:09 +00:00
if meta , ok := cveMeta [ cve ] ; ok {
args . EPSSProbability = meta . EPSSProbability
args . CVSSScore = meta . CVSSScore
args . CISAKnownExploit = meta . CISAKnownExploit
2023-03-28 20:11:31 +00:00
args . CVEPublished = meta . Published
2022-10-26 14:42:09 +00:00
}
job , err := QueueJob ( ctx , ds , jiraName , jiraArgs { Vulnerability : & args } )
2022-04-11 20:42:16 +00:00
if err != nil {
return ctxerr . Wrap ( ctx , err , "queueing job" )
}
level . Debug ( logger ) . Log ( "job_id" , job . ID )
}
2022-04-05 16:56:15 +00:00
return nil
}
2022-06-06 14:41:51 +00:00
// QueueJiraFailingPolicyJob queues a Jira job for a failing policy to process
// asynchronously via the worker.
func QueueJiraFailingPolicyJob ( 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 {
2022-12-06 14:59:20 +00:00
PolicyID : policy . ID ,
PolicyName : policy . Name ,
PolicyCritical : policy . Critical ,
Hosts : hosts ,
TeamID : policy . TeamID ,
2022-06-06 14:41:51 +00:00
}
job , err := QueueJob ( ctx , ds , jiraName , jiraArgs { FailingPolicy : args } )
if err != nil {
return ctxerr . Wrap ( ctx , err , "queueing job" )
}
level . Debug ( logger ) . Log ( "job_id" , job . ID )
return nil
}