Feature 10196: Add filepath to end-points and third party integrations (#11285)

Adds the software installed path property to the proper end-points and third party integrations (webhook, Zendesk and Jira).
This commit is contained in:
Juan Fernandez 2023-05-17 16:53:15 -04:00 committed by GitHub
parent 49b04ba4a5
commit 009a87d33e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 494 additions and 239 deletions

View File

@ -0,0 +1,4 @@
- The 'GET /api/v1/fleet/hosts/{id}' and 'GET /api/v1/fleet/hosts/identifier/{identifier}' now
include the software installed path on their payload.
- Third party vulnerability integrations now include the installed path of the vulnerable software
on each host.

View File

@ -477,8 +477,8 @@ func TestScanVulnerabilities(t *testing.T) {
},
}, nil
}
ds.HostsBySoftwareIDsFunc = func(ctx context.Context, softwareIDs []uint) ([]*fleet.HostShort, error) {
return []*fleet.HostShort{
ds.HostVulnSummariesBySoftwareIDsFunc = func(ctx context.Context, softwareIDs []uint) ([]fleet.HostVulnerabilitySummary, error) {
return []fleet.HostVulnerabilitySummary{
{
ID: 1,
Hostname: "1",

View File

@ -43,7 +43,8 @@ POST https://server.com/example
{
"id": 1,
"hostname": "macbook-1",
"url": "https://fleet.example.com/hosts/1"
"url": "https://fleet.example.com/hosts/1",
"software_installed_paths": ["/usr/lib/some-path"],
},
{
"id": 2,

View File

@ -2109,7 +2109,8 @@ Returns the information of the specified host.
"version": "4.5.1",
"source": "rpm_packages",
"generated_cpe": "",
"vulnerabilities": null
"vulnerabilities": null,
"installed_paths": ["/usr/lib/some-path-1"]
},
{
"id": 1146,
@ -2127,7 +2128,8 @@ Returns the information of the specified host.
"bundle_identifier": "com.some.app",
"last_opened_at": "2021-08-18T21:14:00Z",
"generated_cpe": "",
"vulnerabilities": null
"vulnerabilities": null,
"installed_paths": ["/usr/lib/some-path-2"]
}
],
"id": 1,
@ -2338,7 +2340,8 @@ Returns the information of the host specified using the `uuid`, `osquery_host_id
"version": "0.8.0",
"source": "python_packages",
"generated_cpe": "",
"vulnerabilities": null
"vulnerabilities": null,
"installed_paths": ["/usr/lib/some_path/"]
}
],
"id": 33,

View File

@ -17,7 +17,7 @@ func NewMapper() fleetwebhooks.VulnMapper {
func (m *Mapper) GetPayload(
hostBaseURL *url.URL,
hosts []*fleet.HostShort,
hosts []fleet.HostVulnerabilitySummary,
cve string,
meta fleet.CVEMeta,
) fleetwebhooks.WebhookPayload {

View File

@ -9,6 +9,7 @@ export interface IHostsAffected {
id: number;
display_name: string;
url: string;
software_installed_paths?: string[];
}
export interface IVulnerability {
cve: string;

View File

@ -36,6 +36,7 @@ const PreviewPayloadModal = ({
id: 1,
display_name: "macbook-1",
url: "https://fleet.example.com/hosts/1",
software_installed_paths: ["/usr/lib/some-path"],
},
{
id: 2,

View File

@ -112,6 +112,7 @@ func (ds *Datastore) UpdateHostSoftwareInstalledPaths(
})
}
// getHostSoftwareInstalledPaths returns all HostSoftwareInstalledPath for the given hostID.
func (ds *Datastore) getHostSoftwareInstalledPaths(
ctx context.Context,
hostID uint,
@ -852,7 +853,27 @@ func (ds *Datastore) LoadHostSoftware(ctx context.Context, host *fleet.Host, inc
if err != nil {
return err
}
host.Software = software
installedPaths, err := ds.getHostSoftwareInstalledPaths(
ctx,
host.ID,
)
if err != nil {
return err
}
lookup := make(map[uint][]string)
for _, ip := range installedPaths {
lookup[ip.SoftwareID] = append(lookup[ip.SoftwareID], ip.InstalledPath)
}
host.Software = make([]fleet.HostSoftwareEntry, 0, len(software))
for _, s := range software {
host.Software = append(host.Software, fleet.HostSoftwareEntry{
Software: s,
InstalledPaths: lookup[s.ID],
})
}
return nil
}
@ -1266,58 +1287,106 @@ func (ds *Datastore) SyncHostsSoftware(ctx context.Context, updatedAt time.Time)
return nil
}
// HostsBySoftwareIDs returns a list of all hosts that have at least one of the specified Software
// installed. It returns a minimal represention of matching hosts.
func (ds *Datastore) HostsBySoftwareIDs(ctx context.Context, softwareIDs []uint) ([]*fleet.HostShort, error) {
queryStmt := `
SELECT
h.id,
h.hostname,
if(h.computer_name = '', h.hostname, h.computer_name) display_name
FROM
hosts h
INNER JOIN
host_software hs
ON
h.id = hs.host_id
WHERE
hs.software_id IN (?)
GROUP BY h.id, h.hostname
ORDER BY
h.id`
func (ds *Datastore) HostVulnSummariesBySoftwareIDs(ctx context.Context, softwareIDs []uint) ([]fleet.HostVulnerabilitySummary, error) {
stmt := `
SELECT DISTINCT
h.id,
h.hostname,
if(h.computer_name = '', h.hostname, h.computer_name) display_name,
COALESCE(hsip.installed_path, '') AS software_installed_path
FROM hosts h
INNER JOIN host_software hs ON h.id = hs.host_id AND hs.software_id IN (?)
LEFT JOIN host_software_installed_paths hsip ON hs.host_id = hsip.host_id AND hs.software_id = hsip.software_id
ORDER BY h.id`
stmt, args, err := sqlx.In(queryStmt, softwareIDs)
stmt, args, err := sqlx.In(stmt, softwareIDs)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building query args")
}
var hosts []*fleet.HostShort
if err := sqlx.SelectContext(ctx, ds.reader, &hosts, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "select hosts by cpes")
var qR []struct {
HostID uint `db:"id"`
HostName string `db:"hostname"`
DisplayName string `db:"display_name"`
SPath string `db:"software_installed_path"`
}
return hosts, nil
if err := sqlx.SelectContext(ctx, ds.reader, &qR, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting hosts by softwareIDs")
}
var result []fleet.HostVulnerabilitySummary
lookup := make(map[uint]int)
for _, r := range qR {
i, ok := lookup[r.HostID]
if ok {
result[i].AddSoftwareInstalledPath(r.SPath)
continue
}
mapped := fleet.HostVulnerabilitySummary{
ID: r.HostID,
Hostname: r.HostName,
DisplayName: r.DisplayName,
}
mapped.AddSoftwareInstalledPath(r.SPath)
result = append(result, mapped)
lookup[r.HostID] = len(result) - 1
}
return result, nil
}
func (ds *Datastore) HostsByCVE(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
query := `
SELECT DISTINCT
(h.id),
h.hostname,
if(h.computer_name = '', h.hostname, h.computer_name) display_name
FROM
hosts h
INNER JOIN host_software hs ON h.id = hs.host_id
INNER JOIN software_cve scv ON scv.software_id = hs.software_id
WHERE
scv.cve = ?
ORDER BY
h.id
`
// ** DEPRECATED **
func (ds *Datastore) HostsByCVE(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
stmt := `
SELECT DISTINCT
(h.id),
h.hostname,
if(h.computer_name = '', h.hostname, h.computer_name) display_name,
COALESCE(hsip.installed_path, '') AS software_installed_path
FROM hosts h
INNER JOIN host_software hs ON h.id = hs.host_id
INNER JOIN software_cve scv ON scv.software_id = hs.software_id
LEFT JOIN host_software_installed_paths hsip ON hs.host_id = hsip.host_id AND hs.software_id = hsip.software_id
WHERE scv.cve = ?
ORDER BY h.id`
var hosts []*fleet.HostShort
if err := sqlx.SelectContext(ctx, ds.reader, &hosts, query, cve); err != nil {
return nil, ctxerr.Wrap(ctx, err, "select hosts by cves")
var qR []struct {
HostID uint `db:"id"`
HostName string `db:"hostname"`
DisplayName string `db:"display_name"`
SPath string `db:"software_installed_path"`
}
return hosts, nil
if err := sqlx.SelectContext(ctx, ds.reader, &qR, stmt, cve); err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting hosts by softwareIDs")
}
var result []fleet.HostVulnerabilitySummary
lookup := make(map[uint]int)
for _, r := range qR {
i, ok := lookup[r.HostID]
if ok {
result[i].AddSoftwareInstalledPath(r.SPath)
continue
}
mapped := fleet.HostVulnerabilitySummary{
ID: r.HostID,
Hostname: r.HostName,
DisplayName: r.DisplayName,
}
mapped.AddSoftwareInstalledPath(r.SPath)
result = append(result, mapped)
lookup[r.HostID] = len(result) - 1
}
return result, nil
}
func (ds *Datastore) InsertCVEMeta(ctx context.Context, cveMeta []fleet.CVEMeta) error {

View File

@ -37,7 +37,7 @@ func TestSoftware(t *testing.T) {
{"SyncHostsSoftware", testSoftwareSyncHostsSoftware},
{"DeleteSoftwareVulnerabilities", testDeleteSoftwareVulnerabilities},
{"HostsByCVE", testHostsByCVE},
{"HostsBySoftwareIDs", testHostsBySoftwareIDs},
{"HostVulnSummariesBySoftwareIDs", testHostVulnSummariesBySoftwareIDs},
{"UpdateHostSoftware", testUpdateHostSoftware},
{"UpdateHostSoftwareUpdatesSoftware", testUpdateHostSoftwareUpdatesSoftware},
{"ListSoftwareByHostIDShort", testListSoftwareByHostIDShort},
@ -79,21 +79,31 @@ func testSoftwareSaveHost(t *testing.T, ds *Datastore) {
{Name: "zoo", Version: "0.0.5", Source: "deb_packages", BundleIdentifier: ""},
}
getHostSoftware := func(h *fleet.Host) []fleet.Software {
var software []fleet.Software
for _, s := range h.Software {
software = append(software, s.Software)
}
return software
}
_, err := ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
require.NoError(t, err)
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
test.ElementsMatchSkipIDAndHostCount(t, software1, host1.HostSoftware.Software)
host1Software := getHostSoftware(host1)
test.ElementsMatchSkipIDAndHostCount(t, software1, host1Software)
soft1ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, false)
require.NoError(t, err)
require.NotNil(t, soft1ByID)
assert.Equal(t, host1.HostSoftware.Software[0], *soft1ByID)
assert.Equal(t, host1Software[0], *soft1ByID)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
test.ElementsMatchSkipIDAndHostCount(t, software2, host2.HostSoftware.Software)
host2Software := getHostSoftware(host2)
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
software1 = []fleet.Software{
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
@ -108,10 +118,12 @@ func testSoftwareSaveHost(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
test.ElementsMatchSkipIDAndHostCount(t, software1, host1.HostSoftware.Software)
host1Software = getHostSoftware(host1)
test.ElementsMatchSkipIDAndHostCount(t, software1, host1Software)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
test.ElementsMatchSkipIDAndHostCount(t, software2, host2.HostSoftware.Software)
host2Software = getHostSoftware(host2)
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
software1 = []fleet.Software{
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
@ -120,9 +132,9 @@ func testSoftwareSaveHost(t *testing.T, ds *Datastore) {
_, err = ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
test.ElementsMatchSkipIDAndHostCount(t, software1, host1.HostSoftware.Software)
host1Software = getHostSoftware(host1)
test.ElementsMatchSkipIDAndHostCount(t, software1, host1Software)
software2 = []fleet.Software{
{Name: "foo", Version: "0.0.2", Source: "chrome_extensions"},
@ -133,7 +145,8 @@ func testSoftwareSaveHost(t *testing.T, ds *Datastore) {
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
test.ElementsMatchSkipIDAndHostCount(t, software2, host2.HostSoftware.Software)
host2Software = getHostSoftware(host2)
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
software2 = []fleet.Software{
{Name: "foo", Version: "0.0.2", Source: "chrome_extensions"},
@ -144,7 +157,8 @@ func testSoftwareSaveHost(t *testing.T, ds *Datastore) {
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
test.ElementsMatchSkipIDAndHostCount(t, software2, host2.HostSoftware.Software)
host2Software = getHostSoftware(host2)
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
}
func testSoftwareCPE(t *testing.T, ds *Datastore) {
@ -1132,11 +1146,28 @@ func insertVulnSoftwareForTest(t *testing.T, ds *Datastore) {
},
}
_, err := ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
mutationResults, err := ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
require.NoError(t, err)
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
// Insert paths for software1
s1Paths := map[string]struct{}{}
for _, s := range software1 {
key := fmt.Sprintf("%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, s.ToUniqueStr())
s1Paths[key] = struct{}{}
}
require.NoError(t, ds.UpdateHostSoftwareInstalledPaths(context.Background(), host1.ID, s1Paths, mutationResults))
mutationResults, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
require.NoError(t, err)
// Insert paths for software2
s2Paths := map[string]struct{}{}
for _, s := range software2 {
key := fmt.Sprintf("%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, s.ToUniqueStr())
s2Paths[key] = struct{}{}
}
require.NoError(t, ds.UpdateHostSoftwareInstalledPaths(context.Background(), host2.ID, s2Paths, mutationResults))
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
sort.Slice(host1.Software, func(i, j int) bool {
@ -1279,15 +1310,21 @@ func testHostsByCVE(t *testing.T, ds *Datastore) {
hosts, err = ds.HostsByCVE(ctx, "CVE-2022-0001")
require.NoError(t, err)
require.Len(t, hosts, 2)
require.ElementsMatch(t, hosts, []*fleet.HostShort{
require.ElementsMatch(t, hosts, []fleet.HostVulnerabilitySummary{
{
ID: 1,
Hostname: "host1",
DisplayName: "computer1",
SoftwareInstalledPaths: []string{
"/some/path/foo.chrome",
},
}, {
ID: 2,
Hostname: "host2",
DisplayName: "host2",
SoftwareInstalledPaths: []string{
"/some/path/foo.chrome",
},
},
})
@ -1298,10 +1335,11 @@ func testHostsByCVE(t *testing.T, ds *Datastore) {
require.Equal(t, hosts[0].Hostname, "host2")
}
func testHostsBySoftwareIDs(t *testing.T, ds *Datastore) {
func testHostVulnSummariesBySoftwareIDs(t *testing.T, ds *Datastore) {
ctx := context.Background()
hosts, err := ds.HostsBySoftwareIDs(ctx, []uint{0})
// Invalid non-existing host id
hosts, err := ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{0})
require.NoError(t, err)
require.Len(t, hosts, 0)
@ -1310,48 +1348,57 @@ func testHostsBySoftwareIDs(t *testing.T, ds *Datastore) {
allSoftware, err := ds.ListSoftware(ctx, fleet.SoftwareListOptions{})
require.NoError(t, err)
var fooRpm fleet.Software
var chrome3 fleet.Software
var barRpm fleet.Software
for _, s := range allSoftware {
if s.GenerateCPE == "cpe_foo_chrome_3" {
switch s.GenerateCPE {
case "cpe_foo_rpm":
fooRpm = s
case "cpe_foo_chrome_3":
chrome3 = s
}
if s.GenerateCPE == "cpe_bar_rpm" {
case "cpe_bar_rpm":
barRpm = s
}
}
require.NotZero(t, chrome3.ID)
require.NotZero(t, barRpm.ID)
hosts, err = ds.HostsBySoftwareIDs(ctx, []uint{chrome3.ID})
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{chrome3.ID})
require.NoError(t, err)
require.Len(t, hosts, 2)
require.ElementsMatch(t, hosts, []*fleet.HostShort{
require.ElementsMatch(t, hosts, []fleet.HostVulnerabilitySummary{
{
ID: 1,
Hostname: "host1",
DisplayName: "computer1",
ID: 1,
Hostname: "host1",
DisplayName: "computer1",
SoftwareInstalledPaths: []string{"/some/path/foo.chrome"},
}, {
ID: 2,
Hostname: "host2",
DisplayName: "host2",
ID: 2,
Hostname: "host2",
DisplayName: "host2",
SoftwareInstalledPaths: []string{"/some/path/foo.chrome"},
},
})
hosts, err = ds.HostsBySoftwareIDs(ctx, []uint{barRpm.ID})
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{barRpm.ID})
require.NoError(t, err)
require.Len(t, hosts, 1)
require.Equal(t, hosts[0].Hostname, "host2")
require.ElementsMatch(t, hosts, []fleet.HostVulnerabilitySummary{
{
ID: 2,
Hostname: "host2",
DisplayName: "host2",
SoftwareInstalledPaths: []string{"/some/path/bar.rpm"},
},
})
// Duplicates should not be returned if cpes are found on the same host ie host2 should only appear once
hosts, err = ds.HostsBySoftwareIDs(ctx, []uint{chrome3.ID, barRpm.ID})
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{chrome3.ID, barRpm.ID, fooRpm.ID})
require.NoError(t, err)
require.Len(t, hosts, 2)
require.Equal(t, hosts[0].Hostname, "host1")
require.Equal(t, hosts[1].Hostname, "host2")
require.ElementsMatch(t, hosts[0].SoftwareInstalledPaths, []string{"/some/path/foo.rpm", "/some/path/foo.chrome"})
require.ElementsMatch(t, hosts[1].SoftwareInstalledPaths, []string{"/some/path/bar.rpm", "/some/path/foo.chrome"})
}
// testUpdateHostSoftwareUpdatesSoftware tests that uninstalling applications
@ -1454,15 +1501,15 @@ func testUpdateHostSoftwareUpdatesSoftware(t *testing.T, ds *Datastore) {
}
cmpNameVersionCount(expectedSoftware, software)
hosts, err := ds.HostsBySoftwareIDs(ctx, []uint{bazSoftwareID})
hosts, err := ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{bazSoftwareID})
require.NoError(t, err)
require.Len(t, hosts, 1)
require.Equal(t, hosts[0].ID, h1.ID)
hosts, err = ds.HostsBySoftwareIDs(ctx, []uint{barSoftwareID})
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{barSoftwareID})
require.NoError(t, err)
require.Empty(t, hosts)
hosts, err = ds.HostsBySoftwareIDs(ctx, []uint{baz2SoftwareID})
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{baz2SoftwareID})
require.NoError(t, err)
require.Empty(t, hosts)
@ -1486,7 +1533,7 @@ func testUpdateHostSoftware(t *testing.T, ds *Datastore) {
lastYear := now.Add(-365 * 24 * time.Hour)
// sort software slice by last opened at timestamp
genSortFn := func(sl []fleet.Software) func(l, r int) bool {
genSortFn := func(sl []fleet.HostSoftwareEntry) func(l, r int) bool {
return func(l, r int) bool {
lsw, rsw := sl[l], sl[r]
lts, rts := lsw.LastOpenedAt, rsw.LastOpenedAt
@ -2001,11 +2048,13 @@ func testAllSoftwareIterator(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
foo_ce_v1 := slices.IndexFunc(host.Software, func(c fleet.Software) bool {
foo_ce_v1 := slices.IndexFunc(host.Software, func(c fleet.HostSoftwareEntry) bool {
return c.Name == "foo" && c.Version == "0.0.1" && c.Source == "chrome_extensions"
})
foo_app_v2 := slices.IndexFunc(host.Software, func(c fleet.Software) bool { return c.Name == "foo" && c.Version == "v0.0.2" && c.Source == "apps" })
bar_v3 := slices.IndexFunc(host.Software, func(c fleet.Software) bool {
foo_app_v2 := slices.IndexFunc(host.Software, func(c fleet.HostSoftwareEntry) bool {
return c.Name == "foo" && c.Version == "v0.0.2" && c.Source == "apps"
})
bar_v3 := slices.IndexFunc(host.Software, func(c fleet.HostSoftwareEntry) bool {
return c.Name == "bar" && c.Version == "0.0.3" && c.Source == "deb_packages"
})

View File

@ -430,8 +430,13 @@ type Datastore interface {
// After aggregation, it cleans up unused software (e.g. software installed
// on removed hosts, software uninstalled on hosts, etc.)
SyncHostsSoftware(ctx context.Context, updatedAt time.Time) error
HostsBySoftwareIDs(ctx context.Context, softwareIDs []uint) ([]*HostShort, error)
HostsByCVE(ctx context.Context, cve string) ([]*HostShort, error)
// HostVulnSummariesBySoftwareIDs returns a list of all hosts that have at least one of the
// specified Software installed. Includes the path were the software was installed.
HostVulnSummariesBySoftwareIDs(ctx context.Context, softwareIDs []uint) ([]HostVulnerabilitySummary, error)
// *DEPRECATED use HostVulnSummariesBySoftwareIDs instead* HostsByCVE
// returns a list of all hosts that have at least one software suceptible to the provided CVE.
// Includes the path were the software was installed.
HostsByCVE(ctx context.Context, cve string) ([]HostVulnerabilitySummary, error)
InsertCVEMeta(ctx context.Context, cveMeta []CVEMeta) error
ListCVEs(ctx context.Context, maxAge time.Duration) ([]CVEMeta, error)

View File

@ -903,11 +903,23 @@ type AggregatedMacadminsData struct {
MDMSolutions []AggregatedMDMSolutions `json:"mobile_device_management_solution"`
}
// HostShort is a minimal host representation returned when querying hosts.
type HostShort struct {
ID uint `json:"id" db:"id"`
Hostname string `json:"hostname" db:"hostname"`
// HostVulnerabilitySummary type used with webhooks and third-party vulnerability automations.
// Contains all pertinent host info plus the installed paths of all affected software.
type HostVulnerabilitySummary struct {
// ID Is the ID of the host
ID uint `json:"id" db:"id"`
// Hostname the host's hostname
Hostname string `json:"hostname" db:"hostname"`
// DisplayName either the 'computer_name' or the 'host_name' (whatever is not empty)
DisplayName string `json:"display_name" db:"display_name"`
// SoftwareInstalledPaths paths of vulnerable software installed on the host.
SoftwareInstalledPaths []string `json:"software_installed_paths,omitempty" db:"software_installed_paths"`
}
func (hvs *HostVulnerabilitySummary) AddSoftwareInstalledPath(p string) {
if p != "" {
hvs.SoftwareInstalledPaths = append(hvs.SoftwareInstalledPaths, p)
}
}
type OSVersions struct {

View File

@ -43,7 +43,7 @@ type Software struct {
// GenerateCPE is the CPE23 string that corresponds to the current software
GenerateCPE string `json:"generated_cpe" db:"generated_cpe"`
// Vulnerabilities lists all the found CVEs for the CPE
// Vulnerabilities lists all found vulnerablities
Vulnerabilities Vulnerabilities `json:"vulnerabilities"`
// HostsCount indicates the number of hosts with that software, filled only
// if explicitly requested.
@ -83,10 +83,18 @@ func (s *AuthzSoftwareInventory) AuthzType() string {
return "software_inventory"
}
type HostSoftwareEntry struct {
// Software details
Software
// Where this software was installed on the host, value is derived from the
// host_software_installed_paths table.
InstalledPaths []string `json:"installed_paths,omitempty"`
}
// HostSoftware is the set of software installed on a specific host
type HostSoftware struct {
// Software is the software information.
Software []Software `json:"software,omitempty" csv:"-"`
Software []HostSoftwareEntry `json:"software,omitempty" csv:"-"`
// SoftwareUpdatedAt is the time that the host software was last updated
SoftwareUpdatedAt time.Time `json:"software_updated_at" db:"software_updated_at" csv:"software_updated_at"`
@ -141,6 +149,10 @@ type UpdateHostSoftwareDBResult struct {
func (uhsdbr *UpdateHostSoftwareDBResult) CurrInstalled() []Software {
var r []Software
if uhsdbr == nil {
return r
}
deleteMap := map[uint]struct{}{}
for _, d := range uhsdbr.Deleted {
deleteMap[d.ID] = struct{}{}

View File

@ -332,9 +332,9 @@ type ListSoftwareByHostIDShortFunc func(ctx context.Context, hostID uint) ([]fle
type SyncHostsSoftwareFunc func(ctx context.Context, updatedAt time.Time) error
type HostsBySoftwareIDsFunc func(ctx context.Context, softwareIDs []uint) ([]*fleet.HostShort, error)
type HostVulnSummariesBySoftwareIDsFunc func(ctx context.Context, softwareIDs []uint) ([]fleet.HostVulnerabilitySummary, error)
type HostsByCVEFunc func(ctx context.Context, cve string) ([]*fleet.HostShort, error)
type HostsByCVEFunc func(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error)
type InsertCVEMetaFunc func(ctx context.Context, cveMeta []fleet.CVEMeta) error
@ -1105,8 +1105,8 @@ type DataStore struct {
SyncHostsSoftwareFunc SyncHostsSoftwareFunc
SyncHostsSoftwareFuncInvoked bool
HostsBySoftwareIDsFunc HostsBySoftwareIDsFunc
HostsBySoftwareIDsFuncInvoked bool
HostVulnSummariesBySoftwareIDsFunc HostVulnSummariesBySoftwareIDsFunc
HostVulnSummariesBySoftwareIDsFuncInvoked bool
HostsByCVEFunc HostsByCVEFunc
HostsByCVEFuncInvoked bool
@ -2661,14 +2661,14 @@ func (s *DataStore) SyncHostsSoftware(ctx context.Context, updatedAt time.Time)
return s.SyncHostsSoftwareFunc(ctx, updatedAt)
}
func (s *DataStore) HostsBySoftwareIDs(ctx context.Context, softwareIDs []uint) ([]*fleet.HostShort, error) {
func (s *DataStore) HostVulnSummariesBySoftwareIDs(ctx context.Context, softwareIDs []uint) ([]fleet.HostVulnerabilitySummary, error) {
s.mu.Lock()
s.HostsBySoftwareIDsFuncInvoked = true
s.HostVulnSummariesBySoftwareIDsFuncInvoked = true
s.mu.Unlock()
return s.HostsBySoftwareIDsFunc(ctx, softwareIDs)
return s.HostVulnSummariesBySoftwareIDsFunc(ctx, softwareIDs)
}
func (s *DataStore) HostsByCVE(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
func (s *DataStore) HostsByCVE(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
s.mu.Lock()
s.HostsByCVEFuncInvoked = true
s.mu.Unlock()

View File

@ -4992,7 +4992,10 @@ func (s *integrationTestSuite) TestPaginateListSoftware() {
if i == 0 {
// this host has all software, refresh the list so we have the software.ID filled
sws = h.Software
sws = make([]fleet.Software, 0, len(h.Software))
for _, s := range h.Software {
sws = append(sws, s.Software)
}
}
}

View File

@ -177,8 +177,8 @@ func TestIntegrationAnalyzer(t *testing.T) {
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
var powerpoint fleet.Software
var word fleet.Software
var powerpoint fleet.HostSoftwareEntry
var word fleet.HostSoftwareEntry
for _, s := range host.HostSoftware.Software {
if s.Name == "Microsoft PowerPoint.app" {
powerpoint = s

View File

@ -13,14 +13,15 @@ import (
// 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, string, fleet.CVEMeta) WebhookPayload
GetPayload(*url.URL, []fleet.HostVulnerabilitySummary, string, fleet.CVEMeta) WebhookPayload
}
type hostPayloadPart struct {
ID uint `json:"id"`
Hostname string `json:"hostname"`
DisplayName string `json:"display_name"`
URL string `json:"url"`
ID uint `json:"id"`
Hostname string `json:"hostname"`
DisplayName string `json:"display_name"`
URL string `json:"url"`
SoftwareInstalledPaths []string `json:"software_installed_paths,omitempty"`
}
type WebhookPayload struct {
@ -42,25 +43,35 @@ func NewMapper() VulnMapper {
func (m *Mapper) getHostPayloadPart(
hostBaseURL *url.URL,
hosts []*fleet.HostShort,
hosts []fleet.HostVulnerabilitySummary,
) []*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{
hostPayload := hostPayloadPart{
ID: h.ID,
Hostname: h.Hostname,
DisplayName: h.DisplayName,
URL: hostURL.String(),
}
for _, p := range h.SoftwareInstalledPaths {
if p != "" {
hostPayload.SoftwareInstalledPaths = append(
hostPayload.SoftwareInstalledPaths,
p,
)
}
}
shortHosts[i] = &hostPayload
}
return shortHosts
}
func (m *Mapper) GetPayload(
hostBaseURL *url.URL,
hosts []*fleet.HostShort,
hosts []fleet.HostVulnerabilitySummary,
cve string,
meta fleet.CVEMeta,
) WebhookPayload {

View File

@ -29,9 +29,48 @@ func TestGetPaylaod(t *testing.T) {
sut := Mapper{}
result := sut.GetPayload(serverURL, nil, vuln.CVE, meta)
require.Empty(t, result.CISAKnownExploit)
require.Empty(t, result.EPSSProbability)
require.Empty(t, result.CVSSScore)
require.Empty(t, result.CVEPublished)
t.Run("does not include EE features", func(t *testing.T) {
result := sut.GetPayload(serverURL, nil, vuln.CVE, meta)
require.Empty(t, result.CISAKnownExploit)
require.Empty(t, result.EPSSProbability)
require.Empty(t, result.CVSSScore)
require.Empty(t, result.CVEPublished)
})
t.Run("host payload only includes valid software paths", func(t *testing.T) {
hosts := []fleet.HostVulnerabilitySummary{
{
ID: 1,
Hostname: "host1",
DisplayName: "d-host1",
SoftwareInstalledPaths: []string{
"",
"/some/path",
},
},
{
ID: 2,
Hostname: "host2",
DisplayName: "d-host2",
SoftwareInstalledPaths: nil,
},
}
result := sut.GetPayload(serverURL, hosts, vuln.CVE, meta)
require.ElementsMatch(t, result.Hosts, []*hostPayloadPart{
{
ID: uint(1),
Hostname: "host1",
DisplayName: "d-host1",
URL: "http://mywebsite.com/hosts/1",
SoftwareInstalledPaths: []string{"/some/path"},
},
{
ID: uint(2),
Hostname: "host2",
DisplayName: "d-host2",
URL: "http://mywebsite.com/hosts/2",
},
},
)
})
}

View File

@ -36,14 +36,13 @@ func TriggerVulnerabilitiesWebhook(
targetURL := vulnConfig.DestinationURL
batchSize := vulnConfig.HostBatchSize
// TODO JUAN: Handle OS Vulns
groups := make(map[string][]uint)
cveGrouped := make(map[string][]uint)
for _, v := range args.Vulnerablities {
groups[v.GetCVE()] = append(groups[v.GetCVE()], v.Affected())
cveGrouped[v.GetCVE()] = append(cveGrouped[v.GetCVE()], v.Affected())
}
for cve, sIDs := range groups {
hosts, err := ds.HostsBySoftwareIDs(ctx, sIDs)
for cve, sIDs := range cveGrouped {
hosts, err := ds.HostVulnSummariesBySoftwareIDs(ctx, sIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "get hosts by software ids")
}

View File

@ -81,7 +81,7 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) {
t.Run("trigger requests", func(t *testing.T) {
now := time.Now()
hosts := []*fleet.HostShort{
hosts := []fleet.HostVulnerabilitySummary{
{ID: 1, Hostname: "h1", DisplayName: "d1"},
{ID: 2, Hostname: "h2", DisplayName: "d2"},
{ID: 3, Hostname: "h3", DisplayName: "d3"},
@ -105,7 +105,7 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) {
name string
vulns []fleet.SoftwareVulnerability
meta map[string]fleet.CVEMeta
hosts []*fleet.HostShort
hosts []fleet.HostVulnerabilitySummary
want string
}{
{
@ -178,7 +178,7 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) {
}))
defer srv.Close()
ds.HostsBySoftwareIDsFunc = func(ctx context.Context, softwareIDs []uint) ([]*fleet.HostShort, error) {
ds.HostVulnSummariesBySoftwareIDsFunc = func(ctx context.Context, softwareIDs []uint) ([]fleet.HostVulnerabilitySummary, error) {
return c.hosts, nil
}
@ -194,8 +194,8 @@ func TestTriggerVulnerabilitiesWebhook(t *testing.T) {
err := TriggerVulnerabilitiesWebhook(ctx, ds, logger, args, &mapper)
require.NoError(t, err)
assert.True(t, ds.HostsBySoftwareIDsFuncInvoked)
ds.HostsBySoftwareIDsFuncInvoked = false
assert.True(t, ds.HostVulnSummariesBySoftwareIDsFuncInvoked)
ds.HostVulnSummariesBySoftwareIDsFuncInvoked = false
want := strings.Split(c.want, "\n")
assert.ElementsMatch(t, want, requests)

View File

@ -18,8 +18,8 @@ import (
func TestJiraFailer(t *testing.T) {
ds := new(mock.Store)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
return []*fleet.HostShort{{ID: 1, Hostname: "test"}}, nil
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
return []fleet.HostVulnerabilitySummary{{ID: 1, Hostname: "test"}}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{Integrations: fleet.Integrations{
@ -73,7 +73,7 @@ func TestJiraFailer(t *testing.T) {
cves := []string{"CVE-2018-1234", "CVE-2019-1234", "CVE-2020-1234", "CVE-2021-1234"}
for i := 0; i < 10; i++ {
cve := cves[i%len(cves)]
err := jira.Run(license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierFree}), json.RawMessage(fmt.Sprintf(`{"cve":%q}`, cve)))
err := jira.Run(license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierFree}), json.RawMessage(fmt.Sprintf(`{"vulnerability":{"cve":%q}}`, cve)))
if err != nil {
failedIndices = append(failedIndices, i)
}
@ -89,8 +89,8 @@ func TestJiraFailer(t *testing.T) {
func TestZendeskFailer(t *testing.T) {
ds := new(mock.Store)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
return []*fleet.HostShort{{ID: 1, Hostname: "test"}}, nil
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
return []fleet.HostVulnerabilitySummary{{ID: 1, Hostname: "test"}}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{Integrations: fleet.Integrations{
@ -132,7 +132,7 @@ func TestZendeskFailer(t *testing.T) {
cves := []string{"CVE-2018-1234", "CVE-2019-1234", "CVE-2020-1234", "CVE-2021-1234"}
for i := 0; i < 10; i++ {
cve := cves[i%len(cves)]
err := zendesk.Run(license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierFree}), json.RawMessage(fmt.Sprintf(`{"cve":%q}`, cve)))
err := zendesk.Run(license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierFree}), json.RawMessage(fmt.Sprintf(`{"vulnerability":{"cve":%q}}`, cve)))
if err != nil {
failedIndices = append(failedIndices, i)
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"sync"
@ -58,6 +59,9 @@ Affected hosts:
{{ $end := len .Hosts }}{{ if gt $end 50 }}{{ $end = 50 }}{{ end }}
{{ range slice .Hosts 0 $end }}
* [{{ .DisplayName }}|{{ $.FleetURL }}/hosts/{{ .ID }}]
{{ range $path := .SoftwareInstalledPaths }}
** {{ $path }}
{{ end }}
{{ end }}
View the affected software and more affected hosts:
@ -96,7 +100,7 @@ type jiraVulnTplArgs struct {
NVDURL string
FleetURL string
CVE string
Hosts []*fleet.HostShort
Hosts []fleet.HostVulnerabilitySummary
IsPremium bool
@ -220,9 +224,6 @@ func (j *Jira) getClient(ctx context.Context, args jiraArgs) (JiraClient, error)
// jiraArgs are the arguments for the Jira integration job.
type jiraArgs struct {
// CVE is deprecated but kept for backwards compatibility (there may be jobs
// enqueued in that format to process).
CVE string `json:"cve,omitempty"`
Vulnerability *vulnArgs `json:"vulnerability,omitempty"`
FailingPolicy *failingPolicyArgs `json:"failing_policy,omitempty"`
}
@ -265,15 +266,22 @@ func (j *Jira) Run(ctx context.Context, argsJSON json.RawMessage) error {
func (j *Jira) runVuln(ctx context.Context, cli JiraClient, args jiraArgs) error {
vargs := args.Vulnerability
if vargs == nil {
// support the old format of vulnerability args, where only the CVE
// is provided.
vargs = &vulnArgs{
CVE: args.CVE,
}
return errors.New("invalid job args")
}
var hosts []fleet.HostVulnerabilitySummary
var err error
// Default to deprecated method in case we are processing an 'old' job payload
// we are deprecating this because of performance reasons - querying by software_id should be
// way more efficient than by CVE.
if len(vargs.AffectedSoftwareIDs) == 0 {
hosts, err = j.Datastore.HostsByCVE(ctx, vargs.CVE)
} else {
hosts, err = j.Datastore.HostVulnSummariesBySoftwareIDs(ctx, vargs.AffectedSoftwareIDs)
}
hosts, err := j.Datastore.HostsByCVE(ctx, vargs.CVE)
if err != nil {
return ctxerr.Wrap(ctx, err, "find hosts by cve")
return ctxerr.Wrap(ctx, err, "fetching hosts")
}
tplArgs := &jiraVulnTplArgs{
@ -374,13 +382,13 @@ func QueueJiraVulnJobs(
sort.Strings(cves)
level.Debug(logger).Log("recent_cves", fmt.Sprintf("%v", cves))
uniqCVEs := make(map[string]bool)
cveGrouped := make(map[string][]uint)
for _, v := range recentVulns {
uniqCVEs[v.GetCVE()] = true
cveGrouped[v.GetCVE()] = append(cveGrouped[v.GetCVE()], v.Affected())
}
for cve := range uniqCVEs {
args := vulnArgs{CVE: cve}
for cve, sIDs := range cveGrouped {
args := vulnArgs{CVE: cve, AffectedSoftwareIDs: sIDs}
if meta, ok := cveMeta[cve]; ok {
args.EPSSProbability = meta.EPSSProbability
args.CVSSScore = meta.CVSSScore

View File

@ -23,11 +23,15 @@ import (
func TestJiraRun(t *testing.T) {
ds := new(mock.Store)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
return []*fleet.HostShort{
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
return []fleet.HostVulnerabilitySummary{
{
ID: 1,
Hostname: "test",
SoftwareInstalledPaths: []string{
"/some/path/1",
"/some/path/2",
},
},
}, nil
}
@ -54,7 +58,8 @@ func TestJiraRun(t *testing.T) {
}, nil
}
var expectedSummary, expectedDescription, expectedNotInDescription string
var expectedSummary, expectedNotInDescription string
var expectedDescription []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(501)
@ -71,8 +76,10 @@ func TestJiraRun(t *testing.T) {
if expectedSummary != "" {
require.Contains(t, string(body), expectedSummary)
}
if expectedDescription != "" {
require.Contains(t, string(body), expectedDescription)
if len(expectedDescription) != 0 {
for _, s := range expectedDescription {
require.Contains(t, string(body), s)
}
}
if expectedNotInDescription != "" {
fmt.Println(string(body))
@ -105,23 +112,20 @@ func TestJiraRun(t *testing.T) {
licenseTier string
payload string
expectedSummary string
expectedDescription string
expectedDescription []string
expectedNotInDescription string
}{
{
"old vuln format free",
fleet.TierFree,
`{"cve":"CVE-1234-5678"}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
"Probability of exploit",
},
{
"vuln free",
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
[]string{
"Affected hosts:",
"https://fleetdm.com/hosts/1",
"** /some/path/1",
"** /some/path/2",
},
"Probability of exploit",
},
{
@ -129,7 +133,12 @@ func TestJiraRun(t *testing.T) {
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
[]string{
"Affected hosts:",
"https://fleetdm.com/hosts/1",
"** /some/path/1",
"** /some/path/2",
},
"Probability of exploit",
},
{
@ -137,7 +146,7 @@ func TestJiraRun(t *testing.T) {
fleet.TierFree,
`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": []}}`,
`"summary":"test-policy policy failed on 0 host(s)"`,
"\\u0026policy_id=1\\u0026policy_response=failing",
[]string{"\\u0026policy_id=1\\u0026policy_response=failing"},
"\\u0026team_id=",
},
{
@ -145,23 +154,20 @@ func TestJiraRun(t *testing.T) {
fleet.TierPremium,
`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": [{"id": 1, "hostname": "test-1"}, {"id": 2, "hostname": "test-2"}]}}`,
`"summary":"test-policy-2 policy failed on 2 host(s)"`,
"\\u0026team_id=123\\u0026policy_id=2\\u0026policy_response=failing",
[]string{"\\u0026team_id=123\\u0026policy_id=2\\u0026policy_response=failing"},
"",
},
{
"old vuln format premium",
fleet.TierPremium,
`{"cve":"CVE-1234-5678"}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
"Probability of exploit",
},
{
"vuln premium",
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Affected hosts:",
[]string{
"Affected hosts:",
"https://fleetdm.com/hosts/1",
"** /some/path/1",
"** /some/path/2",
},
"Probability of exploit",
},
{
@ -169,7 +175,12 @@ func TestJiraRun(t *testing.T) {
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Probability of exploit",
[]string{
"Affected hosts:",
"https://fleetdm.com/hosts/1",
"** /some/path/1",
"** /some/path/2",
},
"",
},
{
@ -177,7 +188,13 @@ func TestJiraRun(t *testing.T) {
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678","cve_published":"2012-04-23T18:25:43.511Z","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"summary":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Published (reported by [NVD|https://nvd.nist.gov/]): 2012-04-23",
[]string{
"Affected hosts:",
"https://fleetdm.com/hosts/1",
"** /some/path/1",
"** /some/path/2",
"Published (reported by [NVD|https://nvd.nist.gov/]): 2012-04-23",
},
"",
},
}

View File

@ -48,11 +48,12 @@ type failingPolicyArgs struct {
// vulnArgs are the args common to all integrations that can process
// vulnerabilities.
type vulnArgs struct {
CVE string `json:"cve,omitempty"`
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
CVEPublished *time.Time `json:"cve_published,omitempty"` // Premium feature only
CVE string `json:"cve,omitempty"`
AffectedSoftwareIDs []uint `json:"affected_software,omitempty"`
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
CVEPublished *time.Time `json:"cve_published,omitempty"` // Premium feature only
}
// Worker runs jobs. NOT SAFE FOR CONCURRENT USE.

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"sync"
@ -59,6 +60,9 @@ Affected hosts:
{{ $end := len .Hosts }}{{ if gt $end 50 }}{{ $end = 50 }}{{ end }}
{{ range slice .Hosts 0 $end }}
* [{{ .DisplayName }}]({{ $.FleetURL }}/hosts/{{ .ID }})
{{ range $path := .SoftwareInstalledPaths }}
* {{ $path }}
{{ end }}
{{ end }}
View the affected software and more affected hosts:
@ -97,7 +101,7 @@ type zendeskVulnTplArgs struct {
NVDURL string
FleetURL string
CVE string
Hosts []*fleet.HostShort
Hosts []fleet.HostVulnerabilitySummary
IsPremium bool
@ -222,9 +226,6 @@ func (z *Zendesk) Name() string {
// zendeskArgs are the arguments for the Zendesk integration job.
type zendeskArgs struct {
// CVE is deprecated but kept for backwards compatibility (there may be jobs
// enqueued in that format to process).
CVE string `json:"cve,omitempty"`
Vulnerability *vulnArgs `json:"vulnerability,omitempty"`
FailingPolicy *failingPolicyArgs `json:"failing_policy,omitempty"`
}
@ -267,15 +268,23 @@ func (z *Zendesk) Run(ctx context.Context, argsJSON json.RawMessage) error {
func (z *Zendesk) runVuln(ctx context.Context, cli ZendeskClient, args zendeskArgs) error {
vargs := args.Vulnerability
if vargs == nil {
// support the old format of vulnerability args, where only the CVE
// is provided.
vargs = &vulnArgs{
CVE: args.CVE,
}
return errors.New("invalid job args")
}
hosts, err := z.Datastore.HostsByCVE(ctx, vargs.CVE)
var hosts []fleet.HostVulnerabilitySummary
var err error
// Default to deprecated method in case we are processing an 'old' job payload
// we are deprecating this because of performance reasons - querying by software_id should be
// way more efficient than by CVE.
if len(vargs.AffectedSoftwareIDs) == 0 {
hosts, err = z.Datastore.HostsByCVE(ctx, vargs.CVE)
} else {
hosts, err = z.Datastore.HostVulnSummariesBySoftwareIDs(ctx, vargs.AffectedSoftwareIDs)
}
if err != nil {
return ctxerr.Wrap(ctx, err, "find hosts by cve")
return ctxerr.Wrap(ctx, err, "fetching hosts")
}
tplArgs := &zendeskVulnTplArgs{
@ -369,13 +378,13 @@ func QueueZendeskVulnJobs(
sort.Strings(cves)
level.Debug(logger).Log("recent_cves", fmt.Sprintf("%v", cves))
uniqCVEs := make(map[string]bool)
cveGrouped := make(map[string][]uint)
for _, v := range recentVulns {
uniqCVEs[v.GetCVE()] = true
cveGrouped[v.GetCVE()] = append(cveGrouped[v.GetCVE()], v.Affected())
}
for cve := range uniqCVEs {
args := vulnArgs{CVE: cve}
for cve, sIDs := range cveGrouped {
args := vulnArgs{CVE: cve, AffectedSoftwareIDs: sIDs}
if meta, ok := cveMeta[cve]; ok {
args.EPSSProbability = meta.EPSSProbability
args.CVSSScore = meta.CVSSScore

View File

@ -22,11 +22,15 @@ import (
func TestZendeskRun(t *testing.T) {
ds := new(mock.Store)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
return []*fleet.HostShort{
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
return []fleet.HostVulnerabilitySummary{
{
ID: 1,
Hostname: "test",
SoftwareInstalledPaths: []string{
"/some/path/1",
"/some/path/2",
},
},
}, nil
}
@ -53,7 +57,8 @@ func TestZendeskRun(t *testing.T) {
}, nil
}
var expectedSubject, expectedDescription, expectedNotInDescription string
var expectedSubject, expectedNotInDescription string
var expectedDescription []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(501)
@ -69,8 +74,10 @@ func TestZendeskRun(t *testing.T) {
if expectedSubject != "" {
require.Contains(t, string(body), expectedSubject)
}
if expectedDescription != "" {
require.Contains(t, string(body), expectedDescription)
if len(expectedDescription) != 0 {
for _, s := range expectedDescription {
require.Contains(t, string(body), s)
}
}
if expectedNotInDescription != "" {
require.NotContains(t, string(body), expectedNotInDescription)
@ -90,23 +97,19 @@ func TestZendeskRun(t *testing.T) {
licenseTier string
payload string
expectedSubject string
expectedDescription string
expectedDescription []string
expectedNotInDescription string
}{
{
"old vuln format free",
fleet.TierFree,
`{"cve":"CVE-1234-5678"}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
"Probability of exploit",
},
{
"vuln free",
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
[]string{
`"group_id":123`,
"/some/path/1",
"/some/path/2",
},
"Probability of exploit",
},
{
@ -114,7 +117,11 @@ func TestZendeskRun(t *testing.T) {
fleet.TierFree,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
[]string{
`"group_id":123`,
"/some/path/1",
"/some/path/2",
},
"Probability of exploit",
},
{
@ -122,7 +129,7 @@ func TestZendeskRun(t *testing.T) {
fleet.TierFree,
`{"failing_policy":{"policy_id": 1, "policy_name": "test-policy", "hosts": [{"id": 123, "hostname": "host-123"}]}}`,
`"subject":"test-policy policy failed on 1 host(s)"`,
"\\u0026policy_id=1\\u0026policy_response=failing",
[]string{"\\u0026policy_id=1\\u0026policy_response=failing"},
"\\u0026team_id=",
},
{
@ -130,23 +137,19 @@ func TestZendeskRun(t *testing.T) {
fleet.TierPremium,
`{"failing_policy":{"policy_id": 2, "policy_name": "test-policy-2", "team_id": 123, "hosts": [{"id": 1, "hostname": "host-1"}, {"id": 2, "hostname": "host-2"}]}}`,
`"subject":"test-policy-2 policy failed on 2 host(s)"`,
"\\u0026team_id=123\\u0026policy_id=2\\u0026policy_response=failing",
[]string{"\\u0026team_id=123\\u0026policy_id=2\\u0026policy_response=failing"},
"",
},
{
"old vuln format premium",
fleet.TierPremium,
`{"cve":"CVE-1234-5678"}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
"Probability of exploit",
},
{
"vuln premium",
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678"}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
`"group_id":123`,
[]string{
`"group_id":123`,
"/some/path/1",
"/some/path/2",
},
"Probability of exploit",
},
{
@ -154,7 +157,11 @@ func TestZendeskRun(t *testing.T) {
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Probability of exploit",
[]string{
"Probability of exploit",
"/some/path/1",
"/some/path/2",
},
"",
},
{
@ -162,7 +169,11 @@ func TestZendeskRun(t *testing.T) {
fleet.TierPremium,
`{"vulnerability":{"cve":"CVE-1234-5678","cve_published":"2012-04-23T18:25:43.511Z","epss_probability":3.4,"cvss_score":50,"cisa_known_exploit":true}}`,
`"subject":"Vulnerability CVE-1234-5678 detected on 1 host(s)"`,
"Published (reported by [NVD|https://nvd.nist.gov/]): 2012-04-23",
[]string{
"Published (reported by [NVD|https://nvd.nist.gov/]): 2012-04-23",
"/some/path/1",
"/some/path/2",
},
"",
},
}

View File

@ -80,10 +80,10 @@ func main() {
logger := kitlog.NewLogfmtLogger(os.Stdout)
ds := new(mock.Store)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
hosts := make([]*fleet.HostShort, *hostsCount)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
hosts := make([]fleet.HostVulnerabilitySummary, *hostsCount)
for i := 0; i < *hostsCount; i++ {
hosts[i] = &fleet.HostShort{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1), DisplayName: fmt.Sprintf("host-test-%d", i+1)}
hosts[i] = fleet.HostVulnerabilitySummary{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1), DisplayName: fmt.Sprintf("host-test-%d", i+1)}
}
return hosts, nil
}

View File

@ -80,10 +80,10 @@ func main() {
logger := kitlog.NewLogfmtLogger(os.Stdout)
ds := new(mock.Store)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]*fleet.HostShort, error) {
hosts := make([]*fleet.HostShort, *hostsCount)
ds.HostsByCVEFunc = func(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
hosts := make([]fleet.HostVulnerabilitySummary, *hostsCount)
for i := 0; i < *hostsCount; i++ {
hosts[i] = &fleet.HostShort{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1), DisplayName: fmt.Sprintf("host-test-%d", i+1)}
hosts[i] = fleet.HostVulnerabilitySummary{ID: uint(i + 1), Hostname: fmt.Sprintf("host-test-%d", i+1), DisplayName: fmt.Sprintf("host-test-%d", i+1)}
}
return hosts, nil
}