Trigger webhooks for recently published vulnerabilities (#3941)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
)
|
||||
|
||||
// TriggerVulnerabilitiesWebhook performs the webhook requests for vulnerabilities.
|
||||
func TriggerVulnerabilitiesWebhook(
|
||||
ctx context.Context,
|
||||
ds fleet.Datastore,
|
||||
logger kitlog.Logger,
|
||||
recentVulns map[string][]string,
|
||||
appConfig *fleet.AppConfig,
|
||||
now time.Time,
|
||||
) error {
|
||||
vulnConfig := appConfig.WebhookSettings.VulnerabilitiesWebhook
|
||||
if !vulnConfig.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
level.Debug(logger).Log("enabled", "true", "recentVulns", len(recentVulns))
|
||||
|
||||
serverURL, err := url.Parse(appConfig.ServerSettings.ServerURL)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "invalid server url")
|
||||
}
|
||||
|
||||
targetURL := vulnConfig.DestinationURL
|
||||
batchSize := vulnConfig.HostBatchSize
|
||||
|
||||
for cve, cpes := range recentVulns {
|
||||
hosts, err := ds.HostsByCPEs(ctx, cpes)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get hosts by CPE")
|
||||
}
|
||||
|
||||
for len(hosts) > 0 {
|
||||
limit := len(hosts)
|
||||
if batchSize > 0 && len(hosts) > batchSize {
|
||||
limit = batchSize
|
||||
}
|
||||
if err := sendVulnerabilityHostBatch(ctx, targetURL, cve, serverURL, hosts[:limit], now); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "send vulnerability host batch")
|
||||
}
|
||||
hosts = hosts[limit:]
|
||||
}
|
||||
}
|
||||
|
||||
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.CPEHost, 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(),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
if err := server.PostJSONWithTimeout(ctx, targetURL, &payload); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "posting to %s", targetURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tj/assert"
|
||||
)
|
||||
|
||||
func TestTriggerVulnerabilitiesWebhook(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ds := new(mock.Store)
|
||||
logger := kitlog.NewNopLogger()
|
||||
|
||||
appCfg := &fleet.AppConfig{
|
||||
WebhookSettings: fleet.WebhookSettings{
|
||||
VulnerabilitiesWebhook: fleet.VulnerabilitiesWebhookSettings{
|
||||
Enable: true,
|
||||
HostBatchSize: 2,
|
||||
},
|
||||
},
|
||||
ServerSettings: fleet.ServerSettings{
|
||||
ServerURL: "https://fleet.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
recentVulns := map[string][]string{
|
||||
"CVE-2012-1234": {"cpe1", "cpe2"},
|
||||
}
|
||||
|
||||
t.Run("disabled", func(t *testing.T) {
|
||||
appCfg := *appCfg
|
||||
appCfg.WebhookSettings.VulnerabilitiesWebhook.Enable = false
|
||||
err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, recentVulns, &appCfg, time.Now())
|
||||
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())
|
||||
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())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("trigger requests", func(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
hosts := []*fleet.CPEHost{
|
||||
{ID: 1, Hostname: "h1"},
|
||||
{ID: 2, Hostname: "h2"},
|
||||
{ID: 3, Hostname: "h3"},
|
||||
{ID: 4, Hostname: "h4"},
|
||||
}
|
||||
jsonH1 := fmt.Sprintf(`{"id":1,"hostname":"h1","url":"%s/hosts/1"}`, appCfg.ServerSettings.ServerURL)
|
||||
jsonH2 := fmt.Sprintf(`{"id":2,"hostname":"h2","url":"%s/hosts/2"}`, appCfg.ServerSettings.ServerURL)
|
||||
jsonH3 := fmt.Sprintf(`{"id":3,"hostname":"h3","url":"%s/hosts/3"}`, appCfg.ServerSettings.ServerURL)
|
||||
jsonH4 := fmt.Sprintf(`{"id":4,"hostname":"h4","url":"%s/hosts/4"}`, appCfg.ServerSettings.ServerURL)
|
||||
|
||||
cves := []string{
|
||||
"CVE-2012-1234",
|
||||
"CVE-2012-4567",
|
||||
}
|
||||
jsonCVE1 := fmt.Sprintf(`{"timestamp":"%s","vulnerability":{"cve":%q,"details_link":"https://nvd.nist.gov/vuln/detail/%[2]s","hosts_affected":`,
|
||||
now.Format(time.RFC3339Nano), cves[0])
|
||||
jsonCVE2 := fmt.Sprintf(`{"timestamp":"%s","vulnerability":{"cve":%q,"details_link":"https://nvd.nist.gov/vuln/detail/%[2]s","hosts_affected":`,
|
||||
now.Format(time.RFC3339Nano), cves[1])
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
vulns map[string][]string
|
||||
hosts []*fleet.CPEHost
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"1 vuln, 1 host",
|
||||
map[string][]string{cves[0]: {"cpe1"}},
|
||||
hosts[:1],
|
||||
fmt.Sprintf("%s[%s]}}", jsonCVE1, jsonH1),
|
||||
},
|
||||
{
|
||||
"1 vuln, 2 hosts",
|
||||
map[string][]string{cves[0]: {"cpe1"}},
|
||||
hosts[:2],
|
||||
fmt.Sprintf("%s[%s,%s]}}", jsonCVE1, jsonH1, jsonH2),
|
||||
},
|
||||
{
|
||||
"1 vuln, 3 hosts",
|
||||
map[string][]string{cves[0]: {"cpe1"}},
|
||||
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",
|
||||
map[string][]string{cves[0]: {"cpe1"}},
|
||||
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",
|
||||
map[string][]string{cves[0]: {"cpe1"}, cves[1]: {"cpe2"}},
|
||||
hosts[:1],
|
||||
fmt.Sprintf("%s[%s]}}\n%s[%s]}}", jsonCVE1, jsonH1, jsonCVE2, jsonH1),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var requests []string
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
requests = append(requests, string(b))
|
||||
w.Write(nil)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
ds.HostsByCPEsFunc = func(ctx context.Context, cpes []string) ([]*fleet.CPEHost, error) {
|
||||
return c.hosts, nil
|
||||
}
|
||||
|
||||
appCfg := *appCfg
|
||||
appCfg.WebhookSettings.VulnerabilitiesWebhook.DestinationURL = srv.URL
|
||||
err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, c.vulns, &appCfg, now)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, ds.HostsByCPEsFuncInvoked)
|
||||
ds.HostsByCPEsFuncInvoked = false
|
||||
|
||||
want := strings.Split(c.want, "\n")
|
||||
assert.ElementsMatch(t, want, requests)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user