mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
> Related issue: #20229 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality
3968 lines
142 KiB
Go
3968 lines
142 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd"
|
|
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
func TestSoftware(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, ds *Datastore)
|
|
}{
|
|
{"SaveHost", testSoftwareSaveHost},
|
|
{"CPE", testSoftwareCPE},
|
|
{"HostDuplicates", testSoftwareHostDuplicates},
|
|
{"LoadVulnerabilities", testSoftwareLoadVulnerabilities},
|
|
{"ListSoftwareCPEs", testListSoftwareCPEs},
|
|
{"NothingChanged", testSoftwareNothingChanged},
|
|
{"LoadSupportsTonsOfCVEs", testSoftwareLoadSupportsTonsOfCVEs},
|
|
{"List", testSoftwareList},
|
|
{"SyncHostsSoftware", testSoftwareSyncHostsSoftware},
|
|
{"DeleteSoftwareVulnerabilities", testDeleteSoftwareVulnerabilities},
|
|
{"HostsByCVE", testHostsByCVE},
|
|
{"HostVulnSummariesBySoftwareIDs", testHostVulnSummariesBySoftwareIDs},
|
|
{"UpdateHostSoftware", testUpdateHostSoftware},
|
|
{"UpdateHostSoftwareDeadlock", testUpdateHostSoftwareDeadlock},
|
|
{"UpdateHostSoftwareUpdatesSoftware", testUpdateHostSoftwareUpdatesSoftware},
|
|
{"ListSoftwareByHostIDShort", testListSoftwareByHostIDShort},
|
|
{"ListSoftwareVulnerabilitiesByHostIDsSource", testListSoftwareVulnerabilitiesByHostIDsSource},
|
|
{"InsertSoftwareVulnerability", testInsertSoftwareVulnerability},
|
|
{"ListCVEs", testListCVEs},
|
|
{"ListSoftwareForVulnDetection", testListSoftwareForVulnDetection},
|
|
{"AllSoftwareIterator", testAllSoftwareIterator},
|
|
{"AllSoftwareIteratorForCustomLinuxImages", testSoftwareIteratorForLinuxKernelCustomImages},
|
|
{"UpsertSoftwareCPEs", testUpsertSoftwareCPEs},
|
|
{"DeleteOutOfDateVulnerabilities", testDeleteOutOfDateVulnerabilities},
|
|
{"DeleteSoftwareCPEs", testDeleteSoftwareCPEs},
|
|
{"SoftwareByIDNoDuplicatedVulns", testSoftwareByIDNoDuplicatedVulns},
|
|
{"SoftwareByIDIncludesCVEPublishedDate", testSoftwareByIDIncludesCVEPublishedDate},
|
|
{"getHostSoftwareInstalledPaths", testGetHostSoftwareInstalledPaths},
|
|
{"hostSoftwareInstalledPathsDelta", testHostSoftwareInstalledPathsDelta},
|
|
{"deleteHostSoftwareInstalledPaths", testDeleteHostSoftwareInstalledPaths},
|
|
{"insertHostSoftwareInstalledPaths", testInsertHostSoftwareInstalledPaths},
|
|
{"VerifySoftwareChecksum", testVerifySoftwareChecksum},
|
|
{"ListHostSoftware", testListHostSoftware},
|
|
{"SetHostSoftwareInstallResult", testSetHostSoftwareInstallResult},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer TruncateTables(t, ds)
|
|
c.fn(t, ds)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSoftwareSaveHost(t *testing.T, ds *Datastore) {
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
|
|
|
software1 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
}
|
|
software2 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages", BundleIdentifier: "com.some.identifier"},
|
|
{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))
|
|
host1Software := getHostSoftware(host1)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software1, host1Software)
|
|
|
|
soft1ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, nil, false, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, soft1ByID)
|
|
assert.Equal(t, host1Software[0], *soft1ByID)
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
|
|
host2Software := getHostSoftware(host2)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
|
|
|
|
software1 = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "towel", Version: "42.0.0", Source: "apps"},
|
|
}
|
|
software2 = []fleet.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))
|
|
host1Software = getHostSoftware(host1)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software1, host1Software)
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
|
|
host2Software = getHostSoftware(host2)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
|
|
|
|
software1 = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "towel", Version: "42.0.0", Source: "apps"},
|
|
}
|
|
|
|
_, err = ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
|
|
host1Software = getHostSoftware(host1)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software1, host1Software)
|
|
|
|
software2 = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages", BundleIdentifier: "com.some.identifier"},
|
|
{Name: "zoo", Version: "0.0.5", Source: "deb_packages", BundleIdentifier: "com.zoo"}, // "empty" -> "non-empty"
|
|
}
|
|
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
|
|
host2Software = getHostSoftware(host2)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
|
|
|
|
software2 = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages", BundleIdentifier: "com.some.other"}, // "non-empty" -> "non-empty"
|
|
{Name: "zoo", Version: "0.0.5", Source: "deb_packages", BundleIdentifier: ""}, // non-empty -> empty
|
|
}
|
|
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
|
|
host2Software = getHostSoftware(host2)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software2, host2Software)
|
|
}
|
|
|
|
func testSoftwareCPE(t *testing.T, ds *Datastore) {
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software1 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
}
|
|
|
|
software2 := []fleet.Software{
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages", BundleIdentifier: "com.some.other"}, // "non-empty" -> "non-empty"
|
|
{Name: "zoo", Version: "0.0.5", Source: "rpm_packages", BundleIdentifier: ""}, // non-empty -> empty
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(context.Background(), host1.ID, append(software1, software2...))
|
|
require.NoError(t, err)
|
|
|
|
q := fleet.SoftwareIterQueryOptions{ExcludedSources: oval.SupportedSoftwareSources}
|
|
iterator, err := ds.AllSoftwareIterator(context.Background(), q)
|
|
require.NoError(t, err)
|
|
defer iterator.Close()
|
|
|
|
loops := 0
|
|
for iterator.Next() {
|
|
software, err := iterator.Value()
|
|
require.NoError(t, err)
|
|
require.NoError(t, iterator.Err())
|
|
|
|
require.NotEmpty(t, software.ID)
|
|
require.NotEmpty(t, software.Name)
|
|
require.NotEmpty(t, software.Version)
|
|
require.NotEmpty(t, software.Source)
|
|
|
|
require.NotEqual(t, software.Name, "bar")
|
|
require.NotEqual(t, software.Name, "zoo")
|
|
|
|
if loops > 2 {
|
|
t.Error("Looping through more software than we have")
|
|
}
|
|
loops++
|
|
}
|
|
assert.Equal(t, len(software1), loops)
|
|
require.NoError(t, iterator.Close())
|
|
}
|
|
|
|
func testSoftwareHostDuplicates(t *testing.T, ds *Datastore) {
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
longName := strings.Repeat("a", fleet.SoftwareNameMaxLength+5)
|
|
|
|
incoming := make(map[string]fleet.Software)
|
|
sw, err := fleet.SoftwareFromOsqueryRow(longName+"b", "0.0.1", "chrome_extension", "", "", "", "", "", "", "", "")
|
|
require.NoError(t, err)
|
|
soft2Key := sw.ToUniqueStr()
|
|
incoming[soft2Key] = *sw
|
|
|
|
incomingByChecksum, existingSoftware, existingTitlesForNewSoftware, err := ds.getExistingSoftware(
|
|
context.Background(), make(map[string]fleet.Software), incoming,
|
|
)
|
|
require.NoError(t, err)
|
|
tx, err := ds.writer(context.Background()).Beginx()
|
|
require.NoError(t, err)
|
|
_, err = ds.insertNewInstalledHostSoftwareDB(
|
|
context.Background(), tx, host1.ID, incomingByChecksum, existingSoftware, existingTitlesForNewSoftware,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tx.Commit())
|
|
|
|
// Check that the software entry was stored for the host.
|
|
var software []fleet.Software
|
|
err = sqlx.SelectContext(context.Background(), ds.reader(context.Background()),
|
|
&software, `SELECT s.id, s.name FROM software s JOIN host_software hs WHERE hs.host_id = ?`,
|
|
host1.ID,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, software, 1)
|
|
require.NotZero(t, software[0].ID)
|
|
require.Equal(t, strings.Repeat("a", fleet.SoftwareNameMaxLength), software[0].Name)
|
|
|
|
incoming = make(map[string]fleet.Software)
|
|
sw, err = fleet.SoftwareFromOsqueryRow(longName+"c", "0.0.1", "chrome_extension", "", "", "", "", "", "", "", "")
|
|
require.NoError(t, err)
|
|
soft3Key := sw.ToUniqueStr()
|
|
incoming[soft3Key] = *sw
|
|
|
|
incomingByChecksum, existingSoftware, existingTitlesForNewSoftware, err = ds.getExistingSoftware(
|
|
context.Background(), make(map[string]fleet.Software), incoming,
|
|
)
|
|
require.NoError(t, err)
|
|
tx, err = ds.writer(context.Background()).Beginx()
|
|
require.NoError(t, err)
|
|
_, err = ds.insertNewInstalledHostSoftwareDB(
|
|
context.Background(), tx, host1.ID, incomingByChecksum, existingSoftware, existingTitlesForNewSoftware,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tx.Commit())
|
|
|
|
// Check that the software entry was not modified with the new insert because of the name trimming.
|
|
var software2 []fleet.Software
|
|
err = sqlx.SelectContext(context.Background(), ds.reader(context.Background()),
|
|
&software2, `SELECT s.id, s.name FROM software s JOIN host_software hs WHERE hs.host_id = ?`,
|
|
host1.ID,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, software2, 1)
|
|
require.Equal(t, strings.Repeat("a", fleet.SoftwareNameMaxLength), software2[0].Name)
|
|
require.Equal(t, software[0].ID, software2[0].ID)
|
|
}
|
|
|
|
func testSoftwareLoadVulnerabilities(t *testing.T, ds *Datastore) {
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "apps"},
|
|
{Name: "blah", Version: "1.0", Source: "apps"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(context.Background(), host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "somecpe"},
|
|
{SoftwareID: host.Software[1].ID, CPE: "someothercpewithoutvulns"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(context.Background(), cpes)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
|
|
|
|
vulns := []fleet.SoftwareVulnerability{
|
|
{SoftwareID: host.Software[0].ID, CVE: "CVE-2022-0001"},
|
|
{SoftwareID: host.Software[0].ID, CVE: "CVE-2022-0002"},
|
|
}
|
|
for _, v := range vulns {
|
|
_, err = ds.InsertSoftwareVulnerability(context.Background(), v, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
}
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
|
|
|
|
softByID, err := ds.SoftwareByID(context.Background(), host.HostSoftware.Software[0].ID, nil, false, nil)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, softByID)
|
|
require.Len(t, softByID.Vulnerabilities, 2)
|
|
assert.GreaterOrEqual(t, softByID.Vulnerabilities[0].CreatedAt, time.Now().Add(-time.Minute))
|
|
|
|
assert.Equal(t, "somecpe", host.Software[0].GenerateCPE)
|
|
require.Len(t, host.Software[0].Vulnerabilities, 2)
|
|
|
|
sort.Slice(host.Software[0].Vulnerabilities, func(i, j int) bool {
|
|
return host.Software[0].Vulnerabilities[i].CVE < host.Software[0].Vulnerabilities[j].CVE
|
|
})
|
|
|
|
assert.Equal(t, "CVE-2022-0001", host.Software[0].Vulnerabilities[0].CVE)
|
|
assert.Equal(t,
|
|
"https://nvd.nist.gov/vuln/detail/CVE-2022-0001", host.Software[0].Vulnerabilities[0].DetailsLink)
|
|
assert.Equal(t, "CVE-2022-0002", host.Software[0].Vulnerabilities[1].CVE)
|
|
assert.Equal(t,
|
|
"https://nvd.nist.gov/vuln/detail/CVE-2022-0002", host.Software[0].Vulnerabilities[1].DetailsLink)
|
|
assert.Equal(t, "someothercpewithoutvulns", host.Software[1].GenerateCPE)
|
|
require.Len(t, host.Software[1].Vulnerabilities, 0)
|
|
}
|
|
|
|
func testListSoftwareCPEs(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
debian := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
|
debian.Platform = "debian"
|
|
require.NoError(t, ds.UpdateHost(ctx, debian))
|
|
|
|
ubuntu := test.NewHost(t, ds, "host4", "", "host4key", "host4uuid", time.Now())
|
|
ubuntu.Platform = "ubuntu"
|
|
require.NoError(t, ds.UpdateHost(ctx, ubuntu))
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "apps"},
|
|
{Name: "biz", Version: "0.0.1", Source: "deb_packages"},
|
|
{Name: "baz", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, debian.ID, software[:2])
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, debian, false))
|
|
|
|
_, err = ds.UpdateHostSoftware(ctx, ubuntu.ID, software[2:])
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, ubuntu, false))
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: debian.Software[0].ID, CPE: "cpe1"},
|
|
{SoftwareID: debian.Software[1].ID, CPE: "cpe2"},
|
|
{SoftwareID: ubuntu.Software[0].ID, CPE: "cpe3"},
|
|
{SoftwareID: ubuntu.Software[1].ID, CPE: "cpe4"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
cpes, err = ds.ListSoftwareCPEs(ctx)
|
|
expected := []string{
|
|
"cpe1", "cpe2", "cpe3", "cpe4",
|
|
}
|
|
var actual []string
|
|
for _, v := range cpes {
|
|
actual = append(actual, v.CPE)
|
|
}
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, actual, expected)
|
|
}
|
|
|
|
func testSoftwareNothingChanged(t *testing.T, ds *Datastore) {
|
|
cases := []struct {
|
|
desc string
|
|
current []fleet.Software
|
|
incoming []fleet.Software
|
|
want bool
|
|
}{
|
|
{"both nil", nil, nil, true},
|
|
{"different len", nil, []fleet.Software{{}}, false},
|
|
|
|
{
|
|
"identical",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
true,
|
|
},
|
|
{
|
|
"different version",
|
|
[]fleet.Software{{Name: "A", Version: "1.1", Source: "ASD"}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
false,
|
|
},
|
|
{
|
|
"new software",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}, {Name: "B", Version: "1.0", Source: "ASD"}},
|
|
false,
|
|
},
|
|
{
|
|
"removed software",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}, {Name: "B", Version: "1.0", Source: "ASD"}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
false,
|
|
},
|
|
{
|
|
"identical with similar last open",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now())}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now())}},
|
|
true,
|
|
},
|
|
{
|
|
"identical with no new last open",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now())}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
true,
|
|
},
|
|
{
|
|
"identical but added last open",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now())}},
|
|
false,
|
|
},
|
|
{
|
|
"identical but significantly changed last open",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now().Add(-365 * 24 * time.Hour))}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now())}},
|
|
false,
|
|
},
|
|
{
|
|
"identical but insignificantly changed last open",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now().Add(-time.Second))}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now())}},
|
|
true,
|
|
},
|
|
{
|
|
"identical with duplicates incoming",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}},
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD"}, {Name: "A", Version: "1.0", Source: "ASD"}},
|
|
true,
|
|
},
|
|
{
|
|
"identical with duplicates incoming and insignificantly changed last open",
|
|
[]fleet.Software{{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now().Add(-time.Second))}},
|
|
[]fleet.Software{
|
|
{Name: "A", Version: "1.0", Source: "ASD"},
|
|
{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now().Add(-time.Hour))},
|
|
{Name: "A", Version: "1.0", Source: "ASD", LastOpenedAt: ptr.Time(time.Now())},
|
|
},
|
|
true,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.desc, func(t *testing.T) {
|
|
current, incoming, got := nothingChanged(c.current, c.incoming, defaultMinLastOpenedAtDiff)
|
|
if c.want {
|
|
assert.True(t, got)
|
|
assert.Equal(t, len(current), len(incoming))
|
|
} else {
|
|
assert.False(t, got)
|
|
}
|
|
assert.Equal(t, len(c.current), len(current))
|
|
})
|
|
}
|
|
}
|
|
|
|
func generateCVEMeta(n int) fleet.CVEMeta {
|
|
CVEID := fmt.Sprintf("CVE-2022-%05d", n)
|
|
cvssScore := ptr.Float64(rand.Float64() * 10)
|
|
epssProbability := ptr.Float64(rand.Float64())
|
|
cisaKnownExploit := ptr.Bool(rand.Intn(2) == 1)
|
|
return fleet.CVEMeta{
|
|
CVE: CVEID,
|
|
CVSSScore: cvssScore,
|
|
EPSSProbability: epssProbability,
|
|
CISAKnownExploit: cisaKnownExploit,
|
|
}
|
|
}
|
|
|
|
func testSoftwareLoadSupportsTonsOfCVEs(t *testing.T, ds *Datastore) {
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "apps"},
|
|
{Name: "blah", Version: "1.0", Source: "apps"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(context.Background(), host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
|
|
|
|
sort.Slice(host.Software, func(i, j int) bool { return host.Software[i].Name < host.Software[j].Name })
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[1].ID, CPE: "someothercpewithoutvulns"},
|
|
{SoftwareID: host.Software[0].ID, CPE: "somecpe"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(context.Background(), cpes)
|
|
require.NoError(t, err)
|
|
|
|
var cveMeta []fleet.CVEMeta
|
|
for i := 0; i < 1000; i++ {
|
|
cveMeta = append(cveMeta, generateCVEMeta(i))
|
|
}
|
|
|
|
err = ds.InsertCVEMeta(context.Background(), cveMeta)
|
|
require.NoError(t, err)
|
|
|
|
values := strings.TrimSuffix(strings.Repeat("(?, ?), ", len(cveMeta)), ", ")
|
|
query := `INSERT INTO software_cve (software_id, cve) VALUES ` + values
|
|
var args []interface{}
|
|
for _, cve := range cveMeta {
|
|
args = append(args, host.Software[0].ID, cve.CVE)
|
|
}
|
|
_, err = ds.writer(context.Background()).ExecContext(context.Background(), query, args...)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
|
|
|
|
for _, software := range host.Software {
|
|
switch software.Name {
|
|
case "bar":
|
|
assert.Equal(t, "somecpe", software.GenerateCPE)
|
|
require.Len(t, software.Vulnerabilities, 1000)
|
|
assert.True(t, strings.HasPrefix(software.Vulnerabilities[0].CVE, "CVE-"))
|
|
assert.Equal(t,
|
|
"https://nvd.nist.gov/vuln/detail/"+software.Vulnerabilities[0].CVE,
|
|
software.Vulnerabilities[0].DetailsLink,
|
|
)
|
|
case "blah":
|
|
assert.Len(t, software.Vulnerabilities, 0)
|
|
assert.Equal(t, "someothercpewithoutvulns", software.GenerateCPE)
|
|
case "foo":
|
|
assert.Len(t, software.Vulnerabilities, 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testSoftwareList(t *testing.T, ds *Datastore) {
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
|
host3 := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
|
|
|
software1 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
}
|
|
software2 := []fleet.Software{
|
|
{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
software3 := []fleet.Software{
|
|
{Name: "baz", Version: "0.0.1", Source: "deb_packages"},
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(context.Background(), host3.ID, software3)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host3, false))
|
|
sort.Slice(host1.Software, func(i, j int) bool {
|
|
return host1.Software[i].Name+host1.Software[i].Version < host1.Software[j].Name+host1.Software[j].Version
|
|
})
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host1.Software[0].ID, CPE: "somecpe"},
|
|
{SoftwareID: host1.Software[1].ID, CPE: "someothercpewithoutvulns"},
|
|
{SoftwareID: host3.Software[0].ID, CPE: "somecpe2"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(context.Background(), cpes)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host3, false))
|
|
sort.Slice(host1.Software, func(i, j int) bool {
|
|
return host1.Software[i].Name+host1.Software[i].Version < host1.Software[j].Name+host1.Software[j].Version
|
|
})
|
|
|
|
vulns := []fleet.SoftwareVulnerability{
|
|
{SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0001", ResolvedInVersion: ptr.String("2.0.0")},
|
|
{SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0002", ResolvedInVersion: ptr.String("2.0.0")},
|
|
{SoftwareID: host3.Software[0].ID, CVE: "CVE-2022-0003", ResolvedInVersion: ptr.String("2.0.0")},
|
|
}
|
|
|
|
for _, v := range vulns {
|
|
_, err = ds.InsertSoftwareVulnerability(context.Background(), v, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
cveMeta := []fleet.CVEMeta{
|
|
{
|
|
CVE: "CVE-2022-0001",
|
|
CVSSScore: ptr.Float64(2.0),
|
|
EPSSProbability: ptr.Float64(0.01),
|
|
CISAKnownExploit: ptr.Bool(false),
|
|
Published: ptr.Time(now.Add(-2 * time.Hour)),
|
|
Description: "this is a description for CVE-2022-0001",
|
|
},
|
|
{
|
|
CVE: "CVE-2022-0002",
|
|
CVSSScore: ptr.Float64(1.0),
|
|
EPSSProbability: ptr.Float64(0.99),
|
|
CISAKnownExploit: ptr.Bool(false),
|
|
Published: ptr.Time(now),
|
|
Description: "this is a description for CVE-2022-0002",
|
|
},
|
|
{
|
|
CVE: "CVE-2022-0003",
|
|
CVSSScore: ptr.Float64(3.0),
|
|
EPSSProbability: ptr.Float64(0.98),
|
|
CISAKnownExploit: ptr.Bool(true),
|
|
Published: ptr.Time(now.Add(-1 * time.Hour)),
|
|
Description: "this is a description for CVE-2022-0003",
|
|
},
|
|
}
|
|
err = ds.InsertCVEMeta(context.Background(), cveMeta)
|
|
require.NoError(t, err)
|
|
|
|
foo001 := fleet.Software{
|
|
Name: "foo",
|
|
Version: "0.0.1",
|
|
Source: "chrome_extensions",
|
|
GenerateCPE: "somecpe",
|
|
Vulnerabilities: fleet.Vulnerabilities{
|
|
{
|
|
CVE: "CVE-2022-0001",
|
|
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0001",
|
|
CVSSScore: ptr.Float64Ptr(2.0),
|
|
EPSSProbability: ptr.Float64Ptr(0.01),
|
|
CISAKnownExploit: ptr.BoolPtr(false),
|
|
CVEPublished: ptr.TimePtr(now.Add(-2 * time.Hour)),
|
|
Description: ptr.StringPtr("this is a description for CVE-2022-0001"),
|
|
ResolvedInVersion: ptr.StringPtr("2.0.0"),
|
|
},
|
|
{
|
|
CVE: "CVE-2022-0002",
|
|
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0002",
|
|
CVSSScore: ptr.Float64Ptr(1.0),
|
|
EPSSProbability: ptr.Float64Ptr(0.99),
|
|
CISAKnownExploit: ptr.BoolPtr(false),
|
|
CVEPublished: ptr.TimePtr(now),
|
|
Description: ptr.StringPtr("this is a description for CVE-2022-0002"),
|
|
ResolvedInVersion: ptr.StringPtr("2.0.0"),
|
|
},
|
|
},
|
|
}
|
|
foo002 := fleet.Software{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"}
|
|
foo003 := fleet.Software{Name: "foo", Version: "0.0.3", Source: "chrome_extensions", GenerateCPE: "someothercpewithoutvulns"}
|
|
bar003 := fleet.Software{Name: "bar", Version: "0.0.3", Source: "deb_packages"}
|
|
baz001 := fleet.Software{
|
|
Name: "baz",
|
|
Version: "0.0.1",
|
|
Source: "deb_packages",
|
|
GenerateCPE: "somecpe2",
|
|
Vulnerabilities: fleet.Vulnerabilities{
|
|
{
|
|
CVE: "CVE-2022-0003",
|
|
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0003",
|
|
CVSSScore: ptr.Float64Ptr(3.0),
|
|
EPSSProbability: ptr.Float64Ptr(0.98),
|
|
CISAKnownExploit: ptr.BoolPtr(true),
|
|
CVEPublished: ptr.TimePtr(now.Add(-1 * time.Hour)),
|
|
Description: ptr.StringPtr("this is a description for CVE-2022-0003"),
|
|
ResolvedInVersion: ptr.StringPtr("2.0.0"),
|
|
},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, ds.SyncHostsSoftware(context.Background(), time.Now()))
|
|
|
|
t.Run("lists everything", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "name,version",
|
|
},
|
|
IncludeCVEScores: true,
|
|
}
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, opts, false)
|
|
expected := []fleet.Software{bar003, baz001, foo001, foo002, foo003}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
})
|
|
|
|
t.Run("paginates", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
Page: 1,
|
|
PerPage: 1,
|
|
OrderKey: "version",
|
|
IncludeMetadata: true,
|
|
},
|
|
IncludeCVEScores: true,
|
|
}
|
|
software := listSoftwareCheckCount(t, ds, 1, 5, opts, true)
|
|
require.Len(t, software, 1)
|
|
var expected []fleet.Software
|
|
// Both foo001 and baz001 have the same version, thus we check which one the database picked
|
|
// for the second page.
|
|
if software[0].Name == "foo" {
|
|
expected = []fleet.Software{foo001}
|
|
} else {
|
|
expected = []fleet.Software{baz001}
|
|
}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
})
|
|
|
|
t.Run("filters by team", func(t *testing.T) {
|
|
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{host1.ID}))
|
|
|
|
require.NoError(t, ds.SyncHostsSoftware(context.Background(), time.Now()))
|
|
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "version",
|
|
},
|
|
TeamID: &team1.ID,
|
|
IncludeCVEScores: true,
|
|
}
|
|
software := listSoftwareCheckCount(t, ds, 2, 2, opts, true)
|
|
expected := []fleet.Software{foo001, foo003}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
|
|
// Now that we have the software, we can test pagination.
|
|
// Figure out which software has the highest ID.
|
|
targetSoftware := software[0]
|
|
if targetSoftware.ID < software[1].ID {
|
|
targetSoftware = software[1]
|
|
}
|
|
expected = []fleet.Software{foo001}
|
|
if targetSoftware.Name == "foo" && targetSoftware.Version == "0.0.3" {
|
|
expected = []fleet.Software{foo003}
|
|
}
|
|
|
|
opts = fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
PerPage: 1,
|
|
Page: 1, // 2nd item, since 1st item is on page 0
|
|
OrderKey: "id",
|
|
IncludeMetadata: true,
|
|
},
|
|
TeamID: &team1.ID,
|
|
IncludeCVEScores: true,
|
|
}
|
|
software = listSoftwareCheckCount(t, ds, 1, 2, opts, true)
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
})
|
|
|
|
t.Run("filters vulnerable software", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "name",
|
|
},
|
|
VulnerableOnly: true,
|
|
IncludeCVEScores: true,
|
|
}
|
|
software := listSoftwareCheckCount(t, ds, 2, 2, opts, true)
|
|
expected := []fleet.Software{foo001, baz001}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
})
|
|
|
|
t.Run("filters by CVE", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
MatchQuery: "CVE-2022-0001",
|
|
},
|
|
IncludeCVEScores: true,
|
|
}
|
|
software := listSoftwareCheckCount(t, ds, 1, 1, opts, true)
|
|
expected := []fleet.Software{foo001}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
|
|
opts.ListOptions.MatchQuery = "CVE-2022-0002"
|
|
software = listSoftwareCheckCount(t, ds, 1, 1, opts, true)
|
|
expected = []fleet.Software{foo001}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
|
|
// partial CVE
|
|
opts.ListOptions.MatchQuery = "0002"
|
|
software = listSoftwareCheckCount(t, ds, 1, 1, opts, true)
|
|
expected = []fleet.Software{foo001}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
|
|
// unknown CVE
|
|
opts.ListOptions.MatchQuery = "CVE-2022-0000"
|
|
listSoftwareCheckCount(t, ds, 0, 0, opts, true)
|
|
})
|
|
|
|
t.Run("filters by query", func(t *testing.T) {
|
|
// query by name (case insensitive)
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
MatchQuery: "baR",
|
|
},
|
|
}
|
|
software := listSoftwareCheckCount(t, ds, 1, 1, opts, true)
|
|
expected := []fleet.Software{bar003}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
|
|
// query by version
|
|
opts.ListOptions.MatchQuery = "0.0.3"
|
|
software = listSoftwareCheckCount(t, ds, 2, 2, opts, true)
|
|
expected = []fleet.Software{foo003, bar003}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
|
|
// query by version (case insensitive)
|
|
opts.ListOptions.MatchQuery = "V0.0.2"
|
|
software = listSoftwareCheckCount(t, ds, 1, 1, opts, true)
|
|
expected = []fleet.Software{foo002}
|
|
test.ElementsMatchSkipID(t, software, expected)
|
|
})
|
|
|
|
t.Run("order by name and id", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "name,id",
|
|
OrderDirection: fleet.OrderAscending,
|
|
},
|
|
}
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, opts, false)
|
|
assert.Equal(t, bar003.Name, software[0].Name)
|
|
assert.Equal(t, bar003.Version, software[0].Version)
|
|
|
|
assert.Equal(t, baz001.Name, software[1].Name)
|
|
assert.Equal(t, baz001.Version, software[1].Version)
|
|
|
|
// foo's ordered by id, descending
|
|
assert.Greater(t, software[3].ID, software[2].ID)
|
|
assert.Greater(t, software[4].ID, software[3].ID)
|
|
})
|
|
|
|
t.Run("order by hosts_count", func(t *testing.T) {
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, fleet.SoftwareListOptions{ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}, WithHostCounts: true}, false)
|
|
// ordered by counts descending, so foo003 is first
|
|
assert.Equal(t, foo003.Name, software[0].Name)
|
|
assert.Equal(t, 2, software[0].HostsCount)
|
|
})
|
|
|
|
t.Run("order by epss_probability", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "epss_probability",
|
|
OrderDirection: fleet.OrderDescending,
|
|
},
|
|
IncludeCVEScores: true,
|
|
}
|
|
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, opts, false)
|
|
assert.Equal(t, foo001.Name, software[0].Name)
|
|
assert.Equal(t, foo001.Version, software[0].Version)
|
|
})
|
|
|
|
t.Run("order by cvss_score", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "cvss_score",
|
|
OrderDirection: fleet.OrderDescending,
|
|
},
|
|
IncludeCVEScores: true,
|
|
}
|
|
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, opts, false)
|
|
assert.Equal(t, baz001.Name, software[0].Name)
|
|
assert.Equal(t, baz001.Version, software[0].Version)
|
|
})
|
|
|
|
t.Run("order by cisa_known_exploit", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "cisa_known_exploit",
|
|
OrderDirection: fleet.OrderDescending,
|
|
},
|
|
IncludeCVEScores: true,
|
|
}
|
|
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, opts, false)
|
|
assert.Equal(t, baz001.Name, software[0].Name)
|
|
assert.Equal(t, baz001.Version, software[0].Version)
|
|
})
|
|
|
|
t.Run("order by cve_published", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "cve_published",
|
|
OrderDirection: fleet.OrderDescending,
|
|
},
|
|
IncludeCVEScores: true,
|
|
}
|
|
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, opts, false)
|
|
assert.Equal(t, foo001.Name, software[0].Name)
|
|
assert.Equal(t, foo001.Version, software[0].Version)
|
|
})
|
|
|
|
t.Run("nil cve scores if IncludeCVEScores is false", func(t *testing.T) {
|
|
opts := fleet.SoftwareListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "name,version",
|
|
OrderDirection: fleet.OrderDescending,
|
|
},
|
|
IncludeCVEScores: false,
|
|
}
|
|
|
|
software := listSoftwareCheckCount(t, ds, 5, 5, opts, false)
|
|
for _, s := range software {
|
|
for _, vuln := range s.Vulnerabilities {
|
|
assert.Nil(t, vuln.CVSSScore)
|
|
assert.Nil(t, vuln.EPSSProbability)
|
|
assert.Nil(t, vuln.CISAKnownExploit)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func listSoftwareCheckCount(t *testing.T, ds *Datastore, expectedListCount int, expectedFullCount int, opts fleet.SoftwareListOptions, returnSorted bool) []fleet.Software {
|
|
software, meta, err := ds.ListSoftware(context.Background(), opts)
|
|
require.NoError(t, err)
|
|
require.Len(t, software, expectedListCount)
|
|
count, err := ds.CountSoftware(context.Background(), opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedFullCount, count)
|
|
|
|
if opts.ListOptions.IncludeMetadata {
|
|
require.NotNil(t, meta)
|
|
if expectedListCount == expectedFullCount {
|
|
require.False(t, meta.HasPreviousResults)
|
|
require.True(t, meta.HasNextResults)
|
|
}
|
|
if expectedFullCount > expectedListCount {
|
|
shouldHavePrevious := opts.ListOptions.Page > 0
|
|
require.Equal(t, shouldHavePrevious, meta.HasPreviousResults)
|
|
|
|
shouldHaveNext := uint(expectedFullCount) > (opts.ListOptions.Page+1)*opts.ListOptions.PerPage // page is 0-indexed
|
|
require.Equal(t, shouldHaveNext, meta.HasNextResults)
|
|
}
|
|
} else {
|
|
require.Nil(t, meta)
|
|
}
|
|
|
|
for i, s := range software {
|
|
sort.Slice(s.Vulnerabilities, func(i, j int) bool {
|
|
return s.Vulnerabilities[i].CVE < s.Vulnerabilities[j].CVE
|
|
})
|
|
for i2, v := range s.Vulnerabilities {
|
|
require.Greater(t, v.CreatedAt, time.Now().Add(-time.Hour)) // assert non-zero
|
|
software[i].Vulnerabilities[i2].CreatedAt = time.Time{} // zero out for comparison
|
|
}
|
|
}
|
|
|
|
if returnSorted {
|
|
sort.Slice(software, func(i, j int) bool {
|
|
return software[i].Name+software[i].Version < software[j].Name+software[j].Version
|
|
})
|
|
}
|
|
return software
|
|
}
|
|
|
|
func testSoftwareSyncHostsSoftware(t *testing.T, ds *Datastore) {
|
|
countHostSoftwareBatchSizeOrig := countHostSoftwareBatchSize
|
|
softwareInsertBatchSizeOrig := softwareInsertBatchSize
|
|
t.Cleanup(
|
|
func() {
|
|
countHostSoftwareBatchSize = countHostSoftwareBatchSizeOrig
|
|
softwareInsertBatchSize = softwareInsertBatchSizeOrig
|
|
},
|
|
)
|
|
countHostSoftwareBatchSize = 2
|
|
softwareInsertBatchSize = 2
|
|
|
|
ctx := context.Background()
|
|
|
|
cmpNameVersionCount := func(want, got []fleet.Software) {
|
|
cmp := make([]fleet.Software, len(got))
|
|
for i, sw := range got {
|
|
cmp[i] = fleet.Software{Name: sw.Name, Version: sw.Version, HostsCount: sw.HostsCount}
|
|
}
|
|
require.ElementsMatch(t, want, cmp)
|
|
}
|
|
|
|
// this check ensures that the total number of rows in software_host_counts
|
|
// matches the expected value. we can't rely on ds.CountSoftware alone, as
|
|
// that method (rightfully) ignores orphaned software counts.
|
|
checkTableTotalCount := func(want int) {
|
|
var tableCount int
|
|
err := ds.writer(context.Background()).Get(&tableCount, "SELECT COUNT(*) FROM software_host_counts")
|
|
require.NoError(t, err)
|
|
require.Equal(t, want, tableCount)
|
|
}
|
|
|
|
host0 := test.NewHost(t, ds, "host0", "", "host0key", "host0uuid", time.Now())
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
|
hostTemp := test.NewHost(t, ds, "hostTemp", "", "hostTempKey", "hostTempUuid", time.Now())
|
|
|
|
// Get counts without any software.
|
|
globalOpts := fleet.SoftwareListOptions{
|
|
WithHostCounts: true, ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending},
|
|
}
|
|
_ = listSoftwareCheckCount(t, ds, 0, 0, globalOpts, false)
|
|
|
|
software0 := []fleet.Software{
|
|
{Name: "abc", Version: "0.0.1", Source: "apps"},
|
|
{Name: "def", Version: "0.0.1", Source: "apps"},
|
|
}
|
|
software1 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
}
|
|
software2 := []fleet.Software{
|
|
{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
softwareTemp := make([]fleet.Software, 0, 10)
|
|
for i := 0; i < 10; i++ {
|
|
softwareTemp = append(
|
|
softwareTemp, fleet.Software{Name: fmt.Sprintf("foo%d", i), Version: fmt.Sprintf("%d.0.1", i), Source: "deb_packages"},
|
|
)
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(ctx, host0.ID, software0)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(ctx, host1.ID, software1)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(ctx, hostTemp.ID, softwareTemp)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(ctx, host2.ID, software2)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
|
|
_ = listSoftwareCheckCount(t, ds, 16, 16, globalOpts, false)
|
|
checkTableTotalCount(16)
|
|
|
|
// Now, delete 2 hosts. Software with the lowest ID is removed, and there should be a chunk with missing software IDs from the deleted hostTemp software.
|
|
require.NoError(t, ds.DeleteHost(ctx, host0.ID))
|
|
require.NoError(t, ds.DeleteHost(ctx, hostTemp.ID))
|
|
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
globalCounts := listSoftwareCheckCount(t, ds, 4, 4, globalOpts, false)
|
|
want := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
|
{Name: "bar", Version: "0.0.3", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, globalCounts)
|
|
checkTableTotalCount(4)
|
|
|
|
// update host2, remove "bar" software
|
|
software2 = []fleet.Software{
|
|
{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, host2.ID, software2)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
|
|
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, globalCounts)
|
|
checkTableTotalCount(3)
|
|
|
|
// create a software entry without any host and any counts
|
|
_, err = ds.writer(ctx).ExecContext(ctx, fmt.Sprintf(`INSERT INTO software (name, version, source, checksum) VALUES ('baz', '0.0.1', 'testing', %s)`, softwareChecksumComputedColumn("")))
|
|
require.NoError(t, err)
|
|
|
|
// listing does not return the new software entry
|
|
allSw := listSoftwareCheckCount(t, ds, 3, 3, fleet.SoftwareListOptions{}, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 0},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 0},
|
|
{Name: "foo", Version: "v0.0.2", HostsCount: 0},
|
|
}
|
|
cmpNameVersionCount(want, allSw)
|
|
|
|
// create 2 teams and assign a new host to each
|
|
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
|
require.NoError(t, err)
|
|
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
|
|
require.NoError(t, err)
|
|
host3 := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
|
require.NoError(t, ds.AddHostsToTeam(ctx, &team1.ID, []uint{host3.ID}))
|
|
host4 := test.NewHost(t, ds, "host4", "", "host4key", "host4uuid", time.Now())
|
|
require.NoError(t, ds.AddHostsToTeam(ctx, &team2.ID, []uint{host4.ID}))
|
|
|
|
// assign existing host1 to team1 too, so we have a team with multiple hosts
|
|
require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{host1.ID}))
|
|
// use some software for host3 and host4
|
|
software3 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
}
|
|
software4 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
|
|
_, err = ds.UpdateHostSoftware(ctx, host3.ID, software3)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(ctx, host4.ID, software4)
|
|
require.NoError(t, err)
|
|
|
|
// at this point, there's no counts per team, only global counts
|
|
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, globalCounts)
|
|
checkTableTotalCount(3)
|
|
|
|
team1Opts := fleet.SoftwareListOptions{WithHostCounts: true, TeamID: ptr.Uint(team1.ID), ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}}
|
|
team1Counts := listSoftwareCheckCount(t, ds, 0, 0, team1Opts, false)
|
|
want = []fleet.Software{}
|
|
cmpNameVersionCount(want, team1Counts)
|
|
checkTableTotalCount(3)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host1, false))
|
|
nilSoftware, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, &team1.ID, false, nil)
|
|
assert.Nil(t, nilSoftware)
|
|
assert.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
// after a call to Calculate, the global counts are updated and the team counts appear
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
|
|
globalCounts = listSoftwareCheckCount(t, ds, 4, 4, globalOpts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 4},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
|
{Name: "bar", Version: "0.0.3", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, globalCounts)
|
|
|
|
team1Counts = listSoftwareCheckCount(t, ds, 2, 2, team1Opts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, team1Counts)
|
|
|
|
// composite pk (software_id, team_id), so we expect more rows
|
|
checkTableTotalCount(8)
|
|
|
|
soft1ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[0].ID, &team1.ID, false, nil)
|
|
require.NoError(t, err)
|
|
soft2ByID, err := ds.SoftwareByID(context.Background(), host1.HostSoftware.Software[1].ID, &team1.ID, false, nil)
|
|
require.NoError(t, err)
|
|
test.ElementsMatchSkipIDAndHostCount(t, software1, []fleet.Software{*soft1ByID, *soft2ByID})
|
|
|
|
team2Opts := fleet.SoftwareListOptions{WithHostCounts: true, TeamID: ptr.Uint(team2.ID), ListOptions: fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderDescending}}
|
|
team2Counts := listSoftwareCheckCount(t, ds, 2, 2, team2Opts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 1},
|
|
{Name: "bar", Version: "0.0.3", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, team2Counts)
|
|
|
|
// update host4 (team2), remove "bar" software
|
|
software4 = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
}
|
|
|
|
_, err = ds.UpdateHostSoftware(ctx, host4.ID, software4)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
|
|
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 4},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, globalCounts)
|
|
|
|
team1Counts = listSoftwareCheckCount(t, ds, 2, 2, team1Opts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, team1Counts)
|
|
|
|
team2Counts = listSoftwareCheckCount(t, ds, 1, 1, team2Opts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, team2Counts)
|
|
|
|
checkTableTotalCount(6)
|
|
|
|
// update host4 (team2), remove all software and delete team
|
|
software4 = []fleet.Software{}
|
|
_, err = ds.UpdateHostSoftware(ctx, host4.ID, software4)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.DeleteTeam(ctx, team2.ID))
|
|
|
|
// this call will remove team2 from the software host counts table
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
|
|
globalCounts = listSoftwareCheckCount(t, ds, 3, 3, globalOpts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 3},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
{Name: "foo", Version: "v0.0.2", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, globalCounts)
|
|
|
|
team1Counts = listSoftwareCheckCount(t, ds, 2, 2, team1Opts, false)
|
|
want = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(want, team1Counts)
|
|
|
|
listSoftwareCheckCount(t, ds, 0, 0, team2Opts, false)
|
|
checkTableTotalCount(5)
|
|
}
|
|
|
|
// softwareChecksumComputedColumn computes the checksum for a software entry
|
|
// The calculation must match the one in computeRawChecksum
|
|
func softwareChecksumComputedColumn(tableAlias string) string {
|
|
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
|
|
tableAlias += "."
|
|
}
|
|
|
|
// concatenate with separator \x00
|
|
return fmt.Sprintf(
|
|
` UNHEX(
|
|
MD5(
|
|
CONCAT_WS(CHAR(0),
|
|
%sname,
|
|
%[1]sversion,
|
|
%[1]ssource,
|
|
COALESCE(%[1]sbundle_identifier, ''),
|
|
`+"%[1]s`release`"+`,
|
|
%[1]sarch,
|
|
%[1]svendor,
|
|
%[1]sbrowser,
|
|
%[1]sextension_id
|
|
)
|
|
)
|
|
) `, tableAlias,
|
|
)
|
|
}
|
|
|
|
func insertVulnSoftwareForTest(t *testing.T, ds *Datastore) {
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithComputerName("computer1"))
|
|
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
|
|
|
software1 := []fleet.Software{
|
|
{
|
|
Name: "foo.rpm",
|
|
Version: "0.0.1",
|
|
Source: "rpm_packages",
|
|
GenerateCPE: "cpe_foo_rpm",
|
|
},
|
|
{
|
|
Name: "foo.chrome",
|
|
Version: "0.0.3",
|
|
Source: "chrome_extensions",
|
|
GenerateCPE: "cpe_foo_chrome_3",
|
|
},
|
|
}
|
|
software2 := []fleet.Software{
|
|
{
|
|
Name: "foo.chrome",
|
|
Version: "0.0.2",
|
|
Source: "chrome_extensions",
|
|
GenerateCPE: "cpe_foo_chrome_2",
|
|
},
|
|
{
|
|
Name: "foo.chrome",
|
|
Version: "0.0.3",
|
|
Source: "chrome_extensions",
|
|
GenerateCPE: "cpe_foo_chrome_3",
|
|
Vulnerabilities: fleet.Vulnerabilities{
|
|
{
|
|
CVE: "CVE-2022-0001",
|
|
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0001",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "bar.rpm",
|
|
Version: "0.0.3",
|
|
Source: "rpm_packages",
|
|
GenerateCPE: "cpe_bar_rpm",
|
|
Vulnerabilities: fleet.Vulnerabilities{
|
|
{
|
|
CVE: "CVE-2022-0002",
|
|
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0002",
|
|
},
|
|
{
|
|
CVE: "CVE-2022-0003",
|
|
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-333-444-555",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
mutationResults, err := ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
|
|
require.NoError(t, err)
|
|
|
|
// 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 {
|
|
return host1.Software[i].Name+host1.Software[i].Version < host1.Software[j].Name+host1.Software[j].Version
|
|
})
|
|
sort.Slice(host2.Software, func(i, j int) bool {
|
|
return host2.Software[i].Name+host2.Software[i].Version < host2.Software[j].Name+host2.Software[j].Version
|
|
})
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host1.Software[0].ID, CPE: "cpe_foo_chrome_3"},
|
|
{SoftwareID: host1.Software[1].ID, CPE: "cpe_foo_rpm"},
|
|
{SoftwareID: host2.Software[0].ID, CPE: "cpe_bar_rpm"},
|
|
{SoftwareID: host2.Software[1].ID, CPE: "cpe_foo_chrome_2"},
|
|
{SoftwareID: host2.Software[2].ID, CPE: "cpe_foo_chrome_3"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(context.Background(), cpes)
|
|
require.NoError(t, err)
|
|
|
|
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 {
|
|
return host1.Software[i].Name+host1.Software[i].Version < host1.Software[j].Name+host1.Software[j].Version
|
|
})
|
|
sort.Slice(host2.Software, func(i, j int) bool {
|
|
return host2.Software[i].Name+host2.Software[i].Version < host2.Software[j].Name+host2.Software[j].Version
|
|
})
|
|
|
|
chrome3 := host2.Software[2]
|
|
inserted, err := ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{
|
|
SoftwareID: chrome3.ID,
|
|
CVE: "CVE-2022-0001",
|
|
}, fleet.NVDSource)
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
|
|
barRpm := host2.Software[0]
|
|
vulns := []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: barRpm.ID,
|
|
CVE: "CVE-2022-0002",
|
|
},
|
|
{
|
|
SoftwareID: barRpm.ID,
|
|
CVE: "CVE-2022-0003",
|
|
},
|
|
}
|
|
|
|
for _, v := range vulns {
|
|
inserted, err := ds.InsertSoftwareVulnerability(context.Background(), v, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
}
|
|
|
|
require.NoError(t, ds.SyncHostsSoftware(context.Background(), time.Now()))
|
|
}
|
|
|
|
func testDeleteSoftwareVulnerabilities(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
err := ds.DeleteSoftwareVulnerabilities(ctx, nil)
|
|
require.NoError(t, err)
|
|
|
|
insertVulnSoftwareForTest(t, ds)
|
|
|
|
err = ds.DeleteSoftwareVulnerabilities(ctx, []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: 999, // unknown software
|
|
CVE: "CVE-2022-0003",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
host2, err := ds.HostByIdentifier(ctx, "host2")
|
|
require.NoError(t, err)
|
|
|
|
err = ds.LoadHostSoftware(ctx, host2, false)
|
|
require.NoError(t, err)
|
|
sort.Slice(host2.Software, func(i, j int) bool {
|
|
return host2.Software[i].Name+host2.Software[i].Version < host2.Software[j].Name+host2.Software[j].Version
|
|
})
|
|
|
|
barRPM := host2.Software[0]
|
|
require.Len(t, barRPM.Vulnerabilities, 2)
|
|
|
|
err = ds.DeleteSoftwareVulnerabilities(ctx, []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: barRPM.ID,
|
|
CVE: "CVE-0000-0000", // unknown CVE
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.DeleteSoftwareVulnerabilities(ctx, []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: barRPM.ID,
|
|
CVE: "CVE-2022-0003",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.LoadHostSoftware(ctx, host2, false)
|
|
require.NoError(t, err)
|
|
sort.Slice(host2.Software, func(i, j int) bool {
|
|
return host2.Software[i].Name+host2.Software[i].Version < host2.Software[j].Name+host2.Software[j].Version
|
|
})
|
|
|
|
barRPM = host2.Software[0]
|
|
require.Len(t, barRPM.Vulnerabilities, 1)
|
|
|
|
err = ds.DeleteSoftwareVulnerabilities(ctx, []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: barRPM.ID,
|
|
CVE: "CVE-2022-0002",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = ds.LoadHostSoftware(ctx, host2, false)
|
|
require.NoError(t, err)
|
|
sort.Slice(host2.Software, func(i, j int) bool {
|
|
return host2.Software[i].Name+host2.Software[i].Version < host2.Software[j].Name+host2.Software[j].Version
|
|
})
|
|
|
|
barRPM = host2.Software[0]
|
|
require.Empty(t, barRPM.Vulnerabilities)
|
|
}
|
|
|
|
func testHostsByCVE(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
hosts, err := ds.HostsByCVE(ctx, "CVE-0000-0000")
|
|
require.NoError(t, err)
|
|
require.Len(t, hosts, 0)
|
|
|
|
insertVulnSoftwareForTest(t, ds)
|
|
|
|
// CVE of foo chrome 0.0.3, both hosts have it
|
|
hosts, err = ds.HostsByCVE(ctx, "CVE-2022-0001")
|
|
require.NoError(t, err)
|
|
require.Len(t, hosts, 2)
|
|
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",
|
|
},
|
|
},
|
|
})
|
|
|
|
// CVE of bar.rpm 0.0.3, only host 2 has it
|
|
hosts, err = ds.HostsByCVE(ctx, "CVE-2022-0002")
|
|
require.NoError(t, err)
|
|
require.Len(t, hosts, 1)
|
|
require.Equal(t, hosts[0].Hostname, "host2")
|
|
}
|
|
|
|
func testHostVulnSummariesBySoftwareIDs(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
// Invalid non-existing host id
|
|
hosts, err := ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{0})
|
|
require.NoError(t, err)
|
|
require.Len(t, hosts, 0)
|
|
|
|
insertVulnSoftwareForTest(t, ds)
|
|
|
|
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 {
|
|
switch s.GenerateCPE {
|
|
case "cpe_foo_rpm":
|
|
fooRpm = s
|
|
case "cpe_foo_chrome_3":
|
|
chrome3 = s
|
|
case "cpe_bar_rpm":
|
|
barRpm = s
|
|
}
|
|
}
|
|
require.NotZero(t, chrome3.ID)
|
|
require.NotZero(t, barRpm.ID)
|
|
|
|
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{chrome3.ID})
|
|
require.NoError(t, err)
|
|
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"},
|
|
},
|
|
})
|
|
|
|
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{barRpm.ID})
|
|
require.NoError(t, err)
|
|
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.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
|
|
// from hosts (ds.UpdateHostSoftware) will remove the corresponding entry in
|
|
// `software` if no more hosts have the application installed.
|
|
func testUpdateHostSoftwareUpdatesSoftware(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
h1 := test.NewHost(t, ds, "host", "", "hostkey", "hostuuid", time.Now())
|
|
h2 := test.NewHost(t, ds, "host2", "", "hostkey2", "hostuuid2", time.Now())
|
|
|
|
// Set the initial software list.
|
|
sw1 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo"},
|
|
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar"},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, h1.ID, sw1)
|
|
require.NoError(t, err)
|
|
sw2 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo"},
|
|
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar"},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz"},
|
|
{Name: "baz2", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz"},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, h2.ID, sw2)
|
|
require.NoError(t, err)
|
|
|
|
// ListSoftware uses host_software_counts table.
|
|
err = ds.SyncHostsSoftware(ctx, time.Now())
|
|
require.NoError(t, err)
|
|
|
|
// Check the returned software.
|
|
cmpNameVersionCount := func(expected, got []fleet.Software) {
|
|
cmp := make([]fleet.Software, len(got))
|
|
for i, sw := range got {
|
|
cmp[i] = fleet.Software{Name: sw.Name, Version: sw.Version, HostsCount: sw.HostsCount}
|
|
}
|
|
require.ElementsMatch(t, expected, cmp)
|
|
}
|
|
opts := fleet.SoftwareListOptions{WithHostCounts: true}
|
|
software := listSoftwareCheckCount(t, ds, 4, 4, opts, false)
|
|
expectedSoftware := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 2},
|
|
{Name: "bar", Version: "0.0.2", HostsCount: 2},
|
|
{Name: "baz", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "baz2", Version: "0.0.3", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(expectedSoftware, software)
|
|
|
|
// Update software for the two hosts.
|
|
//
|
|
// - foo is still present in both hosts
|
|
// - new is added to h1.
|
|
// - baz is removed from h2.
|
|
// - baz2 is removed from h2.
|
|
// - bar is removed from both hosts.
|
|
sw1Updated := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo"},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz"},
|
|
{Name: "new", Version: "0.0.4", Source: "test", GenerateCPE: "cpe_new"},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, h1.ID, sw1Updated)
|
|
require.NoError(t, err)
|
|
sw2Updated := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo"},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, h2.ID, sw2Updated)
|
|
require.NoError(t, err)
|
|
|
|
var (
|
|
bazSoftwareID uint
|
|
barSoftwareID uint
|
|
baz2SoftwareID uint
|
|
)
|
|
for _, s := range software {
|
|
if s.Name == "baz" {
|
|
bazSoftwareID = s.ID
|
|
}
|
|
if s.Name == "baz2" {
|
|
baz2SoftwareID = s.ID
|
|
}
|
|
if s.Name == "bar" {
|
|
barSoftwareID = s.ID
|
|
}
|
|
}
|
|
require.NotZero(t, bazSoftwareID)
|
|
require.NotZero(t, barSoftwareID)
|
|
require.NotZero(t, baz2SoftwareID)
|
|
|
|
// "baz2" is still present in the database, even though no hosts are using it, until ds.SyncHostsSoftware is executed.
|
|
soft, err := ds.SoftwareByID(ctx, baz2SoftwareID, nil, false, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "baz2", soft.Name)
|
|
assert.Zero(t, soft.HostsCount)
|
|
|
|
// "new" is not returned until ds.SyncHostsSoftware is executed.
|
|
// "bar" and "baz2" are gone from host_software, but will not be deleted until ds.SyncHostsSoftware is executed.
|
|
// "baz" still has the wrong count because ds.SyncHostsSoftware hasn't run yet.
|
|
//
|
|
// So... counts are "off" until ds.SyncHostsSoftware is run.
|
|
software = listSoftwareCheckCount(t, ds, 4, 4, opts, false)
|
|
expectedSoftware = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 2},
|
|
{Name: "baz", Version: "0.0.3", HostsCount: 2},
|
|
{Name: "bar", Version: "0.0.2", HostsCount: 2},
|
|
{Name: "baz2", Version: "0.0.3", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(expectedSoftware, software)
|
|
|
|
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.HostVulnSummariesBySoftwareIDs(ctx, []uint{barSoftwareID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, hosts)
|
|
hosts, err = ds.HostVulnSummariesBySoftwareIDs(ctx, []uint{baz2SoftwareID})
|
|
require.NoError(t, err)
|
|
require.Empty(t, hosts)
|
|
|
|
// ListSoftware uses host_software_counts table.
|
|
err = ds.SyncHostsSoftware(ctx, time.Now())
|
|
require.NoError(t, err)
|
|
|
|
software = listSoftwareCheckCount(t, ds, 3, 3, opts, false)
|
|
expectedSoftware = []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", HostsCount: 2},
|
|
{Name: "baz", Version: "0.0.3", HostsCount: 1},
|
|
{Name: "new", Version: "0.0.4", HostsCount: 1},
|
|
}
|
|
cmpNameVersionCount(expectedSoftware, software)
|
|
}
|
|
|
|
func testUpdateHostSoftware(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
softwareInsertBatchSizeOrig := softwareInsertBatchSize
|
|
t.Cleanup(
|
|
func() {
|
|
softwareInsertBatchSize = softwareInsertBatchSizeOrig
|
|
},
|
|
)
|
|
softwareInsertBatchSize = 2
|
|
|
|
now := time.Now()
|
|
lastYear := now.Add(-365 * 24 * time.Hour)
|
|
|
|
// sort software slice by last opened at timestamp
|
|
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
|
|
switch {
|
|
case lts == nil && rts == nil:
|
|
return true
|
|
case lts == nil && rts != nil:
|
|
return true
|
|
case lts != nil && rts == nil:
|
|
return false
|
|
default:
|
|
return (*lts).Before(*rts) || ((*lts).Equal(*rts) && lsw.Name < rsw.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
host := test.NewHost(t, ds, "host", "", "hostkey", "hostuuid", time.Now())
|
|
|
|
type tup struct {
|
|
name string
|
|
ts time.Time
|
|
}
|
|
validateSoftware := func(expect ...tup) {
|
|
err := ds.LoadHostSoftware(ctx, host, false)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, host.Software, len(expect))
|
|
sort.Slice(host.Software, genSortFn(host.Software))
|
|
|
|
for i, sw := range host.Software {
|
|
want := expect[i]
|
|
require.Equal(t, want.name, sw.Name)
|
|
|
|
var titleID uint
|
|
require.NoError(
|
|
t, ds.writer(ctx).GetContext(
|
|
ctx, &titleID,
|
|
`SELECT s.title_id FROM software s INNER JOIN software_titles st ON (s.name = st.name AND s.source = st.source AND s.browser = st.browser) WHERE st.id = ?`,
|
|
sw.ID,
|
|
),
|
|
)
|
|
assert.NotZero(t, titleID)
|
|
|
|
if want.ts.IsZero() {
|
|
require.Nil(t, sw.LastOpenedAt)
|
|
} else {
|
|
require.WithinDuration(t, want.ts, *sw.LastOpenedAt, time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
// set the initial software list
|
|
sw := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo", Browser: "chrome"},
|
|
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar", LastOpenedAt: &lastYear},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz", LastOpenedAt: &now},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, sw)
|
|
require.NoError(t, err)
|
|
validateSoftware(tup{name: "foo"}, tup{"bar", lastYear}, tup{"baz", now})
|
|
|
|
// make changes: remove foo, add qux, no new timestamp on bar, small ts change on baz
|
|
nowish := now.Add(3 * time.Second)
|
|
sw = []fleet.Software{
|
|
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar"},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz", LastOpenedAt: &nowish},
|
|
{Name: "qux", Version: "0.0.4", Source: "test", GenerateCPE: "cpe_qux"},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, host.ID, sw)
|
|
require.NoError(t, err)
|
|
validateSoftware(tup{name: "qux"}, tup{"bar", lastYear}, tup{"baz", now}) // baz hasn't been updated to nowish, too small diff
|
|
|
|
// more changes: bar receives a date further in the past, baz and qux to future
|
|
lastLastYear := lastYear.Add(-365 * 24 * time.Hour)
|
|
future := now.Add(3 * 24 * time.Hour)
|
|
sw = []fleet.Software{
|
|
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar", LastOpenedAt: &lastLastYear},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz", LastOpenedAt: &future},
|
|
{Name: "qux", Version: "0.0.4", Source: "test", GenerateCPE: "cpe_qux", LastOpenedAt: &future},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, host.ID, sw)
|
|
require.NoError(t, err)
|
|
validateSoftware(tup{"bar", lastYear}, tup{"baz", future}, tup{"qux", future})
|
|
|
|
// more changes: all software receives a date further in the future, so all should be updated
|
|
farFuture := now.Add(4 * 24 * time.Hour)
|
|
sw = []fleet.Software{
|
|
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar", LastOpenedAt: &farFuture},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz", LastOpenedAt: &farFuture},
|
|
{Name: "qux", Version: "0.0.4", Source: "test", GenerateCPE: "cpe_qux", LastOpenedAt: &farFuture},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, host.ID, sw)
|
|
require.NoError(t, err)
|
|
validateSoftware(tup{"bar", farFuture}, tup{"baz", farFuture}, tup{"qux", farFuture})
|
|
}
|
|
|
|
func testListSoftwareByHostIDShort(t *testing.T, ds *Datastore) {
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
|
|
|
software1 := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
}
|
|
software2 := []fleet.Software{
|
|
{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
|
|
_, 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))
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host2, false))
|
|
|
|
software, err := ds.ListSoftwareByHostIDShort(context.Background(), host1.ID)
|
|
require.NoError(t, err)
|
|
test.ElementsMatchSkipID(t, software1, software)
|
|
|
|
software, err = ds.ListSoftwareByHostIDShort(context.Background(), host2.ID)
|
|
require.NoError(t, err)
|
|
test.ElementsMatchSkipID(t, software2, software)
|
|
|
|
// bad host id returns no software
|
|
badHostID := uint(3)
|
|
software, err = ds.ListSoftwareByHostIDShort(context.Background(), badHostID)
|
|
require.NoError(t, err)
|
|
require.Len(t, software, 0)
|
|
}
|
|
|
|
func testListSoftwareVulnerabilitiesByHostIDsSource(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "apps"},
|
|
{Name: "blah", Version: "1.0", Source: "apps"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "foo_cpe"},
|
|
{SoftwareID: host.Software[1].ID, CPE: "bar_cpe"},
|
|
{SoftwareID: host.Software[2].ID, CPE: "blah_cpe"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
cveMap := map[int]string{
|
|
0: "cve-123",
|
|
1: "cve-456",
|
|
}
|
|
|
|
for i, s := range host.Software {
|
|
cve, ok := cveMap[i]
|
|
if ok {
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
|
|
SoftwareID: s.ID,
|
|
CVE: cve,
|
|
}, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
}
|
|
|
|
}
|
|
result, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
|
|
var actualCVEs []string
|
|
for _, r := range result[host.ID] {
|
|
actualCVEs = append(actualCVEs, r.CVE)
|
|
}
|
|
|
|
expectedCVEs := []string{"cve-123", "cve-456"}
|
|
require.ElementsMatch(t, expectedCVEs, actualCVEs)
|
|
|
|
for _, r := range result[host.ID] {
|
|
require.NotEqual(t, r.SoftwareID, 0)
|
|
}
|
|
}
|
|
|
|
func testInsertSoftwareVulnerability(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("no vulnerabilities to insert", func(t *testing.T) {
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.False(t, inserted)
|
|
})
|
|
|
|
t.Run("duplicated vulnerabilities", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
software := fleet.Software{
|
|
Name: "foo", Version: "0.0.1", Source: "chrome_extensions",
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, []fleet.Software{software})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "foo_cpe_1"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
|
|
SoftwareID: host.Software[0].ID, CVE: "cve-1",
|
|
}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
|
|
inserted, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
|
|
SoftwareID: host.Software[0].ID, CVE: "cve-1",
|
|
}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.False(t, inserted)
|
|
|
|
storedVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
|
|
occurrence := make(map[string]int)
|
|
for _, v := range storedVulns[host.ID] {
|
|
occurrence[v.CVE] = occurrence[v.CVE] + 1
|
|
}
|
|
require.Equal(t, 1, occurrence["cve-1"])
|
|
})
|
|
|
|
t.Run("a vulnerability already exists", func(t *testing.T) {
|
|
host := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
|
software := fleet.Software{
|
|
Name: "foo", Version: "0.0.1", Source: "chrome_extensions",
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, []fleet.Software{software})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "foo_cpe_2"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
var vulns []fleet.SoftwareVulnerability
|
|
for _, s := range host.Software {
|
|
vulns = append(vulns, fleet.SoftwareVulnerability{
|
|
SoftwareID: s.ID,
|
|
CVE: "cve-2",
|
|
})
|
|
}
|
|
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
|
|
inserted, err = ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.False(t, inserted)
|
|
|
|
storedVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
|
|
occurrence := make(map[string]int)
|
|
for _, v := range storedVulns[host.ID] {
|
|
occurrence[v.CVE] = occurrence[v.CVE] + 1
|
|
}
|
|
require.Equal(t, 1, occurrence["cve-1"])
|
|
require.Equal(t, 1, occurrence["cve-2"])
|
|
})
|
|
|
|
t.Run("vulnerability includes version range", func(t *testing.T) {
|
|
// new host
|
|
host := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
|
|
|
// new software
|
|
software := fleet.Software{
|
|
Name: "host3software", Version: "0.0.1", Source: "chrome_extensions",
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, []fleet.Software{software})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
// new software cpe
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "cpe:2.3:a:foo:foo:0.0.1:*:*:*:*:*:*:*"},
|
|
}
|
|
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
// new vulnerability
|
|
vuln := fleet.SoftwareVulnerability{
|
|
SoftwareID: host.Software[0].ID,
|
|
CVE: "cve-3",
|
|
ResolvedInVersion: ptr.String("1.2.3"),
|
|
}
|
|
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, vuln, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
|
|
// vulnerability with no ResolvedInVersion
|
|
vuln = fleet.SoftwareVulnerability{
|
|
SoftwareID: host.Software[0].ID,
|
|
CVE: "cve-4",
|
|
}
|
|
|
|
inserted, err = ds.InsertSoftwareVulnerability(ctx, vuln, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
|
|
storedVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, storedVulns[host.ID], 2)
|
|
require.Equal(t, "cve-3", storedVulns[host.ID][0].CVE)
|
|
require.Equal(t, "1.2.3", *storedVulns[host.ID][0].ResolvedInVersion)
|
|
require.Equal(t, "cve-4", storedVulns[host.ID][1].CVE)
|
|
require.Nil(t, storedVulns[host.ID][1].ResolvedInVersion)
|
|
})
|
|
}
|
|
|
|
func testListCVEs(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
now := time.Now().UTC()
|
|
threeDaysAgo := now.Add(-3 * 24 * time.Hour)
|
|
twoWeeksAgo := now.Add(-14 * 24 * time.Hour)
|
|
twoMonthsAgo := now.Add(-60 * 24 * time.Hour)
|
|
|
|
testCases := []fleet.CVEMeta{
|
|
{CVE: "cve-1", Published: &threeDaysAgo, Description: "cve-1 description"},
|
|
{CVE: "cve-2", Published: &twoWeeksAgo, Description: "cve-2 description"},
|
|
{CVE: "cve-3", Published: &twoMonthsAgo}, // past maxAge
|
|
{CVE: "cve-4"}, // no published date
|
|
}
|
|
|
|
err := ds.InsertCVEMeta(ctx, testCases)
|
|
require.NoError(t, err)
|
|
|
|
result, err := ds.ListCVEs(ctx, 30*24*time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
expected := []string{"cve-1", "cve-1 description", "cve-2", "cve-2 description"}
|
|
var actual []string
|
|
for _, r := range result {
|
|
actual = append(actual, r.CVE)
|
|
actual = append(actual, r.Description)
|
|
}
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|
|
func testListSoftwareForVulnDetection(t *testing.T, ds *Datastore) {
|
|
t.Run("returns software without CPE entries", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
host := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
|
host.Platform = "debian"
|
|
require.NoError(t, ds.UpdateHost(ctx, host))
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "apps"},
|
|
{Name: "biz", Version: "0.0.1", Source: "deb_packages"},
|
|
{Name: "baz", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, []fleet.SoftwareCPE{{SoftwareID: host.Software[0].ID, CPE: "cpe1"}})
|
|
require.NoError(t, err)
|
|
// Load software again so that CPE data is included.
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
filter := fleet.VulnSoftwareFilter{HostID: &host.ID}
|
|
result, err := ds.ListSoftwareForVulnDetection(ctx, filter)
|
|
require.NoError(t, err)
|
|
|
|
sort.Slice(host.Software, func(i, j int) bool { return host.Software[i].ID < host.Software[j].ID })
|
|
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
|
|
|
|
require.Equal(t, len(host.Software), len(result))
|
|
|
|
for i := range host.Software {
|
|
require.Equal(t, host.Software[i].ID, result[i].ID)
|
|
require.Equal(t, host.Software[i].Name, result[i].Name)
|
|
require.Equal(t, host.Software[i].Version, result[i].Version)
|
|
require.Equal(t, host.Software[i].Release, result[i].Release)
|
|
require.Equal(t, host.Software[i].Arch, result[i].Arch)
|
|
require.Equal(t, host.Software[i].GenerateCPE, result[i].GenerateCPE)
|
|
}
|
|
|
|
// test name filter
|
|
filter = fleet.VulnSoftwareFilter{Name: "fo"} // LIKE match
|
|
result, err = ds.ListSoftwareForVulnDetection(ctx, filter)
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 1)
|
|
require.Equal(t, "foo", result[0].Name)
|
|
|
|
// test source filter
|
|
filter = fleet.VulnSoftwareFilter{Source: "deb_packages"}
|
|
result, err = ds.ListSoftwareForVulnDetection(ctx, filter)
|
|
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 2)
|
|
require.Equal(t, "baz", result[0].Name)
|
|
require.Equal(t, "biz", result[1].Name)
|
|
})
|
|
}
|
|
|
|
func testSoftwareByIDNoDuplicatedVulns(t *testing.T, ds *Datastore) {
|
|
t.Run("software installed in multiple hosts does not have duplicated vulnerabilities", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
hostA := test.NewHost(t, ds, "hostA", "", "hostAkey", "hostAuuid", time.Now())
|
|
hostA.Platform = "ubuntu"
|
|
require.NoError(t, ds.UpdateHost(ctx, hostA))
|
|
|
|
hostB := test.NewHost(t, ds, "hostB", "", "hostBkey", "hostBuuid", time.Now())
|
|
hostB.Platform = "ubuntu"
|
|
require.NoError(t, ds.UpdateHost(ctx, hostB))
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo_123", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar_123", Version: "0.0.3", Source: "apps"},
|
|
{Name: "biz_123", Version: "0.0.1", Source: "deb_packages"},
|
|
{Name: "baz_123", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(ctx, hostA.ID, software)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(ctx, hostB.ID, software)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, hostA, false))
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, hostB, false))
|
|
|
|
// Add one vulnerability to each software
|
|
for i, s := range hostA.Software {
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
|
|
SoftwareID: s.ID,
|
|
CVE: fmt.Sprintf("cve-%d", i),
|
|
}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
}
|
|
|
|
for _, s := range hostA.Software {
|
|
result, err := ds.SoftwareByID(ctx, s.ID, nil, true, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, result.Vulnerabilities, 1)
|
|
}
|
|
})
|
|
}
|
|
|
|
func testSoftwareByIDIncludesCVEPublishedDate(t *testing.T, ds *Datastore) {
|
|
t.Run("software.vulnerabilities includes the published date", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
host := test.NewHost(t, ds, "hostA", "", "hostAkey", "hostAuuid", time.Now())
|
|
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{host.ID}))
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
hasVuln bool
|
|
hasMeta bool
|
|
hasPublishedDate bool
|
|
}{
|
|
{"foo_123", true, true, true},
|
|
{"bar_123", true, true, false},
|
|
{"foo_456", true, false, false},
|
|
{"bar_456", false, true, true},
|
|
{"foo_789", false, true, false},
|
|
{"bar_789", false, false, false},
|
|
}
|
|
|
|
// Add software
|
|
var software []fleet.Software
|
|
for _, t := range testCases {
|
|
software = append(software, fleet.Software{
|
|
Name: t.name,
|
|
Version: "0.0.1",
|
|
Source: "apps",
|
|
})
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
|
|
// Add vulnerabilities and CVEMeta
|
|
var meta []fleet.CVEMeta
|
|
for _, tC := range testCases {
|
|
idx := -1
|
|
for i, s := range host.Software {
|
|
if s.Name == tC.name {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
require.NotEqual(t, -1, idx, "software not found")
|
|
|
|
if tC.hasVuln {
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
|
|
SoftwareID: host.Software[idx].ID,
|
|
CVE: fmt.Sprintf("cve-%s", tC.name),
|
|
}, fleet.UbuntuOVALSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
}
|
|
|
|
if tC.hasMeta {
|
|
var published *time.Time
|
|
if tC.hasPublishedDate {
|
|
published = &now
|
|
}
|
|
|
|
meta = append(meta, fleet.CVEMeta{
|
|
CVE: fmt.Sprintf("cve-%s", tC.name),
|
|
CVSSScore: ptr.Float64(5.4),
|
|
EPSSProbability: ptr.Float64(0.5),
|
|
CISAKnownExploit: ptr.Bool(true),
|
|
Published: published,
|
|
})
|
|
}
|
|
}
|
|
require.NoError(t, ds.InsertCVEMeta(ctx, meta))
|
|
|
|
for _, tC := range testCases {
|
|
idx := -1
|
|
for i, s := range host.Software {
|
|
if s.Name == tC.name {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
require.NotEqual(t, -1, idx, "software not found")
|
|
|
|
for _, teamID := range []*uint{nil, &team1.ID} {
|
|
// Test that scores are not included if includeCVEScores = false
|
|
withoutScores, err := ds.SoftwareByID(ctx, host.Software[idx].ID, teamID, false, nil)
|
|
require.NoError(t, err)
|
|
if tC.hasVuln {
|
|
require.Len(t, withoutScores.Vulnerabilities, 1)
|
|
require.Equal(t, fmt.Sprintf("cve-%s", tC.name), withoutScores.Vulnerabilities[0].CVE)
|
|
|
|
require.Nil(t, withoutScores.Vulnerabilities[0].CVSSScore)
|
|
require.Nil(t, withoutScores.Vulnerabilities[0].EPSSProbability)
|
|
require.Nil(t, withoutScores.Vulnerabilities[0].CISAKnownExploit)
|
|
} else {
|
|
require.Empty(t, withoutScores.Vulnerabilities)
|
|
}
|
|
|
|
withScores, err := ds.SoftwareByID(ctx, host.Software[idx].ID, teamID, true, nil)
|
|
require.NoError(t, err)
|
|
if tC.hasVuln {
|
|
require.Len(t, withScores.Vulnerabilities, 1)
|
|
require.Equal(t, fmt.Sprintf("cve-%s", tC.name), withoutScores.Vulnerabilities[0].CVE)
|
|
|
|
if tC.hasMeta {
|
|
require.NotNil(t, withScores.Vulnerabilities[0].CVSSScore)
|
|
require.NotNil(t, *withScores.Vulnerabilities[0].CVSSScore)
|
|
require.Equal(t, **withScores.Vulnerabilities[0].CVSSScore, 5.4)
|
|
|
|
require.NotNil(t, withScores.Vulnerabilities[0].EPSSProbability)
|
|
require.NotNil(t, *withScores.Vulnerabilities[0].EPSSProbability)
|
|
require.Equal(t, **withScores.Vulnerabilities[0].EPSSProbability, 0.5)
|
|
|
|
require.NotNil(t, withScores.Vulnerabilities[0].CISAKnownExploit)
|
|
require.NotNil(t, *withScores.Vulnerabilities[0].CISAKnownExploit)
|
|
require.Equal(t, **withScores.Vulnerabilities[0].CISAKnownExploit, true)
|
|
|
|
if tC.hasPublishedDate {
|
|
require.NotNil(t, withScores.Vulnerabilities[0].CVEPublished)
|
|
require.NotNil(t, *withScores.Vulnerabilities[0].CVEPublished)
|
|
require.Equal(t, (**withScores.Vulnerabilities[0].CVEPublished), now)
|
|
}
|
|
}
|
|
} else {
|
|
require.Empty(t, withoutScores.Vulnerabilities)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func testAllSoftwareIterator(t *testing.T, ds *Datastore) {
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "foobar", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "v0.0.2", Source: "apps"},
|
|
{Name: "foo", Version: "0.0.3", Source: "apps"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
|
{Name: "baz", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(context.Background(), host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
|
|
|
|
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.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"
|
|
})
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[foo_ce_v1].ID, CPE: "cpe:foo_ce_v1"},
|
|
{SoftwareID: host.Software[foo_app_v2].ID, CPE: "cpe:foo_app_v2"},
|
|
{SoftwareID: host.Software[bar_v3].ID, CPE: "cpe:bar_v3"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(context.Background(), cpes)
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
q fleet.SoftwareIterQueryOptions
|
|
expected []fleet.Software
|
|
}{
|
|
{
|
|
name: "include apps source",
|
|
expected: []fleet.Software{
|
|
{Name: "foo", Version: "v0.0.2", Source: "apps", GenerateCPE: "cpe:foo_app_v2"},
|
|
{Name: "foo", Version: "0.0.3", Source: "apps"},
|
|
},
|
|
q: fleet.SoftwareIterQueryOptions{IncludedSources: []string{"apps"}},
|
|
},
|
|
{
|
|
name: "exclude apps source",
|
|
expected: []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions", GenerateCPE: "cpe:foo_ce_v1"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "foobar", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages", GenerateCPE: "cpe:bar_v3"},
|
|
{Name: "baz", Version: "0.0.3", Source: "deb_packages"},
|
|
},
|
|
q: fleet.SoftwareIterQueryOptions{ExcludedSources: []string{"apps"}},
|
|
},
|
|
{
|
|
name: "no filter",
|
|
expected: []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions", GenerateCPE: "cpe:foo_ce_v1"},
|
|
{Name: "foo", Version: "v0.0.2", Source: "apps", GenerateCPE: "cpe:foo_app_v2"},
|
|
{Name: "foo", Version: "0.0.3", Source: "apps"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "foobar", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "baz", Version: "0.0.3", Source: "deb_packages"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages", GenerateCPE: "cpe:bar_v3"},
|
|
},
|
|
q: fleet.SoftwareIterQueryOptions{},
|
|
},
|
|
{
|
|
name: "partial name filter includes deb_packages",
|
|
expected: []fleet.Software{
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages", GenerateCPE: "cpe:bar_v3"},
|
|
},
|
|
q: fleet.SoftwareIterQueryOptions{NameMatch: `ba[r|f]`, IncludedSources: []string{"deb_packages"}},
|
|
},
|
|
{
|
|
name: "name filter includes chrome_extensions",
|
|
expected: []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions", GenerateCPE: "cpe:foo_ce_v1"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "foobar", Version: "0.0.1", Source: "chrome_extensions"},
|
|
},
|
|
q: fleet.SoftwareIterQueryOptions{NameMatch: "foo\\.*", IncludedSources: []string{"chrome_extensions"}},
|
|
},
|
|
{
|
|
name: "name filter and not name filter",
|
|
expected: []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions", GenerateCPE: "cpe:foo_ce_v1"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
},
|
|
q: fleet.SoftwareIterQueryOptions{NameMatch: "foo\\.*", NameExclude: "bar$", IncludedSources: []string{"chrome_extensions"}},
|
|
},
|
|
}
|
|
|
|
for _, tC := range testCases {
|
|
t.Run(tC.name, func(t *testing.T) {
|
|
var actual []fleet.Software
|
|
|
|
iter, err := ds.AllSoftwareIterator(context.Background(), tC.q)
|
|
require.NoError(t, err)
|
|
for iter.Next() {
|
|
software, err := iter.Value()
|
|
require.NoError(t, err)
|
|
actual = append(actual, *software)
|
|
}
|
|
iter.Close()
|
|
test.ElementsMatchSkipID(t, tC.expected, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSoftwareIteratorForLinuxKernelCustomImages(t *testing.T, ds *Datastore) {
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "linux-image-5.4.0-42-generic", Version: "5.4.0-42.46", Source: "deb_packages"},
|
|
{Name: "linux-image-6.5.0-42-generic", Version: "6.5.0-100.27", Source: "deb_packages"},
|
|
{Name: "linux-image-5.4.0-42-custom", Version: "5.4.0-42.46", Source: "deb_packages"},
|
|
{Name: "linux-image-6.5.0-42-1234-foo", Version: "6.5.0-100.27", Source: "deb_packages"},
|
|
{Name: "linux-image-generic", Version: "1.0.0", Source: "deb_packages"},
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(context.Background(), host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(context.Background(), host, false))
|
|
|
|
expected := []fleet.Software{
|
|
{Name: "linux-image-5.4.0-42-custom", Version: "5.4.0-42.46", Source: "deb_packages"},
|
|
{Name: "linux-image-6.5.0-42-1234-foo", Version: "6.5.0-100.27", Source: "deb_packages"},
|
|
}
|
|
|
|
opts := fleet.SoftwareIterQueryOptions{
|
|
NameMatch: nvd.LinuxImageRegex,
|
|
NameExclude: nvd.BuildLinuxExclusionRegex(),
|
|
IncludedSources: []string{"deb_packages"},
|
|
}
|
|
|
|
iterator, err := ds.AllSoftwareIterator(context.Background(), opts)
|
|
require.NoError(t, err)
|
|
|
|
var actual []fleet.Software
|
|
for iterator.Next() {
|
|
software, err := iterator.Value()
|
|
require.NoError(t, err)
|
|
actual = append(actual, *software)
|
|
}
|
|
iterator.Close()
|
|
test.ElementsMatchSkipID(t, expected, actual)
|
|
}
|
|
|
|
func testUpsertSoftwareCPEs(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "cpe:foo_ce_v1"},
|
|
{SoftwareID: host.Software[0].ID, CPE: "cpe:foo_ce_v2"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
cpes, err = ds.ListSoftwareCPEs(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(cpes), 1)
|
|
require.Equal(t, cpes[0].CPE, "cpe:foo_ce_v2")
|
|
|
|
cpes = []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "cpe:foo_ce_v3"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
cpes = []fleet.SoftwareCPE{
|
|
{SoftwareID: host.Software[0].ID, CPE: "cpe:foo_ce_v4"},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
cpes, err = ds.ListSoftwareCPEs(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(cpes), 1)
|
|
require.Equal(t, cpes[0].CPE, "cpe:foo_ce_v4")
|
|
}
|
|
|
|
func testDeleteOutOfDateVulnerabilities(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
vulns := []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: host.Software[0].ID,
|
|
CVE: "CVE-2023-001",
|
|
},
|
|
{
|
|
SoftwareID: host.Software[0].ID,
|
|
CVE: "CVE-2023-002",
|
|
},
|
|
}
|
|
|
|
inserted, err := ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
|
|
inserted, err = ds.InsertSoftwareVulnerability(ctx, vulns[1], fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
require.True(t, inserted)
|
|
|
|
_, err = ds.writer(ctx).ExecContext(ctx, "UPDATE software_cve SET updated_at = '2020-10-10 12:00:00'")
|
|
require.NoError(t, err)
|
|
|
|
// This should update the 'updated_at' timestamp.
|
|
inserted, err = ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
require.False(t, inserted)
|
|
|
|
err = ds.DeleteOutOfDateVulnerabilities(ctx, fleet.NVDSource, 2*time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
storedSoftware, err := ds.SoftwareByID(ctx, host.Software[0].ID, nil, false, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(storedSoftware.Vulnerabilities))
|
|
require.Equal(t, "CVE-2023-001", storedSoftware.Vulnerabilities[0].CVE)
|
|
}
|
|
|
|
func testDeleteSoftwareCPEs(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.1", Source: "chrome_extensions"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
cpes := []fleet.SoftwareCPE{
|
|
{
|
|
SoftwareID: host.Software[0].ID,
|
|
CPE: "CPE-001",
|
|
},
|
|
{
|
|
SoftwareID: host.Software[1].ID,
|
|
CPE: "CPE-002",
|
|
},
|
|
}
|
|
_, err = ds.UpsertSoftwareCPEs(ctx, cpes)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("nothing to delete", func(t *testing.T) {
|
|
affected, err := ds.DeleteSoftwareCPEs(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.Zero(t, affected)
|
|
})
|
|
|
|
t.Run("with invalid software id", func(t *testing.T) {
|
|
toDelete := []fleet.SoftwareCPE{cpes[0], {
|
|
SoftwareID: host.Software[1].ID + 1234,
|
|
CPE: "CPE-002",
|
|
}}
|
|
|
|
affected, err := ds.DeleteSoftwareCPEs(ctx, toDelete)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), affected)
|
|
|
|
storedCPEs, err := ds.ListSoftwareCPEs(ctx)
|
|
require.NoError(t, err)
|
|
test.ElementsMatchSkipID(t, cpes[1:], storedCPEs)
|
|
|
|
storedSoftware, err := ds.SoftwareByID(ctx, cpes[0].SoftwareID, nil, false, nil)
|
|
require.NoError(t, err)
|
|
require.Empty(t, storedSoftware.GenerateCPE)
|
|
})
|
|
}
|
|
|
|
func testGetHostSoftwareInstalledPaths(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.1", Source: "chrome_extensions"},
|
|
}
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
|
|
// No installed_path entries
|
|
actual, err := ds.getHostSoftwareInstalledPaths(ctx, host.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, actual)
|
|
|
|
// Insert an installed_path for a single software entry
|
|
query := `INSERT INTO host_software_installed_paths (host_id, software_id, installed_path) VALUES (?, ?, ?)`
|
|
args := []interface{}{host.ID, host.Software[0].ID, "/some/path"}
|
|
_, err = ds.writer(ctx).ExecContext(ctx, query, args...)
|
|
require.NoError(t, err)
|
|
|
|
actual, err = ds.getHostSoftwareInstalledPaths(ctx, host.ID)
|
|
require.Len(t, actual, 1)
|
|
require.Equal(t, actual[0].SoftwareID, host.Software[0].ID)
|
|
require.Equal(t, actual[0].HostID, host.ID)
|
|
require.Equal(t, actual[0].InstalledPath, "/some/path")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testHostSoftwareInstalledPathsDelta(t *testing.T, ds *Datastore) {
|
|
host := fleet.Host{ID: 1}
|
|
|
|
software := []fleet.Software{
|
|
{
|
|
ID: 2,
|
|
Name: "foo",
|
|
Version: "0.0.1",
|
|
Source: "chrome_extensions",
|
|
},
|
|
{
|
|
ID: 3,
|
|
Name: "bar",
|
|
Version: "0.0.2",
|
|
Source: "chrome_extensions",
|
|
},
|
|
{
|
|
ID: 4,
|
|
Name: "zub",
|
|
Version: "0.0.3",
|
|
Source: "chrome_extensions",
|
|
},
|
|
{
|
|
ID: 5,
|
|
Name: "zib",
|
|
Version: "0.0.4",
|
|
Source: "chrome_extensions",
|
|
},
|
|
}
|
|
|
|
t.Run("empty args", func(t *testing.T) {
|
|
toI, toD, err := hostSoftwareInstalledPathsDelta(host.ID, nil, nil, nil)
|
|
require.Empty(t, toI)
|
|
require.Empty(t, toD)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("nothing reported from osquery", func(t *testing.T) {
|
|
var stored []fleet.HostSoftwareInstalledPath
|
|
for i, s := range software {
|
|
stored = append(stored, fleet.HostSoftwareInstalledPath{
|
|
ID: uint(i),
|
|
HostID: host.ID,
|
|
SoftwareID: s.ID,
|
|
InstalledPath: fmt.Sprintf("/some/path/%d", s.ID),
|
|
})
|
|
}
|
|
|
|
toI, toD, err := hostSoftwareInstalledPathsDelta(host.ID, nil, stored, software)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, toI)
|
|
|
|
// Kind of an edge case ... but if nothing is reported by osquery we want the state of the
|
|
// DB to reflect that.
|
|
require.Len(t, toD, len(stored))
|
|
var expected []uint
|
|
for _, s := range stored {
|
|
expected = append(expected, s.ID)
|
|
}
|
|
require.ElementsMatch(t, toD, expected)
|
|
})
|
|
|
|
t.Run("host has no software but some paths were reported", func(t *testing.T) {
|
|
reported := make(map[string]struct{})
|
|
reported[fmt.Sprintf("/some/path/%d%s%s", software[0].ID, fleet.SoftwareFieldSeparator, software[0].ToUniqueStr())] = struct{}{}
|
|
reported[fmt.Sprintf("/some/path/%d%s%s", software[1].ID+1, fleet.SoftwareFieldSeparator, software[1].ToUniqueStr())] = struct{}{}
|
|
reported[fmt.Sprintf("/some/path/%d%s%s", software[2].ID, fleet.SoftwareFieldSeparator, software[2].ToUniqueStr())] = struct{}{}
|
|
|
|
var stored []fleet.HostSoftwareInstalledPath
|
|
_, _, err := hostSoftwareInstalledPathsDelta(host.ID, reported, stored, nil)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("we have some deltas", func(t *testing.T) {
|
|
getKey := func(s fleet.Software, change uint) string {
|
|
return fmt.Sprintf("/some/path/%d%s%s", s.ID+change, fleet.SoftwareFieldSeparator, s.ToUniqueStr())
|
|
}
|
|
reported := make(map[string]struct{})
|
|
reported[getKey(software[0], 0)] = struct{}{}
|
|
reported[getKey(software[1], 1)] = struct{}{}
|
|
reported[getKey(software[2], 0)] = struct{}{}
|
|
|
|
var stored []fleet.HostSoftwareInstalledPath
|
|
stored = append(stored, fleet.HostSoftwareInstalledPath{
|
|
ID: 1,
|
|
HostID: host.ID,
|
|
SoftwareID: software[0].ID,
|
|
InstalledPath: fmt.Sprintf("/some/path/%d", software[0].ID),
|
|
})
|
|
stored = append(stored, fleet.HostSoftwareInstalledPath{
|
|
ID: 2,
|
|
HostID: host.ID,
|
|
SoftwareID: software[1].ID,
|
|
InstalledPath: fmt.Sprintf("/some/path/%d", software[1].ID),
|
|
})
|
|
stored = append(stored, fleet.HostSoftwareInstalledPath{
|
|
ID: 3,
|
|
HostID: host.ID,
|
|
SoftwareID: software[2].ID,
|
|
InstalledPath: fmt.Sprintf("/some/path/%d", software[2].ID+1),
|
|
})
|
|
stored = append(stored, fleet.HostSoftwareInstalledPath{
|
|
ID: 4,
|
|
HostID: host.ID,
|
|
SoftwareID: software[3].ID,
|
|
InstalledPath: fmt.Sprintf("/some/path/%d", software[3].ID),
|
|
})
|
|
|
|
toI, toD, err := hostSoftwareInstalledPathsDelta(host.ID, reported, stored, software)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, toD, 3)
|
|
require.ElementsMatch(t,
|
|
[]uint{toD[0], toD[1], toD[2]},
|
|
[]uint{stored[1].ID, stored[2].ID, stored[3].ID},
|
|
)
|
|
|
|
require.Len(t, toI, 2)
|
|
for i := range toI {
|
|
require.Equal(t, toI[i].HostID, host.ID)
|
|
}
|
|
|
|
require.ElementsMatch(t,
|
|
[]uint{toI[0].SoftwareID, toI[1].SoftwareID},
|
|
[]uint{software[1].ID, software[2].ID},
|
|
)
|
|
require.ElementsMatch(t,
|
|
[]string{toI[0].InstalledPath, toI[1].InstalledPath},
|
|
[]string{fmt.Sprintf("/some/path/%d", software[1].ID+1), fmt.Sprintf("/some/path/%d", software[2].ID)},
|
|
)
|
|
})
|
|
}
|
|
|
|
func testDeleteHostSoftwareInstalledPaths(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
host1 := fleet.Host{ID: 1}
|
|
host2 := fleet.Host{ID: 2}
|
|
|
|
software1 := []fleet.Software{
|
|
{ID: 1, Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{ID: 2, Name: "bar", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{ID: 3, Name: "zoo", Version: "0.0.1", Source: "chrome_extensions"},
|
|
}
|
|
software2 := []fleet.Software{
|
|
{ID: 4, Name: "zip", Version: "0.0.1", Source: "apps"},
|
|
{ID: 5, Name: "bur", Version: "0.0.1", Source: "apps"},
|
|
}
|
|
|
|
query := `INSERT INTO host_software_installed_paths (host_id, software_id, installed_path) VALUES (?, ?, ?)`
|
|
for _, s := range software1 {
|
|
args := []interface{}{host1.ID, s.ID, fmt.Sprintf("/some/path/%d", s.ID)}
|
|
_, err := ds.writer(ctx).ExecContext(ctx, query, args...)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
args := []interface{}{host2.ID, software2[0].ID, fmt.Sprintf("/some/path/%d", software2[0].ID)}
|
|
_, err := ds.writer(ctx).ExecContext(ctx, query, args...)
|
|
require.NoError(t, err)
|
|
|
|
storedOnHost1, err := ds.getHostSoftwareInstalledPaths(ctx, host1.ID)
|
|
require.NoError(t, err)
|
|
|
|
storedOnHost2, err := ds.getHostSoftwareInstalledPaths(ctx, host2.ID)
|
|
require.NoError(t, err)
|
|
|
|
var toDelete []uint
|
|
for _, r := range storedOnHost1 {
|
|
if r.SoftwareID == software1[0].ID || r.SoftwareID == software1[1].ID {
|
|
toDelete = append(toDelete, r.ID)
|
|
}
|
|
}
|
|
|
|
for _, r := range storedOnHost2 {
|
|
if r.SoftwareID == software2[0].ID {
|
|
toDelete = append(toDelete, r.ID)
|
|
}
|
|
}
|
|
|
|
require.NoError(t, deleteHostSoftwareInstalledPaths(ctx, ds.writer(ctx), toDelete))
|
|
|
|
var actual []fleet.HostSoftwareInstalledPath
|
|
require.NoError(t, sqlx.SelectContext(ctx, ds.reader(ctx), &actual, `SELECT host_id, software_id, installed_path FROM host_software_installed_paths`))
|
|
|
|
expected := []fleet.HostSoftwareInstalledPath{
|
|
{
|
|
HostID: host1.ID,
|
|
SoftwareID: software1[2].ID,
|
|
InstalledPath: fmt.Sprintf("/some/path/%d", software1[2].ID),
|
|
},
|
|
}
|
|
|
|
test.ElementsMatchSkipID(t, actual, expected)
|
|
}
|
|
|
|
func testInsertHostSoftwareInstalledPaths(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
toInsert := []fleet.HostSoftwareInstalledPath{
|
|
{
|
|
HostID: 1,
|
|
SoftwareID: 1,
|
|
InstalledPath: "1",
|
|
},
|
|
{
|
|
HostID: 1,
|
|
SoftwareID: 2,
|
|
InstalledPath: "2",
|
|
},
|
|
{
|
|
HostID: 1,
|
|
SoftwareID: 3,
|
|
InstalledPath: "3",
|
|
},
|
|
}
|
|
require.NoError(t, insertHostSoftwareInstalledPaths(ctx, ds.writer(ctx), toInsert))
|
|
|
|
var actual []fleet.HostSoftwareInstalledPath
|
|
require.NoError(t, sqlx.SelectContext(ctx, ds.reader(ctx), &actual, `SELECT host_id, software_id, installed_path FROM host_software_installed_paths`))
|
|
|
|
require.ElementsMatch(t, actual, toInsert)
|
|
}
|
|
|
|
func TestReconcileSoftwareTitles(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
ctx := context.Background()
|
|
|
|
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
|
host3 := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
|
|
|
expectedSoftware := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions", Browser: "chrome"},
|
|
{Name: "foo", Version: "v0.0.2", Source: "chrome_extensions"},
|
|
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions"},
|
|
{Name: "bar", Version: "0.0.3", Source: "deb_packages"},
|
|
{Name: "baz", Version: "0.0.1", Source: "deb_packages"},
|
|
}
|
|
expectedTitlesByNSB := map[string]fleet.SoftwareTitle{}
|
|
for _, s := range expectedSoftware {
|
|
expectedTitlesByNSB[s.Name+s.Source+s.Browser] = fleet.SoftwareTitle{
|
|
Name: s.Name,
|
|
Source: s.Source,
|
|
Browser: s.Browser,
|
|
}
|
|
}
|
|
|
|
software1 := []fleet.Software{expectedSoftware[0], expectedSoftware[2]}
|
|
software2 := []fleet.Software{expectedSoftware[1], expectedSoftware[2], expectedSoftware[3]}
|
|
software3 := []fleet.Software{expectedSoftware[4]}
|
|
|
|
_, err := ds.UpdateHostSoftware(ctx, host1.ID, software1)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(ctx, host2.ID, software2)
|
|
require.NoError(t, err)
|
|
_, err = ds.UpdateHostSoftware(ctx, host3.ID, software3)
|
|
require.NoError(t, err)
|
|
|
|
getSoftware := func() ([]fleet.Software, error) {
|
|
var sw []fleet.Software
|
|
err := ds.writer(ctx).SelectContext(ctx, &sw, `SELECT
|
|
id, name, version, bundle_identifier, source, extension_id, browser, `+"`release`"+`, vendor, arch, title_id
|
|
FROM software ORDER BY name, source, browser, version`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sw, nil
|
|
}
|
|
|
|
getTitles := func() ([]fleet.SoftwareTitle, error) {
|
|
var swt []fleet.SoftwareTitle
|
|
err := ds.writer(ctx).SelectContext(ctx, &swt, `SELECT id, name, source, browser FROM software_titles ORDER BY name, source, browser`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return swt, nil
|
|
}
|
|
|
|
assertSoftware := func(t *testing.T, wantSoftware []fleet.Software) {
|
|
gotSoftware, err := getSoftware()
|
|
require.NoError(t, err)
|
|
require.Len(t, gotSoftware, len(wantSoftware))
|
|
|
|
byNSBV := map[string]fleet.Software{}
|
|
for _, s := range wantSoftware {
|
|
byNSBV[s.Name+s.Source+s.Browser+s.Version] = s
|
|
}
|
|
|
|
for _, r := range gotSoftware {
|
|
_, ok := byNSBV[r.Name+r.Source+r.Browser+r.Version]
|
|
require.True(t, ok)
|
|
|
|
assert.NotNil(t, r.TitleID)
|
|
swt, ok := expectedTitlesByNSB[r.Name+r.Source+r.Browser]
|
|
require.True(t, ok)
|
|
assert.Equal(t, swt.ID, *r.TitleID)
|
|
assert.Equal(t, swt.Name, r.Name)
|
|
assert.Equal(t, swt.Source, r.Source)
|
|
assert.Equal(t, swt.Browser, r.Browser)
|
|
}
|
|
}
|
|
|
|
assertTitles := func(t *testing.T, gotTitles []fleet.SoftwareTitle, expectMissing []string) {
|
|
for _, r := range gotTitles {
|
|
if len(expectMissing) > 0 {
|
|
require.NotContains(t, expectMissing, r.Name)
|
|
}
|
|
e, ok := expectedTitlesByNSB[r.Name+r.Source+r.Browser]
|
|
require.True(t, ok)
|
|
require.Equal(t, e.ID, r.ID)
|
|
require.Equal(t, e.Name, r.Name)
|
|
require.Equal(t, e.Source, r.Source)
|
|
require.Equal(t, e.Browser, r.Browser)
|
|
}
|
|
}
|
|
|
|
swTitles, err := getTitles()
|
|
require.NoError(t, err)
|
|
for _, swt := range swTitles {
|
|
if _, ok := expectedTitlesByNSB[swt.Name+swt.Source+swt.Browser]; ok {
|
|
expectedTitlesByNSB[swt.Name+swt.Source+swt.Browser] = swt
|
|
}
|
|
}
|
|
|
|
assertSoftware(t, expectedSoftware)
|
|
|
|
// reconcile software titles
|
|
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
|
|
swt, err := getTitles()
|
|
require.NoError(t, err)
|
|
require.Len(t, swt, 4)
|
|
|
|
require.Equal(t, swt[0].Name, "bar")
|
|
require.Equal(t, swt[0].Source, "deb_packages")
|
|
require.Equal(t, swt[0].Browser, "")
|
|
expectedTitlesByNSB[swt[0].Name+swt[0].Source+swt[0].Browser] = swt[0]
|
|
|
|
require.Equal(t, swt[1].Name, "baz")
|
|
require.Equal(t, swt[1].Source, "deb_packages")
|
|
require.Equal(t, swt[1].Browser, "")
|
|
expectedTitlesByNSB[swt[1].Name+swt[1].Source+swt[1].Browser] = swt[1]
|
|
|
|
require.Equal(t, swt[2].Name, "foo")
|
|
require.Equal(t, swt[2].Source, "chrome_extensions")
|
|
require.Equal(t, swt[2].Browser, "")
|
|
expectedTitlesByNSB[swt[2].Name+swt[2].Source+swt[2].Browser] = swt[2]
|
|
|
|
require.Equal(t, swt[3].Name, "foo")
|
|
require.Equal(t, swt[3].Source, "chrome_extensions")
|
|
require.Equal(t, swt[3].Browser, "chrome")
|
|
expectedTitlesByNSB[swt[3].Name+swt[3].Source+swt[3].Browser] = swt[3]
|
|
|
|
// Double check software and titles
|
|
assertSoftware(t, expectedSoftware)
|
|
|
|
// remove the bar software title from host 2
|
|
_, err = ds.UpdateHostSoftware(context.Background(), host2.ID, software2[:2])
|
|
require.NoError(t, err)
|
|
// SyncHostsSoftware will remove the above software item from the software table
|
|
require.NoError(t, ds.SyncHostsSoftware(context.Background(), time.Now()))
|
|
assertSoftware(t, []fleet.Software{expectedSoftware[0], expectedSoftware[1], expectedSoftware[2], expectedSoftware[4]})
|
|
|
|
// bar is no longer associated with any host so the title should be deleted
|
|
require.NoError(t, ds.ReconcileSoftwareTitles(context.Background()))
|
|
gotTitles, err := getTitles()
|
|
require.NoError(t, err)
|
|
require.Len(t, gotTitles, 3)
|
|
assertTitles(t, gotTitles, []string{"bar"})
|
|
|
|
// add bar to host 3
|
|
_, err = ds.UpdateHostSoftware(context.Background(), host3.ID, []fleet.Software{expectedSoftware[3], expectedSoftware[4]})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.SyncHostsSoftware(context.Background(), time.Now()))
|
|
gotTitles, err = getTitles()
|
|
require.NoError(t, err)
|
|
require.Len(t, gotTitles, 4)
|
|
|
|
// bar was added back to software titles with a new ID
|
|
require.Equal(t, "bar", gotTitles[0].Name)
|
|
require.Equal(t, "deb_packages", gotTitles[0].Source)
|
|
require.NotEqual(t, expectedTitlesByNSB[gotTitles[0].Name+gotTitles[0].Source], gotTitles[0].ID)
|
|
expectedTitlesByNSB[gotTitles[0].Name+gotTitles[0].Source] = gotTitles[0]
|
|
assertTitles(t, gotTitles, nil)
|
|
assertSoftware(t, expectedSoftware)
|
|
|
|
// add a new version of foo to host 3
|
|
expectedSoftware = append(expectedSoftware, fleet.Software{Name: "foo", Version: "0.0.4", Source: "chrome_extensions"})
|
|
_, err = ds.UpdateHostSoftware(ctx, host3.ID, expectedSoftware[3:])
|
|
require.NoError(t, err)
|
|
gotTitles, err = getTitles()
|
|
require.NoError(t, err)
|
|
require.Len(t, gotTitles, 4)
|
|
assertTitles(t, gotTitles, nil)
|
|
assertSoftware(t, expectedSoftware)
|
|
|
|
// add a new source of foo to host 3
|
|
expectedSoftware = append(expectedSoftware, fleet.Software{Name: "foo", Version: "0.0.4", Source: "rpm_packages"})
|
|
_, err = ds.UpdateHostSoftware(ctx, host3.ID, expectedSoftware[3:])
|
|
require.NoError(t, err)
|
|
|
|
// new source of foo results in a new software title entry
|
|
gotTitles, err = getTitles()
|
|
require.NoError(t, err)
|
|
require.Len(t, gotTitles, 5)
|
|
require.Equal(t, "foo", gotTitles[4].Name)
|
|
require.Equal(t, "rpm_packages", gotTitles[4].Source)
|
|
require.Equal(t, "", gotTitles[4].Browser)
|
|
expectedTitlesByNSB[gotTitles[4].Name+gotTitles[4].Source+gotTitles[4].Browser] = gotTitles[4]
|
|
assertTitles(t, gotTitles, nil)
|
|
assertSoftware(t, expectedSoftware)
|
|
}
|
|
|
|
func testUpdateHostSoftwareDeadlock(t *testing.T, ds *Datastore) {
|
|
// To increase chance of deadlock increase these numbers.
|
|
// We are keeping them low to not cause CI issues ("too many connections" errors
|
|
// due to concurrent tests).
|
|
const (
|
|
hostCount = 10
|
|
updateCount = 10
|
|
)
|
|
ctx := context.Background()
|
|
var hosts []*fleet.Host
|
|
for i := 1; i <= hostCount; i++ {
|
|
h, err := ds.NewHost(ctx, &fleet.Host{
|
|
ID: uint(i),
|
|
OsqueryHostID: ptr.String(fmt.Sprintf("id-%d", i)),
|
|
NodeKey: ptr.String(fmt.Sprintf("key-%d", i)),
|
|
Platform: "linux",
|
|
Hostname: fmt.Sprintf("host-%d", i),
|
|
DetailUpdatedAt: time.Now(),
|
|
LabelUpdatedAt: time.Now(),
|
|
PolicyUpdatedAt: time.Now(),
|
|
SeenTime: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
hosts = append(hosts, h)
|
|
}
|
|
var g errgroup.Group
|
|
for _, h := range hosts {
|
|
hostID := h.ID
|
|
g.Go(func() error {
|
|
for i := 0; i < updateCount; i++ {
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "test", GenerateCPE: "cpe_foo"},
|
|
{Name: "bar", Version: "0.0.2", Source: "test", GenerateCPE: "cpe_bar"},
|
|
{Name: "baz", Version: "0.0.3", Source: "test", GenerateCPE: "cpe_baz"},
|
|
}
|
|
removeIdx := rand.Intn(len(software))
|
|
software = append(software[:removeIdx], software[removeIdx+1:]...)
|
|
if _, err := ds.UpdateHostSoftware(ctx, hostID, software); err != nil {
|
|
return err
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
err := g.Wait()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testVerifySoftwareChecksum(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
software := []fleet.Software{
|
|
{Name: "foo", Version: "0.0.1", Source: "test"},
|
|
{Name: "foo", Version: "0.0.1", Source: "test", Browser: "firefox"},
|
|
{Name: "foo", Version: "0.0.1", Source: "test", ExtensionID: "ext"},
|
|
{Name: "foo", Version: "0.0.2", Source: "test"},
|
|
}
|
|
|
|
_, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
|
|
checksums := make([]string, len(software))
|
|
for i, sw := range software {
|
|
checksum, err := computeRawChecksum(sw)
|
|
require.NoError(t, err)
|
|
checksums[i] = hex.EncodeToString(checksum)
|
|
}
|
|
for i, cs := range checksums {
|
|
var got fleet.Software
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &got,
|
|
`SELECT name, version, source, bundle_identifier, `+"`release`"+`, arch, vendor, browser, extension_id FROM software WHERE checksum = UNHEX(?)`, cs)
|
|
})
|
|
require.Equal(t, software[i], got)
|
|
}
|
|
}
|
|
|
|
func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
|
|
nanoEnroll(t, ds, host, false)
|
|
otherHost := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now(), test.WithPlatform("linux"))
|
|
opts := fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
|
|
|
|
user, err := ds.NewUser(ctx, &fleet.User{
|
|
Password: []byte("p4ssw0rd.123"),
|
|
Name: "user1",
|
|
Email: "user1@example.com",
|
|
GlobalRole: ptr.String(fleet.RoleAdmin),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
expectStatus := func(s fleet.SoftwareInstallerStatus) *fleet.SoftwareInstallerStatus {
|
|
return &s
|
|
}
|
|
|
|
// no software yet
|
|
sw, meta, err := ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Empty(t, sw)
|
|
require.Equal(t, &fleet.PaginationMetadata{}, meta)
|
|
|
|
// works with available software too
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Empty(t, sw)
|
|
require.Equal(t, &fleet.PaginationMetadata{}, meta)
|
|
|
|
// self-service only works too
|
|
opts.SelfServiceOnly = true
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Empty(t, sw)
|
|
require.Equal(t, &fleet.PaginationMetadata{}, meta)
|
|
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Empty(t, sw)
|
|
require.Equal(t, &fleet.PaginationMetadata{}, meta)
|
|
|
|
// add software to the host
|
|
software := []fleet.Software{
|
|
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
|
|
{Name: "a", Version: "0.0.2", Source: "deb_packages"}, // different source, so different title than a-chrome
|
|
{Name: "b", Version: "0.0.3", Source: "apps"},
|
|
{Name: "c", Version: "0.0.4", Source: "deb_packages"},
|
|
{Name: "c", Version: "0.0.5", Source: "deb_packages"},
|
|
{Name: "d", Version: "0.0.6", Source: "deb_packages"},
|
|
}
|
|
byNSV := map[string]fleet.Software{}
|
|
for _, s := range software {
|
|
byNSV[s.Name+s.Source+s.Version] = s
|
|
}
|
|
|
|
mutationResults, err := ds.UpdateHostSoftware(ctx, host.ID, software)
|
|
require.NoError(t, err)
|
|
require.Len(t, mutationResults.Inserted, len(software))
|
|
for _, m := range mutationResults.Inserted {
|
|
s, ok := byNSV[m.Name+m.Source+m.Version]
|
|
require.True(t, ok)
|
|
require.Equal(t, m.Name, s.Name, "name")
|
|
require.Equal(t, m.Version, s.Version, "version")
|
|
require.Equal(t, m.Source, s.Source, "source")
|
|
require.Zero(t, s.ID) // not set in the map yet
|
|
require.NotZero(t, m.ID)
|
|
s.ID = m.ID
|
|
byNSV[s.Name+s.Source+s.Version] = s
|
|
|
|
}
|
|
|
|
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
|
|
require.Equal(t, len(host.Software), len(software))
|
|
for _, hs := range host.Software {
|
|
s, ok := byNSV[hs.Name+hs.Source+hs.Version]
|
|
require.True(t, ok)
|
|
require.Equal(t, hs.Name, s.Name, "name")
|
|
require.Equal(t, hs.Version, s.Version, "version")
|
|
require.Equal(t, hs.Source, s.Source, "source")
|
|
require.Equal(t, hs.ID, s.ID)
|
|
}
|
|
|
|
// add other software to the other host, won't be returned
|
|
otherSoftware := []fleet.Software{
|
|
{Name: "a", Version: "0.0.7", Source: "chrome_extensions"},
|
|
{Name: "f", Version: "0.0.8", Source: "chrome_extensions"},
|
|
}
|
|
_, err = ds.UpdateHostSoftware(ctx, otherHost.ID, otherSoftware)
|
|
require.NoError(t, err)
|
|
|
|
// shorthand keys for expected software
|
|
a1 := software[0].Name + software[0].Source + software[0].Version
|
|
a2 := software[1].Name + software[1].Source + software[1].Version
|
|
b := software[2].Name + software[2].Source + software[2].Version
|
|
c1 := software[3].Name + software[3].Source + software[3].Version
|
|
c2 := software[4].Name + software[4].Source + software[4].Version
|
|
d := software[5].Name + software[5].Source + software[5].Version
|
|
|
|
// add some vulnerabilities and installed paths
|
|
vulns := []fleet.SoftwareVulnerability{
|
|
{SoftwareID: byNSV[a1].ID, CVE: "CVE-a-0001"},
|
|
{SoftwareID: byNSV[a1].ID, CVE: "CVE-a-0002"},
|
|
{SoftwareID: byNSV[a1].ID, CVE: "CVE-a-0003"},
|
|
{SoftwareID: byNSV[b].ID, CVE: "CVE-b-0001"},
|
|
}
|
|
for _, v := range vulns {
|
|
_, err = ds.InsertSoftwareVulnerability(ctx, v, fleet.NVDSource)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
swPaths := map[string]struct{}{}
|
|
installPaths := make([]string, 0, len(software))
|
|
for _, s := range software {
|
|
path := fmt.Sprintf("/some/path/%s", s.Name)
|
|
key := fmt.Sprintf("%s%s%s", path, fleet.SoftwareFieldSeparator, s.ToUniqueStr())
|
|
swPaths[key] = struct{}{}
|
|
installPaths = append(installPaths, path)
|
|
}
|
|
err = ds.UpdateHostSoftwareInstalledPaths(ctx, host.ID, swPaths, mutationResults)
|
|
require.NoError(t, err)
|
|
|
|
err = ds.ReconcileSoftwareTitles(ctx)
|
|
require.NoError(t, err)
|
|
|
|
expected := map[string]fleet.HostSoftwareWithInstaller{
|
|
byNSV[a1].Name + byNSV[a1].Source: {Name: byNSV[a1].Name, Source: byNSV[a1].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: byNSV[a1].Version, Vulnerabilities: []string{vulns[0].CVE, vulns[1].CVE, vulns[2].CVE}, InstalledPaths: []string{installPaths[0]}},
|
|
}},
|
|
// a1 and a2 are different software titles because they have different sources
|
|
byNSV[a2].Name + byNSV[a2].Source: {Name: byNSV[a2].Name, Source: byNSV[a2].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: byNSV[a2].Version, InstalledPaths: []string{installPaths[1]}},
|
|
}},
|
|
byNSV[b].Name + byNSV[b].Source: {Name: byNSV[b].Name, Source: byNSV[b].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}, InstalledPaths: []string{installPaths[2]}},
|
|
}},
|
|
// c1 and c2 are the same software title because they have the same name and source
|
|
byNSV[c1].Name + byNSV[c1].Source: {Name: byNSV[c1].Name, Source: byNSV[c1].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: byNSV[c1].Version, InstalledPaths: []string{installPaths[3]}},
|
|
{Version: byNSV[c2].Version, InstalledPaths: []string{installPaths[4]}},
|
|
}},
|
|
byNSV[d].Name + byNSV[d].Source: {Name: byNSV[d].Name, Source: byNSV[d].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: byNSV[d].Version, InstalledPaths: []string{installPaths[5]}},
|
|
}},
|
|
}
|
|
|
|
compareResults := func(expected map[string]fleet.HostSoftwareWithInstaller, got []*fleet.HostSoftwareWithInstaller, expectAsc bool, expectOmitted ...string) {
|
|
require.Len(t, got, len(expected)-len(expectOmitted))
|
|
prev := ""
|
|
for _, g := range got {
|
|
e, ok := expected[g.Name+g.Source]
|
|
require.True(t, ok)
|
|
require.Equal(t, e.Name, g.Name)
|
|
require.Equal(t, e.Source, g.Source)
|
|
require.Equal(t, e.SoftwarePackage, g.SoftwarePackage)
|
|
require.Equal(t, e.AppStoreApp, g.AppStoreApp)
|
|
require.Len(t, g.InstalledVersions, len(e.InstalledVersions))
|
|
if len(e.InstalledVersions) > 0 {
|
|
byVers := make(map[string]fleet.HostSoftwareInstalledVersion, len(e.InstalledVersions))
|
|
for _, v := range e.InstalledVersions {
|
|
byVers[v.Version] = *v
|
|
}
|
|
for _, v := range g.InstalledVersions {
|
|
ev, ok := byVers[v.Version]
|
|
require.True(t, ok)
|
|
require.Equal(t, ev.Version, v.Version)
|
|
require.ElementsMatch(t, ev.InstalledPaths, v.InstalledPaths)
|
|
require.ElementsMatch(t, ev.Vulnerabilities, v.Vulnerabilities)
|
|
}
|
|
}
|
|
if prev != "" {
|
|
if expectAsc {
|
|
require.Greater(t, g.Name+g.Source, prev)
|
|
} else {
|
|
require.Less(t, g.Name+g.Source, prev)
|
|
}
|
|
}
|
|
prev = g.Name + g.Source
|
|
}
|
|
}
|
|
|
|
// it now returns the software with vulnerabilities and installed paths
|
|
opts.SelfServiceOnly = false
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 5}, meta)
|
|
compareResults(expected, sw, true)
|
|
|
|
opts.VulnerableOnly = true
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 2}, meta)
|
|
compareResults(expected, sw, true, byNSV[a2].Name+byNSV[a2].Source, byNSV[c1].Name+byNSV[c1].Source, byNSV[d].Name+byNSV[d].Source)
|
|
opts.VulnerableOnly = false
|
|
|
|
// create some Fleet installers and map them to a software title,
|
|
// including one for a team
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
|
require.NoError(t, err)
|
|
|
|
var swi1Pending, swi2Installed, swi3Failed, swi4Available, swi5Tm uint
|
|
var otherHostI1UUID, otherHostI2UUID string
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
// keep title id of software B, will use it to associate an installer with it
|
|
var swbTitleID uint
|
|
err := sqlx.GetContext(ctx, q, &swbTitleID, `SELECT id FROM software_titles WHERE name = 'b' AND source = 'apps'`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create the install script content (same for all installers, doesn't matter)
|
|
installScript := `echo 'foo'`
|
|
res, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`, installScript, installScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scriptContentID, _ := res.LastInsertId()
|
|
|
|
// create software titles for all but swi1Pending (will be linked to
|
|
// existing software title b)
|
|
var titleIDs []uint
|
|
for i := 0; i < 4; i++ {
|
|
res, err := q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES (?, 'apps')`, fmt.Sprintf("i%d", i))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
titleIDs = append(titleIDs, uint(id))
|
|
}
|
|
|
|
var swiIDs []uint
|
|
for i := 0; i < 5; i++ {
|
|
var (
|
|
titleID uint
|
|
teamID *uint
|
|
globalOrTeamID uint
|
|
)
|
|
if i == 0 {
|
|
titleID = swbTitleID
|
|
} else {
|
|
titleID = titleIDs[i-1]
|
|
}
|
|
if i == 4 {
|
|
teamID = &tm.ID
|
|
globalOrTeamID = tm.ID
|
|
}
|
|
res, err := q.ExecContext(ctx, `
|
|
INSERT INTO software_installers
|
|
(team_id, global_or_team_id, title_id, filename, version, install_script_content_id, storage_id, platform, self_service)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, unhex(?), ?, ?)`,
|
|
teamID, globalOrTeamID, titleID, fmt.Sprintf("installer-%d.pkg", i), fmt.Sprintf("v%d.0.0", i), scriptContentID, hex.EncodeToString([]byte("test")), "darwin", i < 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
swiIDs = append(swiIDs, uint(id))
|
|
}
|
|
// sw1Pending and swi2Installed are self-service installers
|
|
swi1Pending, swi2Installed, swi3Failed, swi4Available, swi5Tm = swiIDs[0], swiIDs[1], swiIDs[2], swiIDs[3], swiIDs[4]
|
|
|
|
// create the results for the host
|
|
|
|
// swi1 is pending (all results are NULL)
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id) VALUES (?, ?, ?)`,
|
|
"uuid1", host.ID, swi1Pending)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// swi2 is installed
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, pre_install_query_output, install_script_exit_code, post_install_script_exit_code)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
"uuid2", host.ID, swi2Installed, "ok", 0, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// swi3 is failed, also add an install request on the other host
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, pre_install_query_output, install_script_exit_code)
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
"uuid3", host.ID, swi3Failed, "ok", 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
otherHostI1UUID = uuid.NewString()
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id) VALUES (?, ?, ?)`,
|
|
otherHostI1UUID, otherHost.ID, swi3Failed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// swi4 is available (no install request), but add a pending request on the other host
|
|
otherHostI2UUID = uuid.NewString()
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id) VALUES (?, ?, ?)`,
|
|
otherHostI2UUID, otherHost.ID, swi4Available)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// swi5 is for another team
|
|
_ = swi5Tm
|
|
|
|
// add another installer for a different platform, should be always omitted
|
|
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES ('windows-title', 'programs')`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lid, _ := res.LastInsertId()
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO software_installers
|
|
(team_id, global_or_team_id, title_id, filename, version, install_script_content_id, storage_id, platform)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, unhex(?), ?)`,
|
|
nil, 0, lid, "windows-installer-6.msi", "v6.0.0", scriptContentID, hex.EncodeToString([]byte("test")), "windows")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
// swi1Pending uses software title id of "b"
|
|
expected[byNSV[b].Name+byNSV[b].Source] = fleet.HostSoftwareWithInstaller{
|
|
Name: "b",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerPending),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"},
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true)},
|
|
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}, InstalledPaths: []string{installPaths[2]}},
|
|
},
|
|
}
|
|
i0 := fleet.HostSoftwareWithInstaller{
|
|
Name: "i0",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerInstalled),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid2"},
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-1.pkg", Version: "v1.0.0", SelfService: ptr.Bool(true)},
|
|
}
|
|
expected[i0.Name+i0.Source] = i0
|
|
|
|
i1 := fleet.HostSoftwareWithInstaller{
|
|
Name: "i1",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerFailed),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid3"},
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false)},
|
|
}
|
|
expected[i1.Name+i1.Source] = i1
|
|
|
|
// request without available software
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 7}, meta)
|
|
compareResults(expected, sw, true)
|
|
|
|
// request with available software
|
|
i2 := fleet.HostSoftwareWithInstaller{
|
|
Name: "i2",
|
|
Source: "apps",
|
|
Status: nil,
|
|
LastInstall: nil,
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", SelfService: ptr.Bool(false)},
|
|
}
|
|
expected[i2.Name+i2.Source] = i2
|
|
|
|
i3 := fleet.HostSoftwareWithInstaller{
|
|
Name: "i3",
|
|
Source: "apps",
|
|
Status: nil,
|
|
LastInstall: nil,
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-4.pkg", Version: "v4.0.0", SelfService: ptr.Bool(false)},
|
|
}
|
|
expected[i3.Name+i3.Source] = i3
|
|
|
|
opts.IncludeAvailableForInstall = true
|
|
opts.ListOptions.PerPage = 20
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 8}, meta)
|
|
compareResults(expected, sw, true, i3.Name+i3.Source)
|
|
|
|
// request in descending order
|
|
opts.ListOptions.OrderDirection = fleet.OrderDescending
|
|
opts.ListOptions.TestSecondaryOrderDirection = fleet.OrderDescending
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 7}, meta)
|
|
compareResults(expected, sw, false, i2.Name+i2.Source, i3.Name+i3.Source)
|
|
opts.ListOptions.OrderDirection = fleet.OrderAscending
|
|
opts.ListOptions.TestSecondaryOrderDirection = fleet.OrderAscending
|
|
|
|
// record a new install request for i1, this time as pending, and mark install request for b (swi1) as failed
|
|
time.Sleep(time.Second) // ensure the timestamp is later
|
|
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
|
HostID: host.ID,
|
|
InstallUUID: "uuid1",
|
|
InstallScriptExitCode: ptr.Int(2),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
// swi3 has a new install request pending
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id)
|
|
VALUES (?, ?, ?)`,
|
|
"uuid4", host.ID, swi3Failed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
expected[byNSV[b].Name+byNSV[b].Source] = fleet.HostSoftwareWithInstaller{
|
|
Name: "b",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerFailed),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"},
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true)},
|
|
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}, InstalledPaths: []string{installPaths[2]}},
|
|
},
|
|
}
|
|
expected[i1.Name+i1.Source] = fleet.HostSoftwareWithInstaller{
|
|
Name: "i1",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerPending),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid4"},
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false)},
|
|
}
|
|
|
|
// request without available software
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 7}, meta)
|
|
compareResults(expected, sw, true, i2.Name+i2.Source, i3.Name+i3.Source)
|
|
|
|
// request with available software)
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 8}, meta)
|
|
compareResults(expected, sw, true, i3.Name+i3.Source)
|
|
|
|
// create a new host in the team, with no software
|
|
tmHost := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now(), test.WithPlatform("darwin"))
|
|
nanoEnroll(t, ds, tmHost, false)
|
|
err = ds.AddHostsToTeam(ctx, &tm.ID, []uint{tmHost.ID})
|
|
require.NoError(t, err)
|
|
tmHost.TeamID = &tm.ID
|
|
|
|
// no installed software for this host
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, meta, err = ds.ListHostSoftware(ctx, tmHost, opts)
|
|
require.NoError(t, err)
|
|
require.Empty(t, sw)
|
|
require.Equal(t, &fleet.PaginationMetadata{}, meta)
|
|
|
|
// sees the available installer in its team
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, meta, err = ds.ListHostSoftware(ctx, tmHost, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 1}, meta)
|
|
compareResults(map[string]fleet.HostSoftwareWithInstaller{
|
|
i3.Name + i3.Source: expected[i3.Name+i3.Source],
|
|
}, sw, true)
|
|
|
|
// test with a search query (searches on name), with and without available software
|
|
opts.ListOptions.MatchQuery = "a"
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
compareResults(map[string]fleet.HostSoftwareWithInstaller{
|
|
byNSV[a1].Name + byNSV[a1].Source: expected[byNSV[a1].Name+byNSV[a1].Source],
|
|
byNSV[a2].Name + byNSV[a2].Source: expected[byNSV[a2].Name+byNSV[a2].Source],
|
|
}, sw, true)
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
compareResults(map[string]fleet.HostSoftwareWithInstaller{
|
|
byNSV[a1].Name + byNSV[a1].Source: expected[byNSV[a1].Name+byNSV[a1].Source],
|
|
byNSV[a2].Name + byNSV[a2].Source: expected[byNSV[a2].Name+byNSV[a2].Source],
|
|
}, sw, true)
|
|
|
|
opts.ListOptions.MatchQuery = "zz"
|
|
opts.IncludeAvailableForInstall = false
|
|
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Empty(t, sw)
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Empty(t, sw)
|
|
|
|
// add VPP apps, one for both no team and team, and two for no-team only.
|
|
va1, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{AdamID: "adam_vpp_1", Name: "vpp1", BundleIdentifier: "com.app.vpp1"}, nil)
|
|
require.NoError(t, err)
|
|
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{AdamID: "adam_vpp_1", Name: "vpp1", BundleIdentifier: "com.app.vpp1"}, &tm.ID)
|
|
require.NoError(t, err)
|
|
vpp1 := va1.AdamID
|
|
va2, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{AdamID: "adam_vpp_2", Name: "vpp2", BundleIdentifier: "com.app.vpp2"}, nil)
|
|
require.NoError(t, err)
|
|
va3, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{AdamID: "adam_vpp_3", Name: "vpp3", BundleIdentifier: "com.app.vpp3"}, nil)
|
|
require.NoError(t, err)
|
|
vpp2, vpp3 := va2.AdamID, va3.AdamID
|
|
|
|
// create an installation request for vpp1 and vpp2, leaving vpp3 as
|
|
// available only
|
|
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp1, user.ID)
|
|
vpp2CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp2, user.ID)
|
|
// make vpp1 install a success, while vpp2 has its initial request as failed
|
|
// and a subsequent request as pending.
|
|
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
|
|
createVPPAppInstallResult(t, ds, host, vpp2CmdUUID, fleet.MDMAppleStatusError)
|
|
time.Sleep(time.Second) // ensure a different created_at timestamp
|
|
vpp2bCmdUUID := createVPPAppInstallRequest(t, ds, host, vpp2, user.ID)
|
|
require.NotEmpty(t, vpp2bCmdUUID)
|
|
// add an install request for the team host on vpp1, should not impact
|
|
// main host
|
|
vpp1TmCmdUUID := createVPPAppInstallRequest(t, ds, tmHost, vpp1, user.ID)
|
|
require.NotEmpty(t, vpp1TmCmdUUID)
|
|
|
|
expected["vpp1apps"] = fleet.HostSoftwareWithInstaller{
|
|
Name: "vpp1",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerInstalled),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: vpp1CmdUUID},
|
|
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1},
|
|
}
|
|
expected["vpp2apps"] = fleet.HostSoftwareWithInstaller{
|
|
Name: "vpp2",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerPending),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: vpp2bCmdUUID},
|
|
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp2},
|
|
}
|
|
|
|
opts.IncludeAvailableForInstall = false
|
|
opts.ListOptions.MatchQuery = ""
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 9}, meta)
|
|
compareResults(expected, sw, true, i3.Name+i3.Source, i2.Name+i2.Source) // i3 is for team, i2 is available (excluded)
|
|
|
|
expected["vpp3apps"] = fleet.HostSoftwareWithInstaller{
|
|
Name: "vpp3",
|
|
Source: "apps",
|
|
Status: nil,
|
|
LastInstall: nil,
|
|
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp3},
|
|
}
|
|
opts.IncludeAvailableForInstall = true
|
|
opts.ListOptions.PerPage = 20
|
|
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 11}, meta)
|
|
compareResults(expected, sw, true, i3.Name+i3.Source) // i3 is for team
|
|
|
|
// team host sees available i3 and pending vpp1
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, meta, err = ds.ListHostSoftware(ctx, tmHost, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 2}, meta)
|
|
compareResults(map[string]fleet.HostSoftwareWithInstaller{
|
|
i3.Name + i3.Source: expected[i3.Name+i3.Source],
|
|
"vpp1apps": {
|
|
Name: "vpp1",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerPending),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: vpp1TmCmdUUID},
|
|
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1},
|
|
},
|
|
}, sw, true)
|
|
|
|
// other host does not see available VPP apps because it is a linux host
|
|
opts.IncludeAvailableForInstall = true
|
|
sw, meta, err = ds.ListHostSoftware(ctx, otherHost, opts)
|
|
require.NoError(t, err)
|
|
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 4}, meta)
|
|
|
|
expectedOther := map[string]fleet.HostSoftwareWithInstaller{
|
|
otherSoftware[0].Name + otherSoftware[0].Source: {Name: otherSoftware[0].Name, Source: otherSoftware[0].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: otherSoftware[0].Version},
|
|
}},
|
|
otherSoftware[1].Name + otherSoftware[1].Source: {Name: otherSoftware[1].Name, Source: otherSoftware[1].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
|
{Version: otherSoftware[1].Version},
|
|
}},
|
|
"i1apps": {
|
|
Name: "i1",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerPending),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI1UUID},
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false)},
|
|
},
|
|
"i2apps": {
|
|
Name: "i2",
|
|
Source: "apps",
|
|
Status: expectStatus(fleet.SoftwareInstallerPending),
|
|
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI2UUID},
|
|
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", SelfService: ptr.Bool(false)},
|
|
},
|
|
}
|
|
compareResults(expectedOther, sw, true)
|
|
|
|
// test the pagination
|
|
cases := []struct {
|
|
opts fleet.HostSoftwareTitleListOptions
|
|
wantNames []string
|
|
wantMeta *fleet.PaginationMetadata
|
|
}{
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 3}, IncludeAvailableForInstall: false},
|
|
wantNames: []string{byNSV[a1].Name, byNSV[a2].Name, byNSV[b].Name},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 9},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 3}, IncludeAvailableForInstall: false},
|
|
wantNames: []string{byNSV[c1].Name, byNSV[d].Name, i0.Name},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 9},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 2, PerPage: 3}, IncludeAvailableForInstall: false},
|
|
wantNames: []string{i1.Name, "vpp1", "vpp2"},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 9},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 3, PerPage: 3}, IncludeAvailableForInstall: false},
|
|
wantNames: []string{},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 9},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 4}, IncludeAvailableForInstall: true},
|
|
wantNames: []string{byNSV[a1].Name, byNSV[a2].Name, byNSV[b].Name, byNSV[c1].Name},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 11},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 4}, IncludeAvailableForInstall: true},
|
|
wantNames: []string{byNSV[d].Name, i0.Name, i1.Name, i2.Name},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 11},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 2, PerPage: 4}, IncludeAvailableForInstall: true},
|
|
wantNames: []string{"vpp1", "vpp2", "vpp3"},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 11},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 2}, IncludeAvailableForInstall: true, SelfServiceOnly: true},
|
|
wantNames: []string{byNSV[b].Name, i0.Name},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 2},
|
|
},
|
|
{
|
|
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 2}, IncludeAvailableForInstall: true, SelfServiceOnly: true},
|
|
wantNames: []string{},
|
|
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 2},
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(fmt.Sprintf("%#v", c.opts), func(t *testing.T) {
|
|
// always include metadata
|
|
c.opts.ListOptions.IncludeMetadata = true
|
|
c.opts.ListOptions.OrderKey = "name"
|
|
c.opts.ListOptions.TestSecondaryOrderKey = "source"
|
|
|
|
sw, meta, err := ds.ListHostSoftware(ctx, host, c.opts)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, len(c.wantNames), len(sw))
|
|
require.Equal(t, c.wantMeta, meta)
|
|
|
|
names := make([]string, 0, len(sw))
|
|
for _, s := range sw {
|
|
names = append(names, s.Name)
|
|
}
|
|
require.Equal(t, c.wantNames, names)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSetHostSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
|
|
|
// create a software installer and some host install requests
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
installScript := `echo 'foo'`
|
|
res, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`, installScript, installScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scriptContentID, _ := res.LastInsertId()
|
|
|
|
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES ('foo', 'apps')`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
titleID, _ := res.LastInsertId()
|
|
|
|
res, err = q.ExecContext(ctx, `
|
|
INSERT INTO software_installers
|
|
(title_id, filename, version, install_script_content_id, storage_id)
|
|
VALUES
|
|
(?, ?, ?, ?, unhex(?))`,
|
|
titleID, "installer.pkg", "v1.0.0", scriptContentID, hex.EncodeToString([]byte("test")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
|
|
// create some install requests for the host
|
|
for i := 0; i < 3; i++ {
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id) VALUES (?, ?, ?)`,
|
|
fmt.Sprintf("uuid%d", i), host.ID, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
checkResults := func(want *fleet.HostSoftwareInstallResultPayload) {
|
|
type result struct {
|
|
HostID uint `db:"host_id"`
|
|
InstallUUID string `db:"execution_id"`
|
|
PreInstallConditionOutput *string `db:"pre_install_query_output"`
|
|
InstallScriptExitCode *int `db:"install_script_exit_code"`
|
|
InstallScriptOutput *string `db:"install_script_output"`
|
|
PostInstallScriptExitCode *int `db:"post_install_script_exit_code"`
|
|
PostInstallScriptOutput *string `db:"post_install_script_output"`
|
|
}
|
|
var got result
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &got,
|
|
`SELECT
|
|
host_id,
|
|
execution_id,
|
|
pre_install_query_output,
|
|
install_script_exit_code,
|
|
install_script_output,
|
|
post_install_script_exit_code,
|
|
post_install_script_output
|
|
FROM
|
|
host_software_installs
|
|
WHERE execution_id = ?`, want.InstallUUID)
|
|
})
|
|
assert.Equal(t, want.HostID, got.HostID)
|
|
assert.Equal(t, want.InstallUUID, got.InstallUUID)
|
|
if want.PreInstallConditionOutput == nil {
|
|
assert.Nil(t, got.PreInstallConditionOutput)
|
|
} else {
|
|
assert.NotNil(t, got.PreInstallConditionOutput)
|
|
assert.Equal(t, *want.PreInstallConditionOutput, *got.PreInstallConditionOutput)
|
|
}
|
|
assert.Equal(t, want.InstallScriptExitCode, got.InstallScriptExitCode)
|
|
if want.InstallScriptOutput == nil {
|
|
assert.Nil(t, got.InstallScriptOutput)
|
|
} else {
|
|
assert.NotNil(t, got.InstallScriptOutput)
|
|
assert.EqualValues(t, want.InstallScriptOutput, got.InstallScriptOutput)
|
|
}
|
|
assert.Equal(t, want.PostInstallScriptExitCode, got.PostInstallScriptExitCode)
|
|
if want.PostInstallScriptOutput == nil {
|
|
assert.Nil(t, got.PostInstallScriptOutput)
|
|
} else {
|
|
assert.NotNil(t, got.PostInstallScriptOutput)
|
|
assert.EqualValues(t, want.InstallScriptOutput, got.InstallScriptOutput)
|
|
}
|
|
}
|
|
|
|
// set a result with all fields provided
|
|
want := &fleet.HostSoftwareInstallResultPayload{
|
|
HostID: host.ID,
|
|
InstallUUID: "uuid0",
|
|
PreInstallConditionOutput: ptr.String("1"),
|
|
InstallScriptExitCode: ptr.Int(0),
|
|
InstallScriptOutput: ptr.String("ok"),
|
|
PostInstallScriptExitCode: ptr.Int(0),
|
|
PostInstallScriptOutput: ptr.String("ok"),
|
|
}
|
|
err := ds.SetHostSoftwareInstallResult(ctx, want)
|
|
require.NoError(t, err)
|
|
checkResults(want)
|
|
|
|
// set a result with only the pre-condition that failed
|
|
want = &fleet.HostSoftwareInstallResultPayload{
|
|
HostID: host.ID,
|
|
InstallUUID: "uuid1",
|
|
PreInstallConditionOutput: ptr.String(""),
|
|
}
|
|
err = ds.SetHostSoftwareInstallResult(ctx, want)
|
|
require.NoError(t, err)
|
|
checkResults(want)
|
|
|
|
// set a result with only the install that failed
|
|
want = &fleet.HostSoftwareInstallResultPayload{
|
|
HostID: host.ID,
|
|
InstallUUID: "uuid2",
|
|
InstallScriptExitCode: ptr.Int(1),
|
|
InstallScriptOutput: ptr.String("fail"),
|
|
}
|
|
err = ds.SetHostSoftwareInstallResult(ctx, want)
|
|
require.NoError(t, err)
|
|
checkResults(want)
|
|
|
|
// set a result for a non-existing uuid
|
|
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
|
HostID: host.ID,
|
|
InstallUUID: "uuid-no-such",
|
|
InstallScriptExitCode: ptr.Int(0),
|
|
InstallScriptOutput: ptr.String("ok"),
|
|
})
|
|
require.Error(t, err)
|
|
require.True(t, fleet.IsNotFound(err))
|
|
}
|