Allow vulnerability webhook to fire for fleet free (#39810)
**Related issue:** Resolves #29076 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Vulnerability webhooks are now available for free-tier users. * **Improvements** * Refined webhook payload display for free-tier users by removing certain advanced vulnerability metrics. * Updated UI text descriptions in automation management to reflect free-tier vulnerability scanning behavior. * Simplified permission requirements for accessing automation management features. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Updated logic to trigger vulnerability webhook when on fleet free tier
|
||||
+12
-6
@@ -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":
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+29
-9
@@ -133,7 +133,9 @@ const ManageAutomationsModal = ({
|
||||
setSelectedIntegration,
|
||||
] = useState<IIntegration>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className={`${baseClass}__software-automation-description`}>
|
||||
A ticket will be created in your <b>Integration</b> 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 <b>Integration</b> for each
|
||||
detected vulnerability (CVE).
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
A ticket will be created in your <b>Integration</b> if a detected
|
||||
vulnerability (CVE) was published in the last{" "}
|
||||
{recentVulnerabilityMaxAge ||
|
||||
CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS}{" "}
|
||||
days.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(jiraIntegrationsIndexed && jiraIntegrationsIndexed.length > 0) ||
|
||||
(zendeskIntegrationsIndexed &&
|
||||
@@ -427,9 +438,18 @@ const ManageAutomationsModal = ({
|
||||
<>
|
||||
<div className={`${baseClass}__software-automation-description`}>
|
||||
<p>
|
||||
A request will be sent to your configured <b>Destination URL</b> if
|
||||
a detected vulnerability (CVE) was published in the last{" "}
|
||||
{recentVulnerabilityMaxAge || "30"} days.
|
||||
{isFreeTier ? (
|
||||
<>
|
||||
A request will be sent to your configured <b>Destination URL</b>{" "}
|
||||
for each detected vulnerability (CVE).
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
A request will be sent to your configured <b>Destination URL</b>{" "}
|
||||
if a detected vulnerability (CVE) was published in the last{" "}
|
||||
{recentVulnerabilityMaxAge || "30"} days.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<InputField
|
||||
|
||||
+1
@@ -65,6 +65,7 @@ const PreviewPayloadModal = ({
|
||||
delete json.vulnerability.epss_probability;
|
||||
delete json.vulnerability.cvss_score;
|
||||
delete json.vulnerability.cisa_known_exploit;
|
||||
delete json.vulnerability.cve_published;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user