diff --git a/changes/29076-vuln-freetier-webhook b/changes/29076-vuln-freetier-webhook new file mode 100644 index 0000000000..dd4179ea2e --- /dev/null +++ b/changes/29076-vuln-freetier-webhook @@ -0,0 +1 @@ +* Updated logic to trigger vulnerability webhook when on fleet free tier \ No newline at end of file diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 65cca59f16..2e7794657d 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -206,14 +206,20 @@ func scanVulnerabilities( vulns = append(vulns, govalDictVulns...) vulns = append(vulns, customVulns...) - meta, err := ds.ListCVEs(ctx, config.RecentVulnerabilityMaxAge) - if err != nil { - errHandler(ctx, logger, "could not fetch CVE meta", err) - return nil + var recentV []fleet.SoftwareVulnerability + var matchingMeta map[string]fleet.CVEMeta + if license.IsPremium(ctx) { + meta, err := ds.ListCVEs(ctx, config.RecentVulnerabilityMaxAge) + if err != nil { + errHandler(ctx, logger, "could not fetch CVE meta", err) + return nil + } + recentV, matchingMeta = utils.RecentVulns(vulns, meta) + } else { + recentV = vulns + matchingMeta = make(map[string]fleet.CVEMeta) } - recentV, matchingMeta := utils.RecentVulns(vulns, meta) - if len(recentV) > 0 { switch vulnAutomationEnabled { case "webhook": diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index fb593e3e4c..4d8629bbca 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -596,6 +596,173 @@ func TestScanVulnerabilities(t *testing.T) { require.Equal(t, 1, webhookCount) } +func TestScanVulnerabilitiesFreeTier(t *testing.T) { + nettest.Run(t) + + logger := logging.NewNopLogger() + + ctx := context.Background() + + var mu sync.Mutex + var webhookCVEs []string + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var payload map[string]json.RawMessage + err := json.NewDecoder(r.Body).Decode(&payload) + require.NoError(t, err) + + // Free tier payload + var vuln map[string]json.RawMessage + require.NoError(t, json.Unmarshal(payload["vulnerability"], &vuln)) + require.NotContains(t, vuln, "epss_probability") + require.NotContains(t, vuln, "cvss_score") + require.NotContains(t, vuln, "cisa_known_exploit") + require.NotContains(t, vuln, "cve_published") + require.Contains(t, vuln, "cve") + require.Contains(t, vuln, "details_link") + require.Contains(t, vuln, "hosts_affected") + + var cve string + require.NoError(t, json.Unmarshal(vuln["cve"], &cve)) + mu.Lock() + webhookCVEs = append(webhookCVEs, cve) + mu.Unlock() + })) + + appConfig := &fleet.AppConfig{ + Features: fleet.Features{ + EnableSoftwareInventory: true, + }, + WebhookSettings: fleet.WebhookSettings{ + VulnerabilitiesWebhook: fleet.VulnerabilitiesWebhookSettings{ + Enable: true, + DestinationURL: svr.URL, + }, + }, + } + + ds := new(mock.Store) + ds.InsertCVEMetaFunc = func(ctx context.Context, x []fleet.CVEMeta) error { + return nil + } + ds.AllSoftwareIteratorFunc = func(ctx context.Context, query fleet.SoftwareIterQueryOptions) (fleet.SoftwareIterator, error) { + iterator := &softwareIterator{ + softwares: []*fleet.Software{ + { + ID: 1, + Name: "Twisted", + Version: "22.2.0", + BundleIdentifier: "", + Source: "python_packages", + }, + }, + } + return iterator, nil + } + ds.ListSoftwareCPEsFunc = func(ctx context.Context) ([]fleet.SoftwareCPE, error) { + return []fleet.SoftwareCPE{ + { + ID: 1, + SoftwareID: 1, + CPE: "cpe:2.3:a:twistedmatrix:twisted:22.2.0:*:*:*:*:python:*:*", + }, + }, nil + } + ds.InsertSoftwareVulnerabilityFunc = func(ctx context.Context, vuln fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) (bool, error) { + return true, nil + } + ds.UpsertSoftwareCPEsFunc = func(ctx context.Context, cpes []fleet.SoftwareCPE) (int64, error) { + return int64(0), nil + } + ds.DeleteSoftwareCPEsFunc = func(ctx context.Context, cpes []fleet.SoftwareCPE) (int64, error) { + return int64(0), nil + } + ds.DeleteOutOfDateVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error { + return nil + } + ds.OSVersionsFunc = func( + ctx context.Context, teamFilter *fleet.TeamFilter, platform *string, name *string, version *string, + ) (*fleet.OSVersions, error) { + return &fleet.OSVersions{ + CountsUpdatedAt: time.Now(), + OSVersions: []fleet.OSVersion{ + {HostsCount: 1, Name: "Ubuntu 22.04.1 LTS", Platform: "ubuntu"}, + }, + }, nil + } + ds.HostIDsByOSVersionFunc = func(ctx context.Context, osVersion fleet.OSVersion, offset int, limit int) ([]uint, error) { + if offset == 0 { + return []uint{1}, nil + } + return []uint{}, nil + } + ds.ListSoftwareForVulnDetectionFunc = func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) { + return []fleet.Software{ + { + ID: 1, + Name: "Twisted", + Version: "22.2.0", + BundleIdentifier: "", + Source: "python_packages", + }, + }, nil + } + ds.ListSoftwareVulnerabilitiesByHostIDsSourceFunc = func(ctx context.Context, hostIDs []uint, source fleet.VulnerabilitySource) (map[uint][]fleet.SoftwareVulnerability, error) { + require.Equal(t, []uint{1}, hostIDs) + require.Equal(t, fleet.UbuntuOVALSource, source) + return map[uint][]fleet.SoftwareVulnerability{}, nil + } + ds.ListOperatingSystemsFunc = func(ctx context.Context) ([]fleet.OperatingSystem, error) { + return []fleet.OperatingSystem{ + { + ID: 1, + Name: "Ubuntu", + Version: "22.04.1 LTS", + Arch: "x86_64", + KernelVersion: "5.10.124-linuxkit", + }, + }, nil + } + ds.ListOperatingSystemsForPlatformFunc = func(ctx context.Context, platform string) ([]fleet.OperatingSystem, error) { + return []fleet.OperatingSystem{}, nil + } + ds.DeleteOutOfDateOSVulnerabilitiesFunc = func(ctx context.Context, src fleet.VulnerabilitySource, t time.Time) error { + return nil + } + ds.ListCVEsFunc = func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error) { + t.Error("ListCVEs should not be called on free tier") + return nil, nil + } + ds.HostVulnSummariesBySoftwareIDsFunc = func(ctx context.Context, softwareIDs []uint) ([]fleet.HostVulnerabilitySummary, error) { + return []fleet.HostVulnerabilitySummary{ + { + ID: 1, + Hostname: "1", + DisplayName: "1", + }, + }, nil + } + ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { + return true, nil + } + + vulnPath := filepath.Join("..", "..", "server", "vulnerabilities", "testdata") + + vulnsConfig := config.VulnerabilitiesConfig{ + DatabasesPath: vulnPath, + Periodicity: 10 * time.Second, + CurrentInstanceChecks: "auto", + DisableDataSync: true, + } + + ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierFree}) + err := scanVulnerabilities(ctx, ds, logger, &vulnsConfig, appConfig, vulnPath) + require.NoError(t, err) + + require.False(t, ds.DeleteSoftwareVulnerabilitiesFuncInvoked) + + require.NotEmpty(t, webhookCVEs) +} + func TestUpdateVulnHostCounts(t *testing.T) { logger := logging.NewNopLogger() diff --git a/frontend/pages/SoftwarePage/SoftwarePage.tsx b/frontend/pages/SoftwarePage/SoftwarePage.tsx index 013c7b6678..0ebe8e8bb3 100644 --- a/frontend/pages/SoftwarePage/SoftwarePage.tsx +++ b/frontend/pages/SoftwarePage/SoftwarePage.tsx @@ -327,7 +327,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { ); const renderPageActions = () => { - const canManageAutomations = isGlobalAdmin && isPremiumTier; + const canManageAutomations = isGlobalAdmin; const canAddSoftware = isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer; diff --git a/frontend/pages/SoftwarePage/components/modals/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx b/frontend/pages/SoftwarePage/components/modals/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx index a3aefbf075..fd5833365b 100644 --- a/frontend/pages/SoftwarePage/components/modals/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx +++ b/frontend/pages/SoftwarePage/components/modals/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx @@ -133,7 +133,9 @@ const ManageAutomationsModal = ({ setSelectedIntegration, ] = useState(); - const { config: globalConfigFromContext } = useContext(AppContext); + const { config: globalConfigFromContext, isFreeTier } = useContext( + AppContext + ); const gitOpsModeEnabled = globalConfigFromContext?.gitops.gitops_mode_enabled; const maxAgeInNanoseconds = isGlobalSWConfig(softwareConfig) @@ -376,11 +378,20 @@ const ManageAutomationsModal = ({ return ( <>
- A ticket will be created in your Integration if a detected - vulnerability (CVE) was published in the last{" "} - {recentVulnerabilityMaxAge || - CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS}{" "} - days. + {isFreeTier ? ( + <> + A ticket will be created in your Integration for each + detected vulnerability (CVE). + + ) : ( + <> + A ticket will be created in your Integration if a detected + vulnerability (CVE) was published in the last{" "} + {recentVulnerabilityMaxAge || + CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS}{" "} + days. + + )}
{(jiraIntegrationsIndexed && jiraIntegrationsIndexed.length > 0) || (zendeskIntegrationsIndexed && @@ -427,9 +438,18 @@ const ManageAutomationsModal = ({ <>

- A request will be sent to your configured Destination URL if - a detected vulnerability (CVE) was published in the last{" "} - {recentVulnerabilityMaxAge || "30"} days. + {isFreeTier ? ( + <> + A request will be sent to your configured Destination URL{" "} + for each detected vulnerability (CVE). + + ) : ( + <> + A request will be sent to your configured Destination URL{" "} + if a detected vulnerability (CVE) was published in the last{" "} + {recentVulnerabilityMaxAge || "30"} days. + + )}