diff --git a/changes/feature-7559-webhook-payload-includes-cve-scores b/changes/feature-7559-webhook-payload-includes-cve-scores new file mode 100644 index 0000000000..4d0d1c64c3 --- /dev/null +++ b/changes/feature-7559-webhook-payload-includes-cve-scores @@ -0,0 +1,2 @@ +- Include the CVSS score, EPSS score, and known exploits properties in the vulnerability Webhook + payload only if the customer is premium. diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index c7de11bdae..7cc21021b9 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -9,6 +9,7 @@ import ( "strings" "time" + eewebhooks "github.com/fleetdm/fleet/v4/ee/server/webhooks" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server/config" @@ -38,6 +39,7 @@ func cronVulnerabilities( logger kitlog.Logger, identifier string, config *config.VulnerabilitiesConfig, + license *fleet.LicenseInfo, ) { logger = kitlog.With(logger, "cron", lockKeyVulnerabilities) @@ -110,7 +112,7 @@ func cronVulnerabilities( } if vulnPath != "" { level.Info(logger).Log("msg", "scanning vulnerabilities") - if err := scanVulnerabilities(ctx, ds, logger, config, appConfig, vulnPath); err != nil { + if err := scanVulnerabilities(ctx, ds, logger, config, appConfig, vulnPath, license); err != nil { errHandler(ctx, logger, "scanning vulnerabilities", err) } } @@ -130,6 +132,7 @@ func scanVulnerabilities( config *config.VulnerabilitiesConfig, appConfig *fleet.AppConfig, vulnPath string, + license *fleet.LicenseInfo, ) error { level.Debug(logger).Log("msg", "creating vulnerabilities databases path", "databases_path", vulnPath) err := os.MkdirAll(vulnPath, 0o755) @@ -171,22 +174,31 @@ func scanVulnerabilities( level.Debug(logger).Log("vulnAutomationEnabled", vulnAutomationEnabled) - collectVulns := vulnAutomationEnabled != "" - nvdVulns := checkNVDVulnerabilities(ctx, ds, logger, vulnPath, config, collectVulns) - ovalVulns := checkOvalVulnerabilities(ctx, ds, logger, vulnPath, config, collectVulns) - recentVulns := filterRecentVulns(ctx, ds, logger, nvdVulns, ovalVulns, config.RecentVulnerabilityMaxAge) + nvdVulns := checkNVDVulnerabilities(ctx, ds, logger, vulnPath, config, vulnAutomationEnabled != "") + ovalVulns := checkOvalVulnerabilities(ctx, ds, logger, vulnPath, config, vulnAutomationEnabled != "") + vulns, meta := recentVulns(ctx, ds, logger, nvdVulns, ovalVulns, config.RecentVulnerabilityMaxAge) - if len(recentVulns) > 0 { + if len(vulns) > 0 { switch vulnAutomationEnabled { case "webhook": + args := webhooks.VulnArgs{ + Vulnerablities: vulns, + Meta: meta, + AppConfig: appConfig, + Time: time.Now(), + } + mapper := webhooks.NewMapper() + if license.IsPremium() { + mapper = eewebhooks.NewMapper() + } // send recent vulnerabilities via webhook if err := webhooks.TriggerVulnerabilitiesWebhook( ctx, ds, kitlog.With(logger, "webhook", "vulnerabilities"), - recentVulns, - appConfig, - time.Now()); err != nil { + args, + mapper, + ); err != nil { errHandler(ctx, logger, "triggering vulnerabilities webhook", err) } @@ -196,7 +208,7 @@ func scanVulnerabilities( ctx, ds, kitlog.With(logger, "jira", "vulnerabilities"), - recentVulns, + vulns, ); err != nil { errHandler(ctx, logger, "queueing vulnerabilities to jira", err) } @@ -207,7 +219,7 @@ func scanVulnerabilities( ctx, ds, kitlog.With(logger, "zendesk", "vulnerabilities"), - recentVulns, + vulns, ); err != nil { errHandler(ctx, logger, "queueing vulnerabilities to Zendesk", err) } @@ -221,47 +233,48 @@ func scanVulnerabilities( return nil } -func filterRecentVulns( +// recentVulns filters both the vulnerabilities comming from NVD and OVAL based on 'maxAge' +// (any vulnerability older than 'maxAge' will be excluded). Returns the filtered vulnerabilities +// and their meta data. +func recentVulns( ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, nvdVulns []fleet.SoftwareVulnerability, ovalVulns []fleet.SoftwareVulnerability, maxAge time.Duration, -) []fleet.SoftwareVulnerability { +) ([]fleet.SoftwareVulnerability, map[string]fleet.CVEMeta) { if len(nvdVulns) == 0 && len(ovalVulns) == 0 { - return nil + return nil, nil } - recent, err := ds.ListCVEs(ctx, maxAge) + meta, err := ds.ListCVEs(ctx, maxAge) if err != nil { - errHandler(ctx, logger, "could not fetch recent CVEs", err) - return nil + errHandler(ctx, logger, "could not fetch CVE meta", err) + return nil, nil } - lookup := make(map[string]bool) - for _, r := range recent { - lookup[r.CVE] = true + recent := make(map[string]fleet.CVEMeta) + for _, r := range meta { + recent[r.CVE] = r } - filtered := make(map[string]fleet.SoftwareVulnerability) + seen := make(map[string]bool) + var vulns []fleet.SoftwareVulnerability for _, v := range nvdVulns { - if _, ok := lookup[v.CVE]; ok { - filtered[v.Key()] = v + if _, ok := recent[v.CVE]; ok && !seen[v.Key()] { + seen[v.Key()] = true + vulns = append(vulns, v) } } for _, v := range ovalVulns { - if _, ok := lookup[v.CVE]; ok { - filtered[v.Key()] = v + if _, ok := recent[v.CVE]; ok && !seen[v.Key()] { + seen[v.Key()] = true + vulns = append(vulns, v) } } - result := make([]fleet.SoftwareVulnerability, 0, len(filtered)) - for _, v := range filtered { - result = append(result, v) - } - - return result + return vulns, recent } func checkOvalVulnerabilities( diff --git a/cmd/fleet/cron_test.go b/cmd/fleet/cron_test.go index b951315c11..6a956246a6 100644 --- a/cmd/fleet/cron_test.go +++ b/cmd/fleet/cron_test.go @@ -17,8 +17,9 @@ func TestFilterRecentVulns(t *testing.T) { ds := new(mock.Store) logger := kitlog.NewNopLogger() - result := filterRecentVulns(ctx, ds, logger, nil, nil, 2*time.Hour) - require.Empty(t, result) + vulns, meta := recentVulns(ctx, ds, logger, nil, nil, 2*time.Hour) + require.Empty(t, vulns) + require.Empty(t, meta) }) t.Run("filters both NVD and OVAL vulns based on max age", func(t *testing.T) { @@ -26,12 +27,14 @@ func TestFilterRecentVulns(t *testing.T) { ds := new(mock.Store) logger := kitlog.NewNopLogger() + dsMeta := []fleet.CVEMeta{ + {CVE: "cve-recent-1"}, + {CVE: "cve-recent-2"}, + {CVE: "cve-recent-3"}, + } + ds.ListCVEsFunc = func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error) { - return []fleet.CVEMeta{ - {CVE: "cve-recent-1"}, - {CVE: "cve-recent-2"}, - {CVE: "cve-recent-3"}, - }, nil + return dsMeta, nil } ovalVulns := []fleet.SoftwareVulnerability{ @@ -56,11 +59,18 @@ func TestFilterRecentVulns(t *testing.T) { } var actual []string - result := filterRecentVulns(ctx, ds, logger, nvdVulns, ovalVulns, maxAge) - for _, r := range result { + vulns, meta := recentVulns(ctx, ds, logger, nvdVulns, ovalVulns, maxAge) + for _, r := range vulns { actual = append(actual, r.CVE) } + expectedMeta := map[string]fleet.CVEMeta{ + "cve-recent-1": {CVE: "cve-recent-1"}, + "cve-recent-2": {CVE: "cve-recent-2"}, + "cve-recent-3": {CVE: "cve-recent-3"}, + } + require.ElementsMatch(t, expected, actual) + require.Equal(t, expectedMeta, meta) }) } diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 8c4c0145cc..eadde0d5d2 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -674,7 +674,8 @@ func runCrons( task.StartCollectors(ctx, kitlog.With(logger, "cron", "async_task")) go cronVulnerabilities( - ctx, ds, kitlog.With(logger, "cron", "vulnerabilities"), ourIdentifier, &config.Vulnerabilities) + ctx, ds, kitlog.With(logger, "cron", "vulnerabilities"), ourIdentifier, &config.Vulnerabilities, license) + go cronWebhooks(ctx, ds, kitlog.With(logger, "cron", "webhooks"), ourIdentifier, failingPoliciesSet, 1*time.Hour) go cronWorker(ctx, ds, kitlog.With(logger, "cron", "worker"), ourIdentifier) } diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 758df117da..91935f2103 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -229,7 +229,7 @@ func TestCronVulnerabilitiesCreatesDatabasesPath(t *testing.T) { CurrentInstanceChecks: "auto", } - go cronVulnerabilities(ctx, ds, kitlog.NewNopLogger(), "AAA", &config) + go cronVulnerabilities(ctx, ds, kitlog.NewNopLogger(), "AAA", &config, &fleet.LicenseInfo{Tier: "premium"}) require.Eventually(t, func() bool { info, err := os.Lstat(vulnPath) @@ -266,7 +266,7 @@ func TestScanVulnerabilitiesMkdirFailsIfVulnPathIsFile(t *testing.T) { CurrentInstanceChecks: "auto", } - err = scanVulnerabilities(ctx, ds, logger, &config, appConfig, fileVulnPath) + err = scanVulnerabilities(ctx, ds, logger, &config, appConfig, fileVulnPath, &fleet.LicenseInfo{Tier: "premium"}) require.ErrorContains(t, err, "create vulnerabilities databases directory: mkdir") } @@ -301,7 +301,7 @@ func TestCronVulnerabilitiesSkipMkdirIfDisabled(t *testing.T) { CurrentInstanceChecks: "1", } - go cronVulnerabilities(ctx, ds, logger, "AAA", &config) + go cronVulnerabilities(ctx, ds, logger, "AAA", &config, &fleet.LicenseInfo{Tier: "premium"}) // Every cron tick is 10 seconds ... here we just wait for a loop interation and assert the vuln // dir. was not created. diff --git a/docs/Using-Fleet/Automations.md b/docs/Using-Fleet/Automations.md index 54346ee938..651feaf8b8 100644 --- a/docs/Using-Fleet/Automations.md +++ b/docs/Using-Fleet/Automations.md @@ -37,6 +37,9 @@ POST https://server.com/example "vulnerability": { "cve": "CVE-2014-9471", "details_link": "https://nvd.nist.gov/vuln/detail/CVE-2014-9471", + "epss_probability": 0.7, // Premium feature only + "cvss_score": 5.7, // Premium feature only + "cisa_known_exploit": true, // Premium feature only "hosts_affected": [ { "id": 1, diff --git a/ee/server/webhooks/mapper.go b/ee/server/webhooks/mapper.go new file mode 100644 index 0000000000..acec402766 --- /dev/null +++ b/ee/server/webhooks/mapper.go @@ -0,0 +1,33 @@ +package webhooks + +import ( + "net/url" + + "github.com/fleetdm/fleet/v4/server/fleet" + fleetwebhooks "github.com/fleetdm/fleet/v4/server/webhooks" +) + +type Mapper struct { + fleetwebhooks.Mapper +} + +func NewMapper() fleetwebhooks.VulnMapper { + return &Mapper{} +} + +func (m *Mapper) GetPayload( + hostBaseURL *url.URL, + hosts []*fleet.HostShort, + vuln fleet.SoftwareVulnerability, + meta fleet.CVEMeta, +) fleetwebhooks.WebhookPayload { + r := m.Mapper.GetPayload(hostBaseURL, + hosts, + vuln, + meta, + ) + r.EPSSProbability = meta.EPSSProbability + r.CVSSScore = meta.CVSSScore + r.CISAKnownExploit = meta.CISAKnownExploit + return r +} diff --git a/ee/server/webhooks/mapper_test.go b/ee/server/webhooks/mapper_test.go new file mode 100644 index 0000000000..3a5eed5d53 --- /dev/null +++ b/ee/server/webhooks/mapper_test.go @@ -0,0 +1,33 @@ +package webhooks + +import ( + "net/url" + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/stretchr/testify/require" +) + +func TestGetPayload(t *testing.T) { + serverURL, err := url.Parse("http://mywebsite.com") + require.NoError(t, err) + + vuln := fleet.SoftwareVulnerability{ + CVE: "cve-1", + SoftwareID: 1, + } + meta := fleet.CVEMeta{ + CVE: "cve-1", + CVSSScore: ptr.Float64(1), + EPSSProbability: ptr.Float64(0.5), + CISAKnownExploit: ptr.Bool(true), + } + + sut := Mapper{} + + result := sut.GetPayload(serverURL, nil, vuln, meta) + require.Equal(t, *meta.CISAKnownExploit, *result.CISAKnownExploit) + require.Equal(t, *meta.EPSSProbability, *result.EPSSProbability) + require.Equal(t, *meta.CVSSScore, *result.CVSSScore) +} diff --git a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx index d7a09960fc..320cb2dd3b 100644 --- a/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx +++ b/frontend/pages/software/ManageSoftwarePage/components/PreviewPayloadModal/PreviewPayloadModal.tsx @@ -19,6 +19,9 @@ const PreviewPayloadModal = ({ vulnerability: { cve: "CVE-2014-9471", details_link: "https://nvd.nist.gov/vuln/detail/CVE-2014-9471", + epss_probability: 0.7, // Premium feature only + cvss_score: 5.7, // Premium feature only + cisa_known_exploit: true, // Premium feature only hosts_affected: [ { id: 1, diff --git a/server/webhooks/mapper.go b/server/webhooks/mapper.go new file mode 100644 index 0000000000..4af3739ed2 --- /dev/null +++ b/server/webhooks/mapper.go @@ -0,0 +1,67 @@ +package webhooks + +import ( + "fmt" + "net/url" + "path" + "strconv" + + "github.com/fleetdm/fleet/v4/server/fleet" +) + +// VulnMapper used for mapping vulnerabilities and their associated data into the payload that +// will be sent via thrid party webhooks. +type VulnMapper interface { + GetPayload(*url.URL, []*fleet.HostShort, fleet.SoftwareVulnerability, fleet.CVEMeta) WebhookPayload +} + +type hostPayloadPart struct { + ID uint `json:"id"` + Hostname string `json:"hostname"` + URL string `json:"url"` +} + +type WebhookPayload struct { + CVE string `json:"cve"` + Link string `json:"details_link"` + 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 + Hosts []*hostPayloadPart `json:"hosts_affected"` +} + +type Mapper struct{} + +func NewMapper() VulnMapper { + return &Mapper{} +} + +func (m *Mapper) getHostPayloadPart( + hostBaseURL *url.URL, + hosts []*fleet.HostShort, +) []*hostPayloadPart { + shortHosts := make([]*hostPayloadPart, len(hosts)) + for i, h := range hosts { + hostURL := *hostBaseURL + hostURL.Path = path.Join(hostURL.Path, "hosts", strconv.Itoa(int(h.ID))) + shortHosts[i] = &hostPayloadPart{ + ID: h.ID, + Hostname: h.Hostname, + URL: hostURL.String(), + } + } + return shortHosts +} + +func (m *Mapper) GetPayload( + hostBaseURL *url.URL, + hosts []*fleet.HostShort, + vuln fleet.SoftwareVulnerability, + meta fleet.CVEMeta, +) WebhookPayload { + return WebhookPayload{ + CVE: vuln.CVE, + Link: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE), + Hosts: m.getHostPayloadPart(hostBaseURL, hosts), + } +} diff --git a/server/webhooks/mapper_test.go b/server/webhooks/mapper_test.go new file mode 100644 index 0000000000..759680ecd8 --- /dev/null +++ b/server/webhooks/mapper_test.go @@ -0,0 +1,33 @@ +package webhooks + +import ( + "net/url" + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/stretchr/testify/require" +) + +func TestGetPaylaod(t *testing.T) { + serverURL, err := url.Parse("http://mywebsite.com") + require.NoError(t, err) + + vuln := fleet.SoftwareVulnerability{ + CVE: "cve-1", + SoftwareID: 1, + } + meta := fleet.CVEMeta{ + CVE: "cve-1", + CVSSScore: ptr.Float64(1), + EPSSProbability: ptr.Float64(0.5), + CISAKnownExploit: ptr.Bool(true), + } + + sut := Mapper{} + + result := sut.GetPayload(serverURL, nil, vuln, meta) + require.Empty(t, result.CISAKnownExploit) + require.Empty(t, result.EPSSProbability) + require.Empty(t, result.CVSSScore) +} diff --git a/server/webhooks/vuln_args.go b/server/webhooks/vuln_args.go new file mode 100644 index 0000000000..56eb313b36 --- /dev/null +++ b/server/webhooks/vuln_args.go @@ -0,0 +1,14 @@ +package webhooks + +import ( + "time" + + "github.com/fleetdm/fleet/v4/server/fleet" +) + +type VulnArgs struct { + Vulnerablities []fleet.SoftwareVulnerability + Meta map[string]fleet.CVEMeta + AppConfig *fleet.AppConfig + Time time.Time +} diff --git a/server/webhooks/vulnerabilities.go b/server/webhooks/vulnerabilities.go index a0d46997f7..25e829cbc3 100644 --- a/server/webhooks/vulnerabilities.go +++ b/server/webhooks/vulnerabilities.go @@ -2,10 +2,7 @@ package webhooks import ( "context" - "fmt" "net/url" - "path" - "strconv" "time" "github.com/fleetdm/fleet/v4/server" @@ -20,18 +17,18 @@ func TriggerVulnerabilitiesWebhook( ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, - recentVulns []fleet.SoftwareVulnerability, - appConfig *fleet.AppConfig, - now time.Time, + args VulnArgs, + mapper VulnMapper, ) error { - vulnConfig := appConfig.WebhookSettings.VulnerabilitiesWebhook + vulnConfig := args.AppConfig.WebhookSettings.VulnerabilitiesWebhook + if !vulnConfig.Enable { return nil } - level.Debug(logger).Log("enabled", "true", "recentVulns", len(recentVulns)) + level.Debug(logger).Log("enabled", "true", "recentVulns", len(args.Vulnerablities)) - serverURL, err := url.Parse(appConfig.ServerSettings.ServerURL) + serverURL, err := url.Parse(args.AppConfig.ServerSettings.ServerURL) if err != nil { return ctxerr.Wrap(ctx, err, "invalid server url") } @@ -40,11 +37,11 @@ func TriggerVulnerabilitiesWebhook( batchSize := vulnConfig.HostBatchSize softwareIDsGroupedByCVE := make(map[string][]uint) - for _, v := range recentVulns { + for _, v := range args.Vulnerablities { softwareIDsGroupedByCVE[v.CVE] = append(softwareIDsGroupedByCVE[v.CVE], v.SoftwareID) } - for _, v := range recentVulns { + for _, v := range args.Vulnerablities { softwareIDs := softwareIDsGroupedByCVE[v.CVE] hosts, err := ds.HostsBySoftwareIDs(ctx, softwareIDs) @@ -57,7 +54,8 @@ func TriggerVulnerabilitiesWebhook( if batchSize > 0 && len(hosts) > batchSize { limit = batchSize } - if err := sendVulnerabilityHostBatch(ctx, targetURL, v.CVE, serverURL, hosts[:limit], now); err != nil { + payload := mapper.GetPayload(serverURL, hosts[:limit], v, args.Meta[v.CVE]) + if err := sendVulnerabilityHostBatch(ctx, targetURL, payload, args.Time); err != nil { return ctxerr.Wrap(ctx, err, "send vulnerability host batch") } hosts = hosts[limit:] @@ -67,31 +65,10 @@ func TriggerVulnerabilitiesWebhook( return nil } -type vulnHostPayload struct { - ID uint `json:"id"` - Hostname string `json:"hostname"` - URL string `json:"url"` -} - -func sendVulnerabilityHostBatch(ctx context.Context, targetURL, cve string, hostBaseURL *url.URL, hosts []*fleet.HostShort, now time.Time) error { - shortHosts := make([]*vulnHostPayload, len(hosts)) - for i, h := range hosts { - hostURL := *hostBaseURL - hostURL.Path = path.Join(hostURL.Path, "hosts", strconv.Itoa(int(h.ID))) - shortHosts[i] = &vulnHostPayload{ - ID: h.ID, - Hostname: h.Hostname, - URL: hostURL.String(), - } - } - +func sendVulnerabilityHostBatch(ctx context.Context, targetURL string, vuln WebhookPayload, now time.Time) error { payload := map[string]interface{}{ - "timestamp": now, - "vulnerability": map[string]interface{}{ - "cve": cve, - "details_link": fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", cve), - "hosts_affected": shortHosts, - }, + "timestamp": now, + "vulnerability": vuln, } if err := server.PostJSONWithTimeout(ctx, targetURL, &payload); err != nil { diff --git a/server/webhooks/vulnerabilities_test.go b/server/webhooks/vulnerabilities_test.go index 7467edbcf7..4166e75910 100644 --- a/server/webhooks/vulnerabilities_test.go +++ b/server/webhooks/vulnerabilities_test.go @@ -21,6 +21,7 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) { ctx := context.Background() ds := new(mock.Store) logger := kitlog.NewNopLogger() + mapper := Mapper{} appCfg := &fleet.AppConfig{ WebhookSettings: fleet.WebhookSettings{ @@ -42,20 +43,38 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) { t.Run("disabled", func(t *testing.T) { appCfg := *appCfg appCfg.WebhookSettings.VulnerabilitiesWebhook.Enable = false - err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, recentVulns, &appCfg, time.Now()) + args := VulnArgs{ + Vulnerablities: recentVulns, + Meta: nil, + AppConfig: &appCfg, + Time: time.Now(), + } + err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, args, &mapper) require.NoError(t, err) }) t.Run("invalid server url", func(t *testing.T) { appCfg := *appCfg appCfg.ServerSettings.ServerURL = ":nope:" - err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, recentVulns, &appCfg, time.Now()) + args := VulnArgs{ + Vulnerablities: recentVulns, + Meta: nil, + AppConfig: &appCfg, + Time: time.Now(), + } + err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, args, &mapper) require.Error(t, err) assert.Contains(t, err.Error(), "invalid server") }) t.Run("empty recent vulns", func(t *testing.T) { - err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, nil, appCfg, time.Now()) + args := VulnArgs{ + Vulnerablities: nil, + Meta: nil, + AppConfig: appCfg, + Time: time.Now(), + } + err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, args, &mapper) require.NoError(t, err) }) @@ -85,36 +104,42 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) { cases := []struct { name string vulns []fleet.SoftwareVulnerability + meta map[string]fleet.CVEMeta hosts []*fleet.HostShort want string }{ { "1 vuln, 1 host", []fleet.SoftwareVulnerability{{CVE: cves[0], SoftwareID: 1}}, + nil, hosts[:1], fmt.Sprintf("%s[%s]}}", jsonCVE1, jsonH1), }, { "1 vuln, 2 hosts", []fleet.SoftwareVulnerability{{CVE: cves[0], SoftwareID: 1}}, + nil, hosts[:2], fmt.Sprintf("%s[%s,%s]}}", jsonCVE1, jsonH1, jsonH2), }, { "1 vuln, 3 hosts", []fleet.SoftwareVulnerability{{CVE: cves[0], SoftwareID: 1}}, + nil, hosts[:3], fmt.Sprintf("%s[%s,%s]}}\n%s[%s]}}", jsonCVE1, jsonH1, jsonH2, jsonCVE1, jsonH3), // 2 requests, batch of 2 max }, { "1 vuln, 4 hosts", []fleet.SoftwareVulnerability{{CVE: cves[0], SoftwareID: 1}}, + nil, hosts[:4], fmt.Sprintf("%s[%s,%s]}}\n%s[%s,%s]}}", jsonCVE1, jsonH1, jsonH2, jsonCVE1, jsonH3, jsonH4), // 2 requests, batch of 2 max }, { "2 vulns, 1 host each", []fleet.SoftwareVulnerability{{CVE: cves[0], SoftwareID: 1}, {CVE: cves[1], SoftwareID: 2}}, + nil, hosts[:1], fmt.Sprintf("%s[%s]}}\n%s[%s]}}", jsonCVE1, jsonH1, jsonCVE2, jsonH1), }, @@ -138,7 +163,14 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) { appCfg := *appCfg appCfg.WebhookSettings.VulnerabilitiesWebhook.DestinationURL = srv.URL - err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, c.vulns, &appCfg, now) + args := VulnArgs{ + Vulnerablities: c.vulns, + Meta: c.meta, + AppConfig: &appCfg, + Time: now, + } + + err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, args, &mapper) require.NoError(t, err) assert.True(t, ds.HostsBySoftwareIDsFuncInvoked)