Feature 7559: Include 3rd party severity scores in the vulnerability web-hook payload (#7581)
Premium users should receive vulnerability scores as part of the web-hook payload.
This commit is contained in:
@@ -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.
|
||||
+44
-31
@@ -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(
|
||||
|
||||
+19
-9
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
+2
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+3
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user