fleet/server/datastore/mysql/software_test.go
Victor Lyuboslavsky 563bcdf18b
Handle multiple software entries with the same bundle ID during renames. (#33479)
- Adjusted logic to support multiple software versions sharing a bundle
ID.
- Extended tests to validate scenarios involving renamed software across
versions.

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #33468

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests
- [x] Manually QA'd using osquery `--common_software_name_suffix` switch


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Bug Fixes
- Improves handling of apps that share the same bundle ID, ensuring all
versions are correctly linked and consistently renamed across hosts.
- Reduces duplicate software entries and keeps host associations intact
during rename operations.
- Delivers more reliable software inventory views with accurate app
names derived from bundle IDs.

- Tests
- Adds comprehensive coverage for scenarios with multiple versions per
bundle ID to validate linking and renaming behavior across hosts.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-06 11:32:26 -05:00

9436 lines
350 KiB
Go

package mysql
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"math/rand"
"sort"
"strconv"
"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"
kitlog "github.com/go-kit/log"
"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},
{"CheckForDeletedInstalledSoftware", testCheckForDeletedInstalledSoftware},
{"LoadVulnerabilities", testSoftwareLoadVulnerabilities},
{"ListSoftwareCPEs", testListSoftwareCPEs},
{"NothingChanged", testSoftwareNothingChanged},
{"LoadSupportsTonsOfCVEs", testSoftwareLoadSupportsTonsOfCVEs},
{"List", testSoftwareList},
{"SyncHostsSoftware", testSoftwareSyncHostsSoftware},
{"DeleteSoftwareVulnerabilities", testDeleteSoftwareVulnerabilities},
{"HostsByCVE", testHostsByCVE},
{"HostVulnSummariesBySoftwareIDs", testHostVulnSummariesBySoftwareIDs},
{"UpdateHostSoftware", testUpdateHostSoftware},
{"UpdateHostSoftwareDeadlock", testUpdateHostSoftwareDeadlock},
{"UpdateHostSoftwareUpdatesSoftware", testUpdateHostSoftwareUpdatesSoftware},
{"UpdateHostSoftwareSameBundleIDDifferentNames", testUpdateHostSoftwareSameBundleIDDifferentNames},
{"UpdateHostSoftwareSameNameDifferentBundleIDs", testUpdateHostSoftwareSameNameDifferentBundleIDs},
{"UpdateHostSoftwareMultipleSameBundleID", testUpdateHostSoftwareMultipleSameBundleID},
{"UpdateHostSoftwareMultipleChecksumsPerBundleID", testUpdateHostSoftwareMultipleChecksumsPerBundleID},
{"UpdateHostSoftwareLongNameTruncation", testUpdateHostSoftwareLongNameTruncation},
{"UpdateHostBundleIDRenameOnlyNoNewSoftware", testUpdateHostBundleIDRenameOnlyNoNewSoftware},
{"UpdateHostBundleIDRenameWithNewSoftware", testUpdateHostBundleIDRenameWithNewSoftware},
{"UpdateHostBrowserExtensions", testUpdateHostBrowserExtensions},
{"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},
{"ListLinuxHostSoftware", testListLinuxHostSoftware},
{"ListIOSHostSoftware", testListIOSHostSoftware},
{"ListHostSoftwareWithVPPApps", testListHostSoftwareWithVPPApps},
{"ListHostSoftwareVPPSelfService", testListHostSoftwareVPPSelfService},
{"SetHostSoftwareInstallResult", testSetHostSoftwareInstallResult},
{"CreateIntermediateInstallFailureRecord", testCreateIntermediateInstallFailureRecord},
{"ListHostSoftwareInstallThenTransferTeam", testListHostSoftwareInstallThenTransferTeam},
{"ListHostSoftwareInstallThenDeleteInstallers", testListHostSoftwareInstallThenDeleteInstallers},
{"ListSoftwareVersionsVulnerabilityFilters", testListSoftwareVersionsVulnerabilityFilters},
{"TestListHostSoftwareWithLabelScoping", testListHostSoftwareWithLabelScoping},
{"TestListHostSoftwareVulnerabileAndVPP", testListHostSoftwareVulnerabileAndVPP},
{"TestListHostSoftwareQuerySearching", testListHostSoftwareQuerySearching},
{"TestListHostSoftwareWithLabelScopingVPP", testListHostSoftwareWithLabelScopingVPP},
{"TestListHostSoftwareSelfServiceWithLabelScopingHostInstalled", testListHostSoftwareSelfServiceWithLabelScopingHostInstalled},
{"TestListHostSoftwareLastOpenedAt", testListHostSoftwareLastOpenedAt},
{"DeletedInstalledSoftware", testDeletedInstalledSoftware},
{"SoftwareCategories", testSoftwareCategories},
{"LabelScopingTimestampLogic", testLabelScopingTimestampLogic},
{"InventoryPendingSoftware", testInventoryPendingSoftware},
{"PreInsertSoftwareInventory", testPreInsertSoftwareInventory},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Helper()
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 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(), fleet.NewAddHostsToTeamParams(&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 by no team (team 0)", func(t *testing.T) {
opts := fleet.SoftwareListOptions{
TeamID: ptr.Uint(0),
IncludeCVEScores: true,
}
software := listSoftwareCheckCount(t, ds, 4, 4, opts, true)
expected := []fleet.Software{bar003, baz001, foo002, foo003}
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 {
t.Helper()
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", BundleIdentifier: "com.example.abc"},
{Name: "def", Version: "0.0.1", Source: "apps", BundleIdentifier: "com.example.def"},
}
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(32)
// 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(8)
// 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(6)
// 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("", "testing")))
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, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host3.ID})))
host4 := test.NewHost(t, ds, "host4", "", "host4key", "host4uuid", time.Now())
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&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(), fleet.NewAddHostsToTeamParams(&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(6)
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(6)
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, global_stats), so we expect more rows
checkTableTotalCount(11)
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(9)
// 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))
// insert a host_software record with software_id 0 to ensure we still calculate results correctly then
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, 0)`, host1.ID)
require.NoError(t, err)
return nil
})
// this call will remove team2 from the software host counts table,
// and would normally log because we have a zero software_id
realLogger := ds.logger
ds.logger = kitlog.NewNopLogger()
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
ds.logger = realLogger
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(8)
}
// softwareChecksumComputedColumn computes the checksum for a software entry
// The calculation must match the one in computeRawChecksum
func softwareChecksumComputedColumn(tableAlias string, source string) string {
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
tableAlias += "."
}
var nameCol string
if source != "apps" {
nameCol = fmt.Sprintf("%sname,", tableAlias)
}
// concatenate with separator \x00
return fmt.Sprintf(
` UNHEX(
MD5(
CONCAT_WS(CHAR(0),
%s
%[2]sversion,
%[2]ssource,
COALESCE(%[2]sbundle_identifier, ''),
`+"%[2]s`release`"+`,
%[2]sarch,
%[2]svendor,
%[2]sbrowser,
%[2]sextension_id
)
)
) `, nameCol, 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%s%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, "", 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%s%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, "", 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
err := ds.writer(ctx).GetContext(
ctx, &titleID,
`SELECT s.title_id FROM software s WHERE s.id = ?`,
sw.ID,
)
require.NoError(t, err)
require.NotNil(t, titleID, "Software should have a title_id")
assert.NotZero(t, *titleID, "Title ID should not be zero")
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})
// Test logging criteria for LastOpenedAt == nil
oldLogger := ds.logger
buf := &bytes.Buffer{}
newLogger := kitlog.NewLogfmtLogger(buf)
ds.logger = newLogger
sw = []fleet.Software{
{Name: "foo", Version: "0.0.1", Source: "test"},
{Name: "bar_app", Version: "0.0.2", Source: "apps", LastOpenedAt: &now},
{Name: "baz_program", Version: "0.0.3", Source: "programs", LastOpenedAt: &now},
{Name: "qux_package", Version: "0.0.3", Source: "deb_packages", LastOpenedAt: &now},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, sw)
require.NoError(t, err)
sw = []fleet.Software{
{Name: "bar_app", Version: "0.0.2", Source: "apps"},
{Name: "baz_program", Version: "0.0.3", Source: "programs"},
{Name: "qux_package", Version: "0.0.3", Source: "deb_packages"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, sw)
require.NoError(t, err)
require.Contains(t, buf.String(), "baz_program")
require.Contains(t, buf.String(), "qux_package")
require.NotContains(t, buf.String(), "bar_app")
ds.logger = oldLogger
}
// Test edge case: Software with same bundle identifier but different names
// When software with the same bundle ID but different name is added, the system
// reuses the existing software entry (matched by bundle ID) and links it to the host
func testUpdateHostSoftwareSameBundleIDDifferentNames(t *testing.T, ds *Datastore) {
// TEMPORARILY SKIPPED: updateTargetedBundleIDs is commented out for performance reasons
// TODO: Re-enable when updateTargetedBundleIDs is re-enabled
t.Skip("Skipping test: updateTargetedBundleIDs is temporarily disabled for performance reasons")
ctx := t.Context()
host := test.NewHost(t, ds, "bundle-host", "", "bundlekey", "bundleuuid", time.Now())
// First, add software with a bundle ID
sw := []fleet.Software{
{Name: "GoLand.app", Version: "2024.3", Source: "apps", BundleIdentifier: "com.jetbrains.goland"},
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, sw)
require.NoError(t, err)
// Verify the software was added
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 1)
require.Equal(t, "GoLand.app", host.Software[0].Name)
originalSoftwareID := host.Software[0].ID
// Now update with the same bundle ID but different name
// The behavior depends on how the system handles bundle ID matching
sw = []fleet.Software{
{Name: "GoLand 2.app", Version: "2024.3", Source: "apps", BundleIdentifier: "com.jetbrains.goland"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, sw)
require.NoError(t, err)
// The getExistingSoftware function matches by bundle ID when present,
// so it links to the existing software entry and updates the name
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 1)
// The existing software entry is reused (matched by bundle ID) and name is updated
require.Equal(t, "GoLand 2.app", host.Software[0].Name, "Name should be updated to reflect what's on the host")
require.Equal(t, originalSoftwareID, host.Software[0].ID, "Should reuse the same software row")
// Verify the name_source was updated
var nameSource string
err = ds.writer(ctx).GetContext(ctx, &nameSource,
`SELECT name_source FROM software WHERE id = ?`, originalSoftwareID)
require.NoError(t, err)
require.Equal(t, "bundle_4.67", nameSource, "Name source should indicate bundle ID match")
// Verify only one software title exists
var titleCount int
err = ds.writer(ctx).GetContext(ctx, &titleCount,
`SELECT COUNT(DISTINCT id) FROM software_titles WHERE bundle_identifier = ?`,
"com.jetbrains.goland")
require.NoError(t, err)
require.Equal(t, 1, titleCount, "Should have only one software title for the bundle ID")
// Verify only one software entry exists with this bundle ID
var softwareCount int
err = ds.writer(ctx).GetContext(ctx, &softwareCount,
`SELECT COUNT(DISTINCT id) FROM software WHERE bundle_identifier = ?`,
"com.jetbrains.goland")
require.NoError(t, err)
require.Equal(t, 1, softwareCount, "Should have only one software entry for the bundle ID")
}
// Test edge case: Software with same name but different bundle identifiers
// This validates that separate software titles are created
func testUpdateHostSoftwareSameNameDifferentBundleIDs(t *testing.T, ds *Datastore) {
ctx := t.Context()
host := test.NewHost(t, ds, "dupname-host", "", "dupnamekey", "dupnameuuid", time.Now())
// Add two software items with same name but different bundle IDs
sw := []fleet.Software{
{Name: "MyApp", Version: "1.0", Source: "chrome_extensions", BundleIdentifier: "bundle_id1"},
{Name: "MyApp", Version: "1.0", Source: "chrome_extensions", BundleIdentifier: "bundle_id2"},
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, sw)
require.NoError(t, err)
// Verify both software entries exist
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 2)
// Verify two separate software titles exist
var titles []struct {
Name string `db:"name"`
BundleIdentifier *string `db:"bundle_identifier"`
}
err = sqlx.SelectContext(ctx, ds.reader(ctx), &titles,
`SELECT name, bundle_identifier FROM software_titles WHERE name = ? ORDER BY bundle_identifier`,
"MyApp")
require.NoError(t, err)
require.Len(t, titles, 2)
require.NotNil(t, titles[0].BundleIdentifier)
require.NotNil(t, titles[1].BundleIdentifier)
require.Equal(t, "bundle_id1", *titles[0].BundleIdentifier)
require.Equal(t, "bundle_id2", *titles[1].BundleIdentifier)
}
// Test edge case: Multiple software entries with the same bundle ID
// This validates that bundle ID renaming affects all software with that bundle ID
func testUpdateHostSoftwareMultipleSameBundleID(t *testing.T, ds *Datastore) {
// TEMPORARILY SKIPPED: updateTargetedBundleIDs is commented out for performance reasons
// TODO: Re-enable when updateTargetedBundleIDs is re-enabled
t.Skip("Skipping test: updateTargetedBundleIDs is temporarily disabled for performance reasons")
ctx := t.Context()
host1 := test.NewHost(t, ds, "multi-bundle-host1", "", "multikey1", "multiuuid1", time.Now())
host2 := test.NewHost(t, ds, "multi-bundle-host2", "", "multikey2", "multiuuid2", time.Now())
host3 := test.NewHost(t, ds, "multi-bundle-host3", "", "multikey3", "multiuuid3", time.Now())
// Step 1: Host1 reports multiple software with the same bundle ID but different versions
// This can happen with multiple installations or beta/stable versions
sw1 := []fleet.Software{
{Name: "GoLand.app", Version: "2024.2", Source: "apps", BundleIdentifier: "com.jetbrains.goland"},
{Name: "GoLand.app", Version: "2024.3-beta", Source: "apps", BundleIdentifier: "com.jetbrains.goland"},
}
_, err := ds.UpdateHostSoftware(ctx, host1.ID, sw1)
require.NoError(t, err)
// Verify both software entries were created
err = ds.LoadHostSoftware(ctx, host1, false)
require.NoError(t, err)
require.Len(t, host1.Software, 2)
// Step 2: Host2 also reports the same software
sw2 := []fleet.Software{
{Name: "GoLand.app", Version: "2024.2", Source: "apps", BundleIdentifier: "com.jetbrains.goland"},
}
_, err = ds.UpdateHostSoftware(ctx, host2.ID, sw2)
require.NoError(t, err)
// Step 3: Host3 reports the SAME software but with a different name
// This should trigger renaming of ALL software with that bundle ID
sw3 := []fleet.Software{
{Name: "GoLand 2024.app", Version: "2024.2", Source: "apps", BundleIdentifier: "com.jetbrains.goland"},
{Name: "GoLand 2024.app", Version: "2024.3-beta", Source: "apps", BundleIdentifier: "com.jetbrains.goland"},
}
_, err = ds.UpdateHostSoftware(ctx, host3.ID, sw3)
require.NoError(t, err)
// Step 4: Verify the renaming behavior
var updatedSoftware []struct {
ID uint `db:"id"`
Name string `db:"name"`
Version string `db:"version"`
NameSource string `db:"name_source"`
}
err = ds.writer(ctx).SelectContext(ctx, &updatedSoftware,
`SELECT DISTINCT id, name, version, name_source FROM software WHERE bundle_identifier = ? ORDER BY version`,
"com.jetbrains.goland")
require.NoError(t, err)
// Should have exactly 2 software entries (one per version)
require.Len(t, updatedSoftware, 2, "Should have exactly 2 software entries (one per version)")
// Both entries should be renamed to "GoLand 2024.app"
for _, sw := range updatedSoftware {
t.Logf("Software: ID=%d, Name=%s, Version=%s, NameSource=%s", sw.ID, sw.Name, sw.Version, sw.NameSource)
require.Equal(t, "GoLand 2024.app", sw.Name, "All software with same bundle ID should be renamed")
require.Equal(t, "bundle_4.67", sw.NameSource, "Renamed software should have bundle_4.67 source")
}
// Verify that host1 and host2 now see the renamed software
err = ds.LoadHostSoftware(ctx, host1, false)
require.NoError(t, err)
require.Len(t, host1.Software, 2, "Host1 should still have 2 software entries")
for _, s := range host1.Software {
require.Equal(t, "GoLand 2024.app", s.Name, "Host1 should see renamed software")
}
err = ds.LoadHostSoftware(ctx, host2, false)
require.NoError(t, err)
require.Len(t, host2.Software, 1, "Host2 should have 1 software entry")
require.Equal(t, "GoLand 2024.app", host2.Software[0].Name, "Host2 should see renamed software")
}
// Test for the bug where multiple software with the same bundle ID causes
// "software not found for checksum" errors during bundle ID rename operations.
// This test specifically validates that ALL software entries with the same
// bundle ID are properly linked to hosts when renaming occurs.
func testUpdateHostSoftwareMultipleChecksumsPerBundleID(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Note: Basic multiple versions scenario is already covered in testUpdateHostSoftwareMultipleSameBundleID
// This test focuses on the specific bug fix for renamed apps with many versions
// First, establish the software with host1 - using 10 versions to stress test
host1 := test.NewHost(t, ds, "rename-test-host1", "", "rename-key1", "rename-uuid1", time.Now())
// Create 10 versions to stress test the checksum tracking
var initialSoftware []fleet.Software
for i := 0; i < 10; i++ {
initialSoftware = append(initialSoftware, fleet.Software{
Name: "TestApp.app",
Version: fmt.Sprintf("1.%d.0", i),
Source: "apps",
BundleIdentifier: "com.stresstest.app",
})
}
_, err := ds.UpdateHostSoftware(ctx, host1.ID, initialSoftware)
require.NoError(t, err, "Should handle 10 versions with same bundle ID")
// Verify all 10 were inserted
err = ds.LoadHostSoftware(ctx, host1, false)
require.NoError(t, err)
require.Len(t, host1.Software, 10, "Host1 should have all 10 versions")
// Host2 reports the same software but renamed (user renamed the apps)
// This triggers the bundle ID rename logic and tests the bug fix
host2 := test.NewHost(t, ds, "rename-test-host2", "", "rename-key2", "rename-uuid2", time.Now())
var renamedSoftware []fleet.Software
for i := 0; i < 10; i++ {
renamedSoftware = append(renamedSoftware, fleet.Software{
Name: "TestApp Renamed.app", // Different name
Version: fmt.Sprintf("1.%d.0", i),
Source: "apps",
BundleIdentifier: "com.stresstest.app",
})
}
// This is where the bug would occur - only one software would be linked instead of all 10
result, err := ds.UpdateHostSoftware(ctx, host2.ID, renamedSoftware)
require.NoError(t, err, "Should handle renamed apps with 10 versions without 'software not found for checksum' error")
assert.NotNil(t, result)
// Verify the rename was processed in the database
var dbSoftware []struct {
Name string `db:"name"`
Version string `db:"version"`
NameSource string `db:"name_source"`
}
err = ds.writer(ctx).SelectContext(ctx, &dbSoftware,
`SELECT name, version, name_source FROM software
WHERE bundle_identifier = ? ORDER BY version`,
"com.stresstest.app")
require.NoError(t, err)
require.Len(t, dbSoftware, 10, "Should have 10 software entries in database")
// All should be renamed
for _, sw := range dbSoftware {
assert.Equal(t, "TestApp Renamed.app", sw.Name, "All software should use the new name")
assert.Equal(t, "bundle_4.67", sw.NameSource, "Renamed software should have bundle_4.67 source")
}
// Most importantly, verify that host2 has ALL 10 versions linked (this was the bug)
err = ds.LoadHostSoftware(ctx, host2, false)
require.NoError(t, err)
assert.Len(t, host2.Software, 10, "Host2 should have all 10 versions linked (bug fix verification)")
// Verify all versions are present
versions := make(map[string]bool)
for _, sw := range host2.Software {
versions[sw.Version] = true
assert.Equal(t, "TestApp Renamed.app", sw.Name, "Should see renamed app")
}
for i := 0; i < 10; i++ {
version := fmt.Sprintf("1.%d.0", i)
assert.True(t, versions[version], "Should have version %s", version)
}
}
// Test edge case: Software with names exceeding maximum length
// This validates truncation and handling of long names
func testUpdateHostSoftwareLongNameTruncation(t *testing.T, ds *Datastore) {
ctx := t.Context()
host := test.NewHost(t, ds, "longname-host", "", "longnamekey", "longnameuuid", time.Now())
// Create a truncated name (255 'a' characters)
// Both software entries will have the same name after truncation, but different versions
truncatedName := strings.Repeat("a", fleet.SoftwareNameMaxLength)
// Test Case 1: Non-macOS software (name IS included in checksum)
// Add software with the same truncated name but different versions
// Note: In production, fleet.SoftwareFromOsqueryRow() truncates names, but we're testing
// the database layer directly with pre-truncated names
sw := []fleet.Software{
{Name: truncatedName, Version: "1.0", Source: "chrome_extensions"},
{Name: truncatedName, Version: "2.0", Source: "chrome_extensions"},
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, sw)
require.NoError(t, err)
// Verify the software was added with truncated names
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
// The implementation actually creates separate entries based on the full checksum,
// even though the truncated names are the same
require.Len(t, host.Software, 2)
require.Equal(t, truncatedName, host.Software[0].Name)
require.Equal(t, truncatedName, host.Software[1].Name)
// Verify both software entries have title_ids
var softwareWithTitles []struct {
ID uint `db:"id"`
Version string `db:"version"`
TitleID *uint `db:"title_id"`
}
err = ds.writer(ctx).SelectContext(ctx, &softwareWithTitles,
`SELECT s.id, s.version, s.title_id
FROM software s
JOIN host_software hs ON hs.software_id = s.id
WHERE hs.host_id = ? AND s.name = ?
ORDER BY s.version`,
host.ID, truncatedName)
require.NoError(t, err)
require.Len(t, softwareWithTitles, 2)
// Both should have the same title_id (not NULL)
require.NotNil(t, softwareWithTitles[0].TitleID, "First software should have title_id")
require.NotNil(t, softwareWithTitles[1].TitleID, "Second software should have title_id")
require.Equal(t, *softwareWithTitles[0].TitleID, *softwareWithTitles[1].TitleID,
"Both software entries should have the same title_id")
// Test Case 2: macOS apps (name is NOT included in checksum)
// For macOS apps, the checksum doesn't include the name, so even software with
// the same truncated name and different versions will have different checksums
host2 := test.NewHost(t, ds, "longname-host2", "", "longnamekey2", "longnameuuid2", time.Now())
// Add macOS apps with the same truncated name but different versions
macOSApps := []fleet.Software{
{Name: truncatedName + ".app", Version: "1.0", Source: "apps"},
{Name: truncatedName + ".app", Version: "2.0", Source: "apps"},
}
_, err = ds.UpdateHostSoftware(ctx, host2.ID, macOSApps)
require.NoError(t, err)
// Verify both macOS apps were added
err = ds.LoadHostSoftware(ctx, host2, false)
require.NoError(t, err)
require.Len(t, host2.Software, 2)
// Verify both macOS apps have the same title (despite different checksums)
var macOSSoftwareWithTitles []struct {
ID uint `db:"id"`
Name string `db:"name"`
Version string `db:"version"`
TitleID *uint `db:"title_id"`
Source string `db:"source"`
}
err = ds.writer(ctx).SelectContext(ctx, &macOSSoftwareWithTitles,
`SELECT s.id, s.name, s.version, s.title_id, s.source
FROM software s
JOIN host_software hs ON hs.software_id = s.id
WHERE hs.host_id = ? AND s.source = 'apps'
ORDER BY s.version`,
host2.ID)
require.NoError(t, err)
require.Len(t, macOSSoftwareWithTitles, 2)
// Both macOS apps have different checksums but map to the same title
// The test successfully demonstrates the "multiple checksums to same title" scenario
}
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)
// Sleep so that the updated_at timestamp is guaranteed to be updated.
time.Sleep(1 * time.Second)
insertedOrUpdated, err := ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: host.Software[0].ID, CVE: "cve-1",
}, fleet.UbuntuOVALSource)
require.NoError(t, err)
// This will always return true because we always update the timestamp
assert.True(t, insertedOrUpdated)
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]++
}
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)
// Sleep so that the updated_at timestamp is guaranteed to be updated.
time.Sleep(1 * time.Second)
insertedOrUpdated, err := ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.UbuntuOVALSource)
require.NoError(t, err)
// This will always return true because we always update the timestamp
assert.True(t, insertedOrUpdated)
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]++
}
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(), fleet.NewAddHostsToTeamParams(&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",
BundleIdentifier: fmt.Sprintf("com.example.%s", t.name),
})
}
_, 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.
insertedOrUpdated, err := ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.NVDSource)
require.NoError(t, err)
assert.True(t, insertedOrUpdated)
err = ds.DeleteOutOfDateVulnerabilities(ctx, fleet.NVDSource, time.Now().UTC().Add(-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, 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 {
var executableSHA256 *string
if i%2 == 0 {
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("hash-%d", s.ID))))
executableSHA256 = &hash
}
stored = append(stored, fleet.HostSoftwareInstalledPath{
ID: uint(i),
HostID: host.ID,
SoftwareID: s.ID,
InstalledPath: fmt.Sprintf("/some/path/%d", s.ID),
ExecutableSHA256: executableSHA256,
})
}
toI, toD, err := hostSoftwareInstalledPathsDelta(host.ID, nil, stored, software, nil)
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%s%s", software[0].ID, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[0].ToUniqueStr())] = struct{}{}
reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[1].ID+1, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[1].ToUniqueStr())] = struct{}{}
reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[2].ID, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[2].ToUniqueStr())] = struct{}{}
var stored []fleet.HostSoftwareInstalledPath
_, _, err := hostSoftwareInstalledPathsDelta(host.ID, reported, stored, nil, nil)
require.Error(t, err)
})
t.Run("we have some deltas", func(t *testing.T) {
hash1 := fmt.Sprintf("%x", sha256.Sum256([]byte("hash-1")))
hash2 := fmt.Sprintf("%x", sha256.Sum256([]byte("hash-2")))
getKey := func(s fleet.Software, change uint) string {
var key string
switch s.ID {
case 3:
key = fmt.Sprintf(
"%s%d%s%s%s%s%s%s",
"/some/path/", s.ID+change, fleet.SoftwareFieldSeparator, "corp1", fleet.SoftwareFieldSeparator, hash1, fleet.SoftwareFieldSeparator, s.ToUniqueStr(),
)
case 5:
key = fmt.Sprintf(
"%s%d%s%s%s%s%s%s",
"/some/path/", s.ID+change, fleet.SoftwareFieldSeparator, "corp1", fleet.SoftwareFieldSeparator, hash2, fleet.SoftwareFieldSeparator, s.ToUniqueStr(),
)
default:
key = fmt.Sprintf(
"%s%d%s%s%s%s%s%s",
"/some/path/", s.ID+change, fleet.SoftwareFieldSeparator, "corp1", fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr(),
)
}
return key
}
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,
TeamIdentifier: "corp1",
InstalledPath: fmt.Sprintf("/some/path/%d", software[0].ID),
})
stored = append(stored, fleet.HostSoftwareInstalledPath{
ID: 2,
HostID: host.ID,
SoftwareID: software[1].ID,
TeamIdentifier: "corp1",
InstalledPath: fmt.Sprintf("/some/path/%d", software[1].ID),
ExecutableSHA256: &hash1,
})
stored = append(stored, fleet.HostSoftwareInstalledPath{
ID: 3,
HostID: host.ID,
SoftwareID: software[2].ID,
TeamIdentifier: "corp1",
InstalledPath: fmt.Sprintf("/some/path/%d", software[2].ID+1),
})
stored = append(stored, fleet.HostSoftwareInstalledPath{
ID: 4,
HostID: host.ID,
SoftwareID: software[3].ID,
TeamIdentifier: "corp1",
InstalledPath: fmt.Sprintf("/some/path/%d", software[3].ID),
ExecutableSHA256: &hash2,
})
toI, toD, err := hostSoftwareInstalledPathsDelta(host.ID, reported, stored, software, nil)
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)},
)
require.ElementsMatch(t,
[]*string{toI[0].ExecutableSHA256, toI[1].ExecutableSHA256},
[]*string{&hash1, nil},
)
})
}
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",
TeamIdentifier: "corp1",
ExecutableSHA256: nil,
},
{
HostID: 1,
SoftwareID: 2,
InstalledPath: "2",
TeamIdentifier: "corp2",
ExecutableSHA256: ptr.String("c32f12fc330236fcef22a4c776b001da75e6a54d68aa4f1fd3f24ad66be76f63"),
},
{
HostID: 1,
SoftwareID: 3,
InstalledPath: "3",
TeamIdentifier: "",
ExecutableSHA256: nil,
},
}
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, team_identifier, executable_sha256 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)
// Test duplicate key handling in `ReconcileSoftwareTitles`.
// Since the existing software_titles and software entries have different `source` values,
// the code will attempt to insert into `software_titles`, but the bundle_identifier + additional_identifier
// key (com.example.app1-0) will conflict.
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(ctx, `INSERT INTO software_titles (id, name, source, browser, bundle_identifier) VALUES (7, 'App1', 'some_source', 'Chrome', 'com.example.app1')`)
require.NoError(t, err)
_, err = q.ExecContext(ctx, `INSERT INTO software (name, source, browser, bundle_identifier, checksum, version) VALUES ('App1', 'some_other_source', 'Chrome', 'com.example.app1', UNHEX(MD5(CONCAT_WS(CHAR(0), 'App1', '', 'some_other_source', 'com.example.app1', '', '', '', 'Chrome', ''))), '')`)
require.NoError(t, err)
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
return nil
})
}
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 := sw.ComputeRawChecksum()
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()
t.Cleanup(func() { ds.testActivateSpecificNextActivities = nil })
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("ubuntu"))
opts := fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 11, 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)
test.CreateInsertGlobalVPPToken(t, ds)
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)
// available for install only works too
opts.OnlyAvailableForInstall = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Empty(t, sw)
assert.Equal(t, &fleet.PaginationMetadata{}, meta)
// self-service only works too
opts.OnlyAvailableForInstall = false
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"},
{Name: "e", Version: "0.0.2", Source: "deb_packages"}, // not vulnerable version
}
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"},
{Name: "e", Version: "0.0.1", Source: "deb_packages"}, // vulnerable version
}
otherSoftwareByNSV := map[string]fleet.Software{}
for _, s := range otherSoftware {
otherSoftwareByNSV[s.Name+s.Source+s.Version] = s
}
otherMutationResults, err := ds.UpdateHostSoftware(ctx, otherHost.ID, otherSoftware)
require.NoError(t, err)
for _, m := range otherMutationResults.Inserted {
s, ok := otherSoftwareByNSV[m.Name+m.Source+m.Version]
require.True(t, ok)
s.ID = m.ID
otherSoftwareByNSV[s.Name+s.Source+s.Version] = s
}
require.NoError(t, ds.LoadHostSoftware(ctx, otherHost, false))
// 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
e2 := software[6].Name + software[6].Source + software[6].Version
// shorthand keys for other software
e1 := otherSoftware[2].Name + otherSoftware[2].Source + otherSoftware[2].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"},
{SoftwareID: otherSoftwareByNSV[e1].ID, CVE: "CVE-e-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%s%s%s%s", path, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, "", 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]}},
}},
byNSV[e2].Name + byNSV[e2].Source: {Name: byNSV[e2].Name, Source: byNSV[e2].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
{Version: byNSV[e2].Version, InstalledPaths: []string{installPaths[6]}},
}},
}
compareResults := func(expected map[string]fleet.HostSoftwareWithInstaller, got []*fleet.HostSoftwareWithInstaller, expectAsc bool, expectOmitted ...string) {
gotToString := func() string {
var builder strings.Builder
builder.WriteString("Got:\n")
for _, g := range got {
builder.WriteString(fmt.Sprintf("%+v\n", g))
}
builder.WriteString("Expected:\n")
for _, e := range expected {
if expectOmitted != nil && slices.Contains(expectOmitted, e.Name+e.Source) {
continue
}
builder.WriteString(fmt.Sprintf("%+v\n", e))
}
return builder.String()
}
require.Len(t, got, len(expected)-len(expectOmitted), gotToString())
prev := ""
for _, g := range got {
for _, omit := range expectOmitted {
if g.Name+g.Source == omit {
require.FailNowf(t, "Found unexpected software in results", fmt.Sprintf("Found %s", omit))
continue
}
}
e, ok := expected[g.Name+g.Source]
require.True(t, ok, "unexpected software %s%s", g.Name, g.Source)
t.Log("Validating ", g.Name, g.Source)
require.Equal(t, e.Name, g.Name)
require.Equal(t, e.Source, g.Source)
if e.SoftwarePackage != nil {
require.Equal(t, e.SoftwarePackage.SelfService, g.SoftwarePackage.SelfService)
require.Equal(t, e.SoftwarePackage.AppStoreID, g.SoftwarePackage.AppStoreID)
require.Equal(t, e.SoftwarePackage.Name, g.SoftwarePackage.Name)
require.Equal(t, e.SoftwarePackage.Version, g.SoftwarePackage.Version)
if e.SoftwarePackage.LastInstall == nil {
require.Nil(t, g.SoftwarePackage.LastInstall)
} else {
require.NotNil(t, g.SoftwarePackage.LastInstall)
require.Equal(t, e.SoftwarePackage.LastInstall.CommandUUID, g.SoftwarePackage.LastInstall.CommandUUID)
require.Equal(t, e.SoftwarePackage.LastInstall.InstallUUID, g.SoftwarePackage.LastInstall.InstallUUID)
require.NotNil(t, g.SoftwarePackage.LastInstall.InstalledAt)
}
if e.SoftwarePackage.LastUninstall == nil {
require.Nil(t, g.SoftwarePackage.LastUninstall)
} else {
require.NotNil(t, g.SoftwarePackage.LastUninstall)
assert.Equal(t, e.SoftwarePackage.LastUninstall.ExecutionID, g.SoftwarePackage.LastUninstall.ExecutionID)
assert.NotNil(t, g.SoftwarePackage.LastUninstall.UninstalledAt)
}
if e.SoftwarePackage.AutomaticInstallPolicies != nil {
require.Len(t, e.SoftwarePackage.AutomaticInstallPolicies, len(g.SoftwarePackage.AutomaticInstallPolicies))
for i, p := range e.SoftwarePackage.AutomaticInstallPolicies {
require.Equal(t, p.ID, g.SoftwarePackage.AutomaticInstallPolicies[i].ID)
require.Equal(t, p.Name, g.SoftwarePackage.AutomaticInstallPolicies[i].Name)
}
}
}
if e.AppStoreApp != nil {
require.Equal(t, e.AppStoreApp.SelfService, g.AppStoreApp.SelfService)
require.Equal(t, e.AppStoreApp.AppStoreID, g.AppStoreApp.AppStoreID)
require.Equal(t, e.AppStoreApp.Name, g.AppStoreApp.Name)
require.Equal(t, e.AppStoreApp.Version, g.AppStoreApp.Version)
require.Equal(t, e.AppStoreApp.Platform, g.AppStoreApp.Platform)
if e.AppStoreApp.LastInstall != nil {
require.Equal(t, e.AppStoreApp.LastInstall.InstallUUID, g.AppStoreApp.LastInstall.InstallUUID)
require.Equal(t, e.AppStoreApp.LastInstall.CommandUUID, g.AppStoreApp.LastInstall.CommandUUID)
require.NotNil(t, g.AppStoreApp.LastInstall.InstalledAt)
}
if e.AppStoreApp.AutomaticInstallPolicies != nil {
require.Len(t, e.AppStoreApp.AutomaticInstallPolicies, len(g.AppStoreApp.AutomaticInstallPolicies))
for i, p := range e.AppStoreApp.AutomaticInstallPolicies {
require.Equal(t, p.ID, g.AppStoreApp.AutomaticInstallPolicies[i].ID)
require.Equal(t, p.Name, g.AppStoreApp.AutomaticInstallPolicies[i].Name)
}
}
}
// 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: 6}, 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, byNSV[e2].Name+byNSV[e2].Source)
opts.VulnerableOnly = false
// No software that is available for install
opts.OnlyAvailableForInstall = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Empty(t, sw)
assert.Equal(t, &fleet.PaginationMetadata{}, meta)
opts.OnlyAvailableForInstall = 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)
const numberOfSoftwareInstallers = 8
var swi1Pending, swi2Installed, swi3Failed, swi4Available, swi5Tm, swi6PendingUninstall, swi7FailedUninstall, swi8Uninstalled uint
var hostSwi1InstallUUID, hostSwi2InstallUUID, hostSwi3InstallUUID, hostSwi6InstallUUID, hostSwi6UninstallUUID, hostSwi7UninstallUUID, hostSwi8InstallUUID, hostSwi8UninstallUUID,
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 the uninstall script content (same for all installers, doesn't matter)
uninstallScript := `echo 'bar'`
resUninstall, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`,
uninstallScript, uninstallScript)
if err != nil {
return err
}
uninstallScriptContentID, _ := resUninstall.LastInsertId()
// create software titles for all but swi1Pending (will be linked to
// existing software title b)
var titleIDs []uint
for i := 0; i < numberOfSoftwareInstallers-1; 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 < numberOfSoftwareInstallers; 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, extension, version, install_script_content_id, uninstall_script_content_id, storage_id, platform, self_service, package_ids)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
teamID, globalOrTeamID, titleID, fmt.Sprintf("installer-%d.pkg", i), "pkg", fmt.Sprintf("v%d.0.0", i), scriptContentID,
uninstallScriptContentID,
[]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,
swi6PendingUninstall, swi7FailedUninstall, swi8Uninstalled = swiIDs[0], swiIDs[1], swiIDs[2], swiIDs[3], swiIDs[4], swiIDs[5], swiIDs[6], swiIDs[7]
// create the requests/results for the host
ds.testActivateSpecificNextActivities = []string{"-"}
// swi1 is pending (all results are NULL)
hostSwi1InstallUUID, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, swi1Pending, fleet.HostSoftwareInstallOptions{})
if err != nil {
return err
}
// swi2 is installed successfully
hostSwi2InstallUUID, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, swi2Installed, fleet.HostSoftwareInstallOptions{})
if err != nil {
return err
}
ds.testActivateSpecificNextActivities = []string{hostSwi2InstallUUID}
activated, err := ds.activateNextUpcomingActivity(ctx, q, host.ID, "")
if err != nil {
return err
}
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostSwi2InstallUUID,
PreInstallConditionOutput: ptr.String("ok"),
InstallScriptExitCode: ptr.Int(0),
PostInstallScriptExitCode: ptr.Int(0),
})
if err != nil {
return err
}
res, err = q.ExecContext(
ctx,
`INSERT INTO software (name, version, source, title_id, checksum) VALUES (?, ?, ?, ?, ?)`,
"foo2", "0.5", "bar2", &titleIDs[0], []byte("testb"),
)
if err != nil {
return err
}
Swi2InstalledSoftwareID, err := res.LastInsertId()
if err != nil {
return err
}
// insert into host_software
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`,
host.ID, Swi2InstalledSoftwareID)
if err != nil {
return err
}
// swi3 is failed, also add an install request on the other host
hostSwi3InstallUUID, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, swi3Failed, fleet.HostSoftwareInstallOptions{})
if err != nil {
return err
}
ds.testActivateSpecificNextActivities = []string{hostSwi3InstallUUID}
activated, err = ds.activateNextUpcomingActivity(ctx, q, host.ID, "")
if err != nil {
return err
}
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostSwi3InstallUUID,
PreInstallConditionOutput: ptr.String("ok"),
InstallScriptExitCode: ptr.Int(1),
})
if err != nil {
return err
}
otherHostI1UUID, err = ds.InsertSoftwareInstallRequest(ctx, otherHost.ID, swi3Failed, fleet.HostSoftwareInstallOptions{})
if err != nil {
return err
}
// swi4 is available (no install request), but add a pending request on the other host
otherHostI2UUID, err = ds.InsertSoftwareInstallRequest(ctx, otherHost.ID, swi4Available, fleet.HostSoftwareInstallOptions{})
if err != nil {
return err
}
// swi5 is for another team
_ = swi5Tm
// swi6 has been installed, and is pending uninstall
hostSwi6InstallUUID, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, swi6PendingUninstall, fleet.HostSoftwareInstallOptions{})
if err != nil {
return err
}
ds.testActivateSpecificNextActivities = []string{hostSwi6InstallUUID}
activated, err = ds.activateNextUpcomingActivity(ctx, q, host.ID, "")
if err != nil {
return err
}
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostSwi6InstallUUID,
PreInstallConditionOutput: ptr.String("ok"),
InstallScriptExitCode: ptr.Int(0),
PostInstallScriptExitCode: ptr.Int(0),
})
if err != nil {
return err
}
hostSwi6UninstallUUID = uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi6UninstallUUID, host.ID, swi6PendingUninstall, false)
if err != nil {
return err
}
// swi7 is failed uninstall
hostSwi7UninstallUUID = uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi7UninstallUUID, host.ID, swi7FailedUninstall, true)
if err != nil {
return err
}
ds.testActivateSpecificNextActivities = []string{hostSwi7UninstallUUID}
activated, err = ds.activateNextUpcomingActivity(ctx, q, host.ID, "")
if err != nil {
return err
}
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
_, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
HostID: host.ID,
ExecutionID: hostSwi7UninstallUUID,
ExitCode: 1,
})
if err != nil {
return err
}
// swi8 is successfully installed
hostSwi8InstallUUID, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, swi8Uninstalled, fleet.HostSoftwareInstallOptions{})
if err != nil {
return err
}
ds.testActivateSpecificNextActivities = []string{hostSwi8InstallUUID}
activated, err = ds.activateNextUpcomingActivity(ctx, q, host.ID, "")
if err != nil {
return err
}
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostSwi8InstallUUID,
PreInstallConditionOutput: ptr.String("ok"),
InstallScriptExitCode: ptr.Int(0),
PostInstallScriptExitCode: ptr.Int(0),
})
if err != nil {
return err
}
// swi8 is successful uninstall
hostSwi8UninstallUUID = uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi8UninstallUUID, host.ID, swi8Uninstalled, true)
if err != nil {
return err
}
ds.testActivateSpecificNextActivities = []string{hostSwi8UninstallUUID}
activated, err = ds.activateNextUpcomingActivity(ctx, q, host.ID, "")
if err != nil {
return err
}
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
_, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
HostID: host.ID,
ExecutionID: hostSwi8UninstallUUID,
ExitCode: 0,
})
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: nil, // pending installs do not get returned, so status is nil
SoftwarePackage: nil,
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.SoftwareInstalled),
SoftwarePackage: nil,
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
{Version: "0.5", Vulnerabilities: []string{}, InstalledPaths: []string{}},
},
}
expected[i0.Name+i0.Source] = i0
// 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: uint(len(expected))}, meta)
compareResults(expected, sw, true)
// request with available software
i4 := fleet.HostSoftwareWithInstaller{
Name: "i4",
Source: "apps",
Status: expectStatus(fleet.SoftwareUninstallPending),
SoftwarePackage: nil,
}
expected[i4.Name+i4.Source] = i4
i5 := fleet.HostSoftwareWithInstaller{
Name: "i5",
Source: "apps",
Status: expectStatus(fleet.SoftwareUninstallFailed),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "installer-6.pkg", Version: "v6.0.0", Platform: "darwin", SelfService: ptr.Bool(false),
LastUninstall: &fleet.HostSoftwareUninstall{ExecutionID: hostSwi7UninstallUUID},
},
}
expected[i5.Name+i5.Source] = i5
i6 := fleet.HostSoftwareWithInstaller{
Name: "i6",
Source: "apps",
Status: nil,
SoftwarePackage: nil,
}
expected[i6.Name+i6.Source] = i6
i1 := fleet.HostSoftwareWithInstaller{
Name: "i1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallFailed),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "installer-2.pkg", Version: "v2.0.0", Platform: "darwin", SelfService: ptr.Bool(false),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: hostSwi3InstallUUID},
},
}
expected[i1.Name+i1.Source] = i1
i2 := fleet.HostSoftwareWithInstaller{
Name: "i2",
Source: "apps",
Status: nil,
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", Platform: "darwin", SelfService: ptr.Bool(false)},
}
expected[i2.Name+i2.Source] = i2
i3 := fleet.HostSoftwareWithInstaller{
Name: "i3",
Source: "apps",
Status: nil,
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-4.pkg", Version: "v4.0.0", Platform: "darwin", 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: uint(len(expected)) - 1}, meta)
compareResults(expected, sw, true, i3.Name+i3.Source)
// request with available software only (attempted to install and never attempted to install)
expectedAvailableOnly := map[string]fleet.HostSoftwareWithInstaller{}
expectedAvailableOnly[byNSV[b].Name+byNSV[b].Source] = expected[byNSV[b].Name+byNSV[b].Source]
// Although this is already installed, we ignore the status on the host because we can "reinstall" it
// if there is a compatible software package available for the host
expectedAvailableOnly[i0.Name+i0.Source] = i0
expectedAvailableOnly[i1.Name+i1.Source] = i1
expectedAvailableOnly[i2.Name+i2.Source] = i2
expectedAvailableOnly[i4.Name+i4.Source] = i4
expectedAvailableOnly[i5.Name+i5.Source] = i5
expectedAvailableOnly[i6.Name+i6.Source] = i6
opts.OnlyAvailableForInstall = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnly))}, meta)
compareResults(expectedAvailableOnly, sw, true)
opts.OnlyAvailableForInstall = false
// 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: uint(len(expected)) - 6}, meta)
compareResults(expected, sw, false, i1.Name+i1.Source, i2.Name+i2.Source, i3.Name+i3.Source, i4.Name+i4.Source, i5.Name+i5.Source, i6.Name+i6.Source)
opts.ListOptions.OrderDirection = fleet.OrderAscending
opts.ListOptions.TestSecondaryOrderDirection = fleet.OrderAscending
// record a new install request for i1 (swi3), this time as pending, and mark install request for b (swi1) as failed
time.Sleep(time.Second) // ensure the timestamp is later
ds.testActivateSpecificNextActivities = []string{hostSwi1InstallUUID}
activated, err := ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host.ID, "")
require.NoError(t, err)
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostSwi1InstallUUID,
InstallScriptExitCode: ptr.Int(2),
})
require.NoError(t, err)
ds.testActivateSpecificNextActivities = []string{"-"}
// swi3 has a new install request pending
hostSwi3PendingInstallUUID, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, swi3Failed, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
expected[byNSV[b].Name+byNSV[b].Source] = fleet.HostSoftwareWithInstaller{
Name: "b",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallFailed),
SoftwarePackage: nil,
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.SoftwareInstallPending),
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", Platform: "darwin", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: hostSwi3PendingInstallUUID}},
}
expectedAvailableOnly[byNSV[b].Name+byNSV[b].Source] = expected[byNSV[b].Name+byNSV[b].Source]
expectedAvailableOnly[i1.Name+i1.Source] = expected[i1.Name+i1.Source]
// 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: uint(len(expected)) - 6}, meta)
compareResults(expected, sw, true, i1.Name+i1.Source, i2.Name+i2.Source, i3.Name+i3.Source, i4.Name+i4.Source, i5.Name+i5.Source, i6.Name+i6.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: uint(len(expected)) - 1}, 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, fleet.NewAddHostsToTeamParams(&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)
// add policies to software
policy1 := newTestPolicy(t, ds, user, "policy 1", "darwin", &tm.ID)
policy1.SoftwareInstallerID = ptr.Uint(swi5Tm)
err = ds.SavePolicy(context.Background(), policy1, false, false)
require.NoError(t, err)
policy2 := newTestPolicy(t, ds, user, "policy 2", "darwin", &tm.ID)
policy2.SoftwareInstallerID = ptr.Uint(swi5Tm)
err = ds.SavePolicy(context.Background(), policy2, false, false)
require.NoError(t, err)
sw, meta, err = ds.ListHostSoftware(ctx, tmHost, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 1}, meta)
expectedWithPolicies := map[string]fleet.HostSoftwareWithInstaller{
i3.Name + i3.Source: expected[i3.Name+i3.Source],
}
expectedWithPolicies[i3.Name+i3.Source].SoftwarePackage.AutomaticInstallPolicies = []fleet.AutomaticInstallPolicy{
{
ID: policy1.ID,
Name: policy1.Name,
},
{
ID: policy2.ID,
Name: policy2.Name,
},
}
compareResults(expectedWithPolicies, 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{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, nil)
require.NoError(t, err)
_, err = ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, nil)
require.NoError(t, err)
vatm1, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, &tm.ID)
require.NoError(t, err)
vpp1 := va1.AdamID
va2, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.MacOSPlatform}}, Name: "vpp2",
BundleIdentifier: "com.app.vpp2",
}, nil)
require.NoError(t, err)
// create vpp3 app that allows self-service
va3, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.MacOSPlatform}, SelfService: true}, 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)
vpp2CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp2, user)
// make vpp1 install a success, while vpp2 has its initial request as failed
// and a subsequent request as pending.
ds.testActivateSpecificNextActivities = []string{vpp1CmdUUID, vpp2CmdUUID}
activated, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host.ID, "")
require.NoError(t, err)
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
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)
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)
require.NotEmpty(t, vpp1TmCmdUUID)
// add policies to vpp app 1
va1FromDB, err := ds.GetVPPAppMetadataByAdamIDPlatformTeamID(ctx, vatm1.AdamID, vatm1.Platform, &tm.ID)
require.NoError(t, err)
policy3 := newTestPolicy(t, ds, user, "policy 3", "darwin", &tm.ID)
policy3.VPPAppsTeamsID = &va1FromDB.AppTeamID
err = ds.SavePolicy(context.Background(), policy3, false, false)
require.NoError(t, err)
policy4 := newTestPolicy(t, ds, user, "policy 4", "darwin", &tm.ID)
policy4.VPPAppsTeamsID = &va1FromDB.AppTeamID
err = ds.SavePolicy(context.Background(), policy4, false, false)
require.NoError(t, err)
opts.IncludeAvailableForInstall = false
opts.ListOptions.MatchQuery = ""
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 6}, meta)
compareResults(expected, sw, true, i1.Name+i1.Source, i3.Name+i3.Source, i2.Name+i2.Source, i4.Name+i4.Source, i5.Name+i5.Source, i6.Name+i6.Source) // i3 is for team, i2 is available (excluded)
expected["vpp1apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstalled),
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1, Platform: "darwin", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1CmdUUID}},
}
expected["vpp2apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp2",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallPending),
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp2, Platform: "darwin", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp2bCmdUUID}},
}
expected["vpp3apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp3",
Source: "apps",
Status: nil,
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp3, Platform: "darwin", SelfService: ptr.Bool(true)},
}
expectedAvailableOnly["vpp1apps"] = expected["vpp1apps"]
expectedAvailableOnly["vpp2apps"] = expected["vpp2apps"]
expectedAvailableOnly["vpp3apps"] = expected["vpp3apps"]
opts.IncludeAvailableForInstall = true
opts.IsMDMEnrolled = true
opts.ListOptions.PerPage = 20
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 1}, meta)
compareResults(expected, sw, true, i3.Name+i3.Source) // i3 is for team
// Host is not MDM enrolled - we should not get "vpp3papps" because we can't install it.
opts.IsMDMEnrolled = false
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 2}, meta)
compareResults(expected, sw, true, "vpp3apps", i3.Name+i3.Source) // i3 is for team
// Available for install only
opts.OnlyAvailableForInstall = true
opts.IsMDMEnrolled = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnly))}, meta)
compareResults(expectedAvailableOnly, sw, true)
// Available for install only with host not MDM enrolled
// We should only exclude "vpp3apps", because it was not installed previously and we can't
// install it without MDM
opts.IsMDMEnrolled = false
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnly) - 1)}, meta)
compareResults(expectedAvailableOnly, sw, true, "vpp3apps")
opts.IsMDMEnrolled = false
opts.OnlyAvailableForInstall = false
// 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.SoftwareInstallPending),
AppStoreApp: &fleet.SoftwarePackageOrApp{
AppStoreID: vpp1,
Platform: "darwin",
SelfService: ptr.Bool(false),
LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1TmCmdUUID},
AutomaticInstallPolicies: []fleet.AutomaticInstallPolicy{
{
ID: policy3.ID,
Name: policy3.Name,
},
{
ID: policy4.ID,
Name: policy4.Name,
},
},
},
},
}, 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: 5}, 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},
}},
otherSoftware[2].Name + otherSoftware[2].Source: {Name: otherSoftware[2].Name, Source: otherSoftware[2].Source, InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
{Version: otherSoftware[2].Version, Vulnerabilities: []string{vulns[4].CVE}},
}},
"i1apps": {
Name: "i1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallPending),
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", Platform: "darwin", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI1UUID}},
},
"i2apps": {
Name: "i2",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallPending),
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", Platform: "darwin", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI2UUID}},
},
}
compareResults(expectedOther, sw, true)
// i4 is "pending uninstall"
// force i4 to be returned by osquery
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var i4Title struct {
ID uint `db:"id"`
Name string `db:"name"`
Source string `db:"source"`
Version string `db:"version"`
}
err := sqlx.GetContext(ctx, q, &i4Title, `
SELECT software_titles.id, software_titles.name, software_titles.source, software_installers.version
FROM software_installers
JOIN software_titles ON software_installers.title_id = software_titles.id
WHERE software_installers.id = ?`, swi6PendingUninstall)
if err != nil {
return err
}
res, err := q.ExecContext(ctx,
`INSERT INTO software (name, source, bundle_identifier, version, title_id, checksum) VALUES (?, ?, ?, ?, ?, UNHEX(MD5(?)))`,
i4Title.Name,
i4Title.Source,
"i4Title.com.example",
i4Title.Version,
i4Title.ID,
"i4-checksum",
)
if err != nil {
return err
}
softwareID, err := res.LastInsertId()
if err != nil {
return err
}
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`,
host.ID, softwareID)
if err != nil {
return err
}
return nil
})
// test the pagination
cases := []struct {
name string
opts fleet.HostSoftwareTitleListOptions
wantNames []string
wantMeta *fleet.PaginationMetadata
}{
{
name: "No available for install software, page 0",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 5}, IncludeAvailableForInstall: false},
wantNames: []string{byNSV[a1].Name, byNSV[a2].Name, byNSV[b].Name, byNSV[c1].Name, byNSV[d].Name},
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 8},
},
{
name: "No available for install software, page 1",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 5}, IncludeAvailableForInstall: false},
wantNames: []string{byNSV[e2].Name, i0.Name, i4.Name},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8},
},
{
name: "No available for install software, page 2",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 2, PerPage: 5}, IncludeAvailableForInstall: false},
wantNames: []string{},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8},
},
{
name: "Include Available for install software with MDM on, page 0",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 5}, IncludeAvailableForInstall: true, IsMDMEnrolled: true},
wantNames: []string{byNSV[a1].Name, byNSV[a2].Name, byNSV[b].Name, byNSV[c1].Name, byNSV[d].Name},
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 15},
},
{
name: "Include Available for install software with MDM on, page 1",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 5}, IncludeAvailableForInstall: true, IsMDMEnrolled: true},
wantNames: []string{byNSV[e2].Name, i0.Name, i1.Name, i2.Name, i4.Name},
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 15},
},
{
name: "Include Available for install software with MDM on, page 2",
opts: fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 2, PerPage: 5},
IncludeAvailableForInstall: true,
IsMDMEnrolled: true,
},
wantNames: []string{i5.Name, i6.Name, "vpp1", "vpp2", "vpp3"},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 15},
},
{
// Excludes vpp3 because it was never installed, and we can't install it with MDM off
name: "Include Available for install software with MDM off, page 2",
opts: fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 2, PerPage: 5},
IncludeAvailableForInstall: true,
},
wantNames: []string{i5.Name, i6.Name, "vpp1", "vpp2"},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 14},
},
{
name: "Include Available for install software with MDM on, page 3",
opts: fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 3, PerPage: 5},
IncludeAvailableForInstall: true,
IsMDMEnrolled: true,
},
wantNames: []string{},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 15},
},
{
name: "Available for install and self-service only software with MDM on, page 0",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 3}, IncludeAvailableForInstall: true, SelfServiceOnly: true, IsMDMEnrolled: true},
wantNames: []string{byNSV[b].Name, i0.Name, "vpp3"},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 3},
},
{
name: "Available for install and self-service only software with MDM on, page 1",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 3}, IncludeAvailableForInstall: true, SelfServiceOnly: true, IsMDMEnrolled: true},
wantNames: []string{},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 3},
},
{
name: "Available for install and self-service only software with MDM off, page 0",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 3}, IncludeAvailableForInstall: true, SelfServiceOnly: true},
wantNames: []string{byNSV[b].Name, i0.Name},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 2},
},
{
name: "Only available for install software with MDM on, page 0",
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 0, PerPage: 4}, OnlyAvailableForInstall: true, IsMDMEnrolled: true},
wantNames: []string{byNSV[b].Name, "i0", "i1", "i2"},
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 10},
},
{
name: "Only available for install with MDM on, page 2",
opts: fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 2, PerPage: 4},
OnlyAvailableForInstall: true,
IsMDMEnrolled: true,
},
wantNames: []string{"vpp2", "vpp3"},
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 10},
},
}
for _, c := range cases {
t.Run(c.name, 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)
names := make([]string, 0, len(sw))
for _, s := range sw {
names = append(names, s.Name)
}
assert.Equal(t, c.wantNames, names)
assert.Equal(t, c.wantMeta, meta)
})
}
darwinHost := test.NewHost(t, ds, "hostD", "", "hostDkey", "hostDuuid", time.Now(), test.WithPlatform("darwin"))
softwareAlreadyInstalled := fleet.Software{Name: "DummyApp.app", Version: "1.0.1", Source: "apps", BundleIdentifier: "com.example.dummy"}
// Host has software installed, but not by Fleet, and there is no matching software installer.
// Ensure it is not surfaced as "available for install" when filtering by `VulnerableOnly`
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
res, err := q.ExecContext(ctx, `INSERT INTO software_titles (name, source, bundle_identifier) VALUES (?, ?, ?)`,
softwareAlreadyInstalled.Name, softwareAlreadyInstalled.Source, softwareAlreadyInstalled.BundleIdentifier)
if err != nil {
return err
}
titleID, err := res.LastInsertId()
if err != nil {
return err
}
titleIDUint := uint(titleID)
softwareAlreadyInstalled.TitleID = &titleIDUint
res, err = q.ExecContext(ctx, `INSERT INTO software (name, source, bundle_identifier, version, title_id, checksum) VALUES (?, ?, ?, ?, ?, UNHEX(MD5(?)))`,
softwareAlreadyInstalled.Name, softwareAlreadyInstalled.Source, softwareAlreadyInstalled.BundleIdentifier, softwareAlreadyInstalled.Version, titleID, "dummy-checksum")
if err != nil {
return err
}
softwareID, err := res.LastInsertId()
if err != nil {
return err
}
softwareAlreadyInstalled.ID = uint(softwareID)
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`,
darwinHost.ID, softwareID)
if err != nil {
return err
}
return nil
})
opts = fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 11, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
opts.OnlyAvailableForInstall = true
opts.VulnerableOnly = true
sw, _, err = ds.ListHostSoftware(ctx, darwinHost, opts)
require.NoError(t, err)
assert.Len(t, sw, 0, "Expected to find no software in the list")
// Now add a vulnerability to DummyApp.pkg, it should still not come back because we are filtering for "available for install"
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{SoftwareID: softwareAlreadyInstalled.ID, CVE: "CVE-2025-10101"}, fleet.NVDSource)
require.NoError(t, err)
opts = fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 11, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
opts.OnlyAvailableForInstall = true
opts.VulnerableOnly = true
sw, _, err = ds.ListHostSoftware(ctx, darwinHost, opts)
require.NoError(t, err)
assert.Len(t, sw, 0, "Expected to find no software in the list")
// Add a matching software installer
// Ensure it is surfaced as "available for install"
var SoftwareInstallerID uint
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
installScript := `install 'DummyApp.app'`
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()
uninstallScript := `uinstall 'DummyApp.app'`
resUninstall, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`,
uninstallScript, uninstallScript)
if err != nil {
return err
}
uninstallScriptContentID, _ := resUninstall.LastInsertId()
res, err = q.ExecContext(ctx, `
INSERT INTO software_installers
(team_id, global_or_team_id, title_id, filename, extension, version, install_script_content_id, uninstall_script_content_id, storage_id, platform, self_service, package_ids)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
darwinHost.TeamID, 0, softwareAlreadyInstalled.TitleID, "DummyApp.pkg", "pkg", "2.0.0",
scriptContentID, uninstallScriptContentID,
[]byte("test"), "darwin", true, "[]")
if err != nil {
return err
}
lastInsertID, err := res.LastInsertId()
if err != nil {
return err
}
SoftwareInstallerID = uint(lastInsertID)
return nil
})
require.NoError(t, err)
opts = fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 11, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
opts.OnlyAvailableForInstall = true
sw, _, err = ds.ListHostSoftware(ctx, darwinHost, opts)
require.NoError(t, err)
var found bool
index := -1
for i, s := range sw {
if s.Name == softwareAlreadyInstalled.Name && s.Source == softwareAlreadyInstalled.Source {
found = true
index = i
break
}
}
require.True(t, found, "Expected to find software %s in the list", softwareAlreadyInstalled.Name)
assert.Equal(t, sw[index].InstalledVersions[0].Version, softwareAlreadyInstalled.Version)
assert.Equal(t, sw[index].SoftwarePackage.Name, "DummyApp.pkg")
assert.Equal(t, sw[index].SoftwarePackage.Version, "2.0.0")
assert.Equal(t, sw[index].SoftwarePackage.Platform, "darwin")
// Now with matching software installer, if filtering by `VulnerableOnly` we should get the software, as it has a vulnerability
opts = fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 11, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
opts.OnlyAvailableForInstall = true
opts.VulnerableOnly = true
sw, _, err = ds.ListHostSoftware(ctx, darwinHost, opts)
require.NoError(t, err)
found = false
index = -1
for i, s := range sw {
if s.Name == softwareAlreadyInstalled.Name && s.Source == softwareAlreadyInstalled.Source {
found = true
index = i
break
}
}
require.True(t, found, "Expected to find software %s in the list", softwareAlreadyInstalled.Name)
assert.Equal(t, sw[index].InstalledVersions[0].Version, softwareAlreadyInstalled.Version)
// This vulnerable software is not installed on the host, however, has a software installer available for install, should not be returned
opts = fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 11, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
opts.OnlyAvailableForInstall = true
opts.VulnerableOnly = true
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
found = false
for _, s := range sw {
if s.Name == softwareAlreadyInstalled.Name && s.Source == softwareAlreadyInstalled.Source {
found = true
break
}
}
require.False(t, found, "Expected not find software %s in the list", softwareAlreadyInstalled.Name)
// Attempt to install the vulnerable software on the host (pending), if filtering by `VulnerableOnly`, should not be returned
_, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, SoftwareInstallerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
opts = fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 11, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
opts.OnlyAvailableForInstall = true
opts.VulnerableOnly = true
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
found = false
for _, s := range sw {
if s.Name == softwareAlreadyInstalled.Name && s.Source == softwareAlreadyInstalled.Source {
found = true
break
}
}
require.False(t, found, "Expected not find software %s in the list", softwareAlreadyInstalled.Name)
}
func testListLinuxHostSoftware(t *testing.T, ds *Datastore) {
ctx := context.Background()
rpmHost := test.NewHost(t, ds, "RHEL", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("rhel"))
debHost := test.NewHost(t, ds, "Ubuntu", "", "host2key", "host2uuid", time.Now(), test.WithPlatform("pop"))
archHost := test.NewHost(t, ds, "Arch", "", "host3key", "host3uuid", time.Now(), test.WithPlatform("arch"))
genericLinuxHost := test.NewHost(t, ds, "Vanilla Linux", "", "host4key", "host4uuid", time.Now(), test.WithPlatform("linux"))
macHost := test.NewHost(t, ds, "Mac", "", "host5key", "host5uuid", time.Now(), test.WithPlatform("darwin"))
// Add titles and installers for deb (visible on deb host), rpm (visible on rpm host), tarball (visible on non-Mac hosts)
type installerInfo struct {
Filename string
Extension string
}
installers := map[string]installerInfo{
"deb_packages": {Filename: "deb.deb", Extension: "deb"},
"rpm_packages": {Filename: "rpm.rpm", Extension: "rpm"},
"tgz_packages": {Filename: "tar.tar.gz", Extension: "tar.gz"},
}
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
script := `hello world`
res, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`, script, script)
if err != nil {
return err
}
scriptContentID, _ := res.LastInsertId()
for source, installer := range installers {
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES (?, ?)`, source, source)
require.NoError(t, err)
titleID, _ := res.LastInsertId()
_, err = q.ExecContext(ctx, `
INSERT INTO software_installers
(team_id, global_or_team_id, title_id, filename, extension, version, install_script_content_id, uninstall_script_content_id, storage_id, platform, self_service, package_ids)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
nil, 0, titleID, installer.Filename, installer.Extension, "2.0.0",
scriptContentID, scriptContentID,
[]byte("test"), "linux", true, "[]")
require.NoError(t, err)
}
opts := fleet.HostSoftwareTitleListOptions{OnlyAvailableForInstall: true, ListOptions: fleet.ListOptions{OrderKey: "name"}}
for host, expectedInstallers := range map[*fleet.Host][]installerInfo{
rpmHost: {installers["rpm_packages"], installers["tgz_packages"]},
debHost: {installers["deb_packages"], installers["tgz_packages"]},
archHost: {installers["tgz_packages"]},
genericLinuxHost: {installers["deb_packages"], installers["rpm_packages"], installers["tgz_packages"]},
macHost: {},
} {
t.Run(host.Hostname, func(t *testing.T) {
sw, _, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, len(expectedInstallers))
for i, installer := range expectedInstallers {
require.Equal(t, installer.Filename, sw[i].SoftwarePackage.Name)
}
})
}
return nil
})
}
func testListIOSHostSoftware(t *testing.T, ds *Datastore) {
ctx := context.Background()
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
nanoEnroll(t, ds, host, false)
opts := fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{
PerPage: 10, IncludeMetadata: true, OrderKey: "name",
TestSecondaryOrderKey: "source",
}}
test.CreateInsertGlobalVPPToken(t, ds)
user, err := ds.NewUser(ctx, &fleet.User{
Password: []byte("p4ssw0rd.123"),
Name: "userIOS",
Email: "userIOS@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)
assert.Empty(t, sw)
assert.Equal(t, &fleet.PaginationMetadata{}, meta)
// add software to the host
software := []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "ios_apps"},
{Name: "b", Version: "0.0.2", Source: "ios_apps"},
{Name: "c", Version: "0.0.3", Source: "ios_apps"},
{Name: "c", Version: "0.0.4", Source: "ios_apps"},
}
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)
assert.Len(t, mutationResults.Inserted, len(software))
for _, m := range mutationResults.Inserted {
s, ok := byNSV[m.Name+m.Source+m.Version]
assert.True(t, ok)
assert.Equal(t, m.Name, s.Name, "name")
assert.Equal(t, m.Version, s.Version, "version")
assert.Equal(t, m.Source, s.Source, "source")
assert.Zero(t, s.ID) // not set in the map yet
assert.NotZero(t, m.ID)
s.ID = m.ID
byNSV[s.Name+s.Source+s.Version] = s
}
assert.NoError(t, ds.LoadHostSoftware(ctx, host, false))
assert.Equal(t, len(host.Software), len(software))
for _, hs := range host.Software {
s, ok := byNSV[hs.Name+hs.Source+hs.Version]
assert.True(t, ok)
assert.Equal(t, hs.Name, s.Name, "name")
assert.Equal(t, hs.Version, s.Version, "version")
assert.Equal(t, hs.Source, s.Source, "source")
assert.Equal(t, hs.ID, s.ID)
}
// shorthand keys for expected software
getKey := func(i int) string {
return software[i].Name + software[i].Source + software[i].Version
}
a1 := getKey(0)
b := getKey(1)
c1 := getKey(2)
c2 := getKey(3)
// add some vulnerabilities
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)
}
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}},
},
},
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}},
},
},
// 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},
{Version: byNSV[c2].Version},
},
},
}
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, "unexpected software name:%s source:%s", g.Name, g.Source)
require.Equal(t, e.Name, g.Name)
require.Equal(t, e.Source, g.Source)
if e.SoftwarePackage != nil {
require.Equal(t, e.SoftwarePackage.SelfService, g.SoftwarePackage.SelfService)
require.Equal(t, e.SoftwarePackage.AppStoreID, g.SoftwarePackage.AppStoreID)
require.Equal(t, e.SoftwarePackage.Name, g.SoftwarePackage.Name)
require.Equal(t, e.SoftwarePackage.Version, g.SoftwarePackage.Version)
if e.SoftwarePackage.LastInstall != nil {
require.Equal(t, e.SoftwarePackage.LastInstall.CommandUUID, g.SoftwarePackage.LastInstall.CommandUUID)
require.Equal(t, e.SoftwarePackage.LastInstall.InstallUUID, g.SoftwarePackage.LastInstall.InstallUUID)
require.NotNil(t, g.SoftwarePackage.LastInstall.InstalledAt)
}
}
if e.AppStoreApp != nil {
require.Equal(t, e.AppStoreApp.SelfService, g.AppStoreApp.SelfService)
require.Equal(t, e.AppStoreApp.AppStoreID, g.AppStoreApp.AppStoreID)
require.Equal(t, e.AppStoreApp.Name, g.AppStoreApp.Name)
require.Equal(t, e.AppStoreApp.Version, g.AppStoreApp.Version)
require.Equal(t, e.AppStoreApp.Platform, g.AppStoreApp.Platform)
if e.AppStoreApp.LastInstall != nil {
require.Equal(t, e.AppStoreApp.LastInstall.InstallUUID, g.AppStoreApp.LastInstall.InstallUUID)
require.Equal(t, e.AppStoreApp.LastInstall.CommandUUID, g.AppStoreApp.LastInstall.CommandUUID)
require.NotNil(t, g.AppStoreApp.LastInstall.InstalledAt)
}
}
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)
assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected))}, meta)
compareResults(expected, sw, true)
opts.VulnerableOnly = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected) - 1)}, meta)
compareResults(expected, sw, true, byNSV[c1].Name+byNSV[c1].Source)
opts.VulnerableOnly = false
// No software that is available for install
opts.OnlyAvailableForInstall = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Empty(t, sw)
assert.Equal(t, &fleet.PaginationMetadata{}, meta)
opts.OnlyAvailableForInstall = false
// Create a team
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "mobile team"})
require.NoError(t, err)
// add VPP apps, one for both no team and team, and three for no-team only.
va1, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, nil)
require.NoError(t, err)
_, err = ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, nil)
require.NoError(t, err)
_, err = ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IPadOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, nil)
require.NoError(t, err)
_, err = ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IPadOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, &tm.ID)
require.NoError(t, err)
vpp1 := va1.AdamID
va2, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.IOSPlatform}}, Name: "vpp2",
BundleIdentifier: "com.app.vpp2",
}, nil)
require.NoError(t, err)
va3, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.IOSPlatform}}, Name: "vpp3",
BundleIdentifier: "com.app.vpp3",
}, nil)
require.NoError(t, err)
va4, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_4", Platform: fleet.IOSPlatform}}, Name: "vpp4",
BundleIdentifier: "com.app.vpp4",
}, nil)
require.NoError(t, err)
vpp2, vpp3, vpp4 := va2.AdamID, va3.AdamID, va4.AdamID
// create an installation request for vpp1 and vpp2, leaving vpp3 and vpp4 as
// available only
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp1, user)
vpp2CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp2, user)
// 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)
require.NotEmpty(t, vpp2bCmdUUID)
opts.IncludeAvailableForInstall = false
opts.ListOptions.MatchQuery = ""
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected))}, meta)
compareResults(expected, sw, true) // i3 is for team, i2 is available (excluded)
expected["vpp1ios_apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp1",
Source: "ios_apps",
Status: expectStatus(fleet.SoftwareInstalled),
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1, Platform: "ios", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1CmdUUID}},
}
expected["vpp2ios_apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp2",
Source: "ios_apps",
Status: expectStatus(fleet.SoftwareInstallPending),
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp2, Platform: "ios", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp2bCmdUUID}},
}
expected["vpp3ios_apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp3",
Source: "ios_apps",
Status: nil,
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp3, Platform: "ios", SelfService: ptr.Bool(false)},
}
expected["vpp4ios_apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp4",
Source: "ios_apps",
Status: nil,
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp4, Platform: "ios", SelfService: ptr.Bool(false)},
}
expectedAvailableOnly := map[string]fleet.HostSoftwareWithInstaller{}
expectedAvailableOnly["vpp1ios_apps"] = expected["vpp1ios_apps"]
expectedAvailableOnly["vpp2ios_apps"] = expected["vpp2ios_apps"]
expectedAvailableOnly["vpp3ios_apps"] = expected["vpp3ios_apps"]
expectedAvailableOnly["vpp4ios_apps"] = expected["vpp4ios_apps"]
opts.IncludeAvailableForInstall = true
opts.IsMDMEnrolled = true
opts.ListOptions.PerPage = 20
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected))}, meta)
compareResults(expected, sw, true)
// Available for install only
opts.OnlyAvailableForInstall = true
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnly))}, meta)
compareResults(expectedAvailableOnly, sw, true)
opts.OnlyAvailableForInstall = false
}
func testListHostSoftwareWithVPPApps(t *testing.T, ds *Datastore) {
ctx := context.Background()
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
nanoEnroll(t, ds, host, false)
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &tm.ID
numberOfApps := 5
// create a second host and add it to the team
anotherHost := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
nanoEnroll(t, ds, anotherHost, false)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{anotherHost.ID}))
require.NoError(t, err)
anotherHost.TeamID = &tm.ID
software := []fleet.Software{}
for i := 0; i < numberOfApps; i++ {
software = append(software, fleet.Software{
Name: fmt.Sprintf("z%d", i),
Version: fmt.Sprintf("0.0.%d", i),
Source: "apps",
BundleIdentifier: fmt.Sprintf("com.example.%d", i),
})
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
time.Sleep(time.Second)
vPPApp := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}},
Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
LatestVersion: "1.0.0",
}
va1, err := ds.InsertVPPAppWithTeam(ctx, vPPApp, &tm.ID)
require.NoError(t, err)
vpp1 := va1.AdamID
// vpp1 is not installed yet, but host is mdm enrolled so it should show up
sw, _, err := ds.ListHostSoftware(
ctx,
anotherHost,
fleet.HostSoftwareTitleListOptions{
OnlyAvailableForInstall: true,
IsMDMEnrolled: true,
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
},
)
require.NoError(t, err)
assert.Len(t, sw, 1)
assert.Equal(t, vPPApp.Name, sw[0].Name)
// vpp1 is not installed yet, but host is not mdm enrolled so it should not show up
sw, _, err = ds.ListHostSoftware(
ctx,
anotherHost,
fleet.HostSoftwareTitleListOptions{
OnlyAvailableForInstall: true,
IsMDMEnrolled: false,
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
},
)
require.NoError(t, err)
assert.Len(t, sw, 0)
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp1, user)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host.ID, "")
require.NoError(t, err)
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// Insert software entry for vpp app
res, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp.Name, "1.2.3", "apps", vPPApp.BundleIdentifier, vPPApp.TitleID, hex.EncodeToString([]byte("vpp1")),
)
require.NoError(t, err)
time.Sleep(time.Second)
softwareID, err := res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, host.ID, softwareID)
require.NoError(t, err)
hostLibraryOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 20},
SelfServiceOnly: false,
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: true,
IsMDMEnrolled: true,
}
// vpp1 installed and inventoried by osquery
// ensure we don't lose install data from host_vpp_software_installs
sw, _, err = ds.ListHostSoftware(ctx, host, hostLibraryOpts)
require.NoError(t, err)
assert.Len(t, sw, 1)
assert.NotNil(t, sw[0].InstalledVersions)
assert.NotNil(t, sw[0].AppStoreApp.LastInstall)
assert.Equal(t, vpp1CmdUUID, sw[0].AppStoreApp.LastInstall.CommandUUID)
assert.NotNil(t, sw[0].AppStoreApp.LastInstall.InstalledAt)
opts := fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: uint(numberOfApps - 1), IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
sw, meta, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, numberOfApps-1)
assert.Equal(t, numberOfApps+1, int(meta.TotalResults))
assert.True(t, meta.HasNextResults)
// The vpp app returned should have an installed version as per the sql above.
// However this is a special case because we are not
// passing one of the IncludeAvailableForInstall or OnlyAvailableForInstall flags.
// Passing one of those flags would typically add the installed version (from host_software/software) to the list
// of InstalledVersions. But we need to make a special case for this when neither of those flags are set.
assert.Len(t, sw[0].InstalledVersions, 1)
assert.Equal(t, "1.2.3", sw[0].InstalledVersions[0].Version)
assert.Equal(t, "apps", sw[0].InstalledVersions[0].Source)
assert.Equal(t, vPPApp.BundleIdentifier, sw[0].InstalledVersions[0].BundleIdentifier)
// The vpp app is installed by fleet, and also has been inventoried by osquery
// Ensure we don't lose the version for the vpp app
opts.IncludeAvailableForInstall = true
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw[0].InstalledVersions, 1)
assert.Equal(t, "1.2.3", sw[0].InstalledVersions[0].Version)
// have the second host install a vpp app, but not by fleet
res, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp.Name, "0.1.0", "apps", vPPApp.BundleIdentifier, vPPApp.TitleID, []byte("vpp1v0.1.0"),
)
require.NoError(t, err)
time.Sleep(time.Second)
softwareID, err = res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, anotherHost.ID, softwareID)
require.NoError(t, err)
// when filtering by available for install ensure
// that the pre-installed app store app that has a match vpp app is returned
opts = fleet.HostSoftwareTitleListOptions{OnlyAvailableForInstall: true, ListOptions: fleet.ListOptions{PerPage: uint(numberOfApps - 1), IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"}}
sw, _, err = ds.ListHostSoftware(ctx, anotherHost, opts)
require.NoError(t, err)
assert.Len(t, sw, 1)
assert.Equal(t, vPPApp.Name, sw[0].Name)
assert.Equal(t, vPPApp.AdamID, sw[0].AppStoreApp.AppStoreID)
assert.Equal(t, "0.1.0", sw[0].InstalledVersions[0].Version)
assert.Nil(t, sw[0].Status)
// insert an icon
icon, err := ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{
TeamID: tm.ID,
TitleID: va1.TitleID,
StorageID: "storage-id-1",
Filename: "test-icon.png",
})
require.NoError(t, err)
opts = fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 0, PerPage: 20},
SelfServiceOnly: false,
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: true,
IsMDMEnrolled: true,
}
sw, _, err = ds.ListHostSoftware(ctx, anotherHost, opts)
require.NoError(t, err)
assert.Len(t, sw, 1)
assert.Equal(t, icon.IconUrl(), *sw[0].IconUrl)
}
func testListHostSoftwareVPPSelfService(t *testing.T, ds *Datastore) {
ctx := context.Background()
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
nanoEnroll(t, ds, host, false)
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &tm.ID
opts := fleet.HostSoftwareTitleListOptions{
SelfServiceOnly: true,
IsMDMEnrolled: true,
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: false,
VulnerableOnly: false,
KnownExploit: false,
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
}
// setup vpp
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
time.Sleep(time.Second)
vPPApp := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{SelfService: true, VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}},
Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
LatestVersion: "1.0.0",
}
va1, err := ds.InsertVPPAppWithTeam(ctx, vPPApp, &tm.ID)
require.NoError(t, err)
vpp1 := va1.AdamID
// vpp1 is self service, not installed yet, so it should show as available for install
sw, _, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, 1)
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp1, user)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host.ID, "")
require.NoError(t, err)
vPPApp2 := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{SelfService: true, VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.MacOSPlatform}},
Name: "vpp2",
BundleIdentifier: "com.app.vpp2",
LatestVersion: "1.0.1",
}
_, err = ds.InsertVPPAppWithTeam(ctx, vPPApp2, &tm.ID)
require.NoError(t, err)
res, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp2.Name, "0.5.0", "apps", vPPApp2.BundleIdentifier, vPPApp2.TitleID, hex.EncodeToString([]byte("vpp2")),
)
require.NoError(t, err)
time.Sleep(time.Second)
softwareID, err := res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, host.ID, softwareID)
require.NoError(t, err)
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, 2)
// pending install
assert.NotNil(t, sw[0].AppStoreApp)
assert.Equal(t, "1.0.0", sw[0].AppStoreApp.Version)
assert.Equal(t, fleet.SoftwareInstallerStatus("pending_install"), *sw[0].Status)
assert.NotNil(t, sw[0].AppStoreApp.LastInstall)
assert.Nil(t, sw[0].InstalledVersions)
// installed but not by fleet, so status is nil
assert.NotNil(t, sw[1].AppStoreApp)
assert.Equal(t, "1.0.1", sw[1].AppStoreApp.Version)
assert.Nil(t, sw[1].Status)
assert.Nil(t, sw[1].AppStoreApp.LastInstall)
assert.NotNil(t, sw[1].InstalledVersions)
assert.Equal(t, "0.5.0", sw[1].InstalledVersions[0].Version)
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// Insert software entry for vpp app
res, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp.Name, "1.2.3", "apps", vPPApp.BundleIdentifier, vPPApp.TitleID, hex.EncodeToString([]byte("vpp1")),
)
require.NoError(t, err)
time.Sleep(time.Second)
softwareID, err = res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, host.ID, softwareID)
require.NoError(t, err)
// vpp apps should not be returned in self service
// if the host is not mdm enrolled
opts.IsMDMEnrolled = false
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, 0)
}
func testCreateIntermediateInstallFailureRecord(t *testing.T, ds *Datastore) {
ctx := t.Context()
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
user := test.NewUser(t, ds, "test", "test@example.com", true)
// Create a software installer using the standard method
tfr, err := fleet.NewTempFileReader(strings.NewReader("test-package"), t.TempDir)
require.NoError(t, err)
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: `echo 'foo'`,
UninstallScript: `echo 'uninstall'`,
InstallerFile: tfr,
StorageID: "test-storage",
Filename: "installer.pkg",
Title: "test-app",
Version: "v1.0.0",
Source: "apps",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// Create a pending install request with a specific execution ID and created_at for testing
// We need to use raw SQL here to set a specific execution_id and created_at that we can reference in the test
originalCreatedAt := time.Now().Add(-1 * time.Hour).UTC().Truncate(time.Microsecond) // Set to 1 hour ago
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, user_id, policy_id, self_service, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
"original-uuid", host.ID, installerID, user.ID, nil, false, originalCreatedAt)
return err
})
// Create an intermediate failure record
failedExecID, installResult, isNewRecord, err := ds.CreateIntermediateInstallFailureRecord(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: "original-uuid",
InstallScriptExitCode: ptr.Int(1),
InstallScriptOutput: ptr.String("network timeout"),
RetriesRemaining: 2,
})
require.NoError(t, err)
require.NotEmpty(t, failedExecID)
require.NotNil(t, installResult)
require.True(t, isNewRecord, "First call should create a new record")
require.Equal(t, "test-app", installResult.SoftwareTitle)
require.Equal(t, "installer.pkg", installResult.SoftwarePackage)
require.Equal(t, user.ID, *installResult.UserID)
require.Nil(t, installResult.PolicyID)
require.False(t, installResult.SelfService)
// Verify original record is still pending using GetSoftwareInstallResults
originalResult, err := ds.GetSoftwareInstallResults(ctx, "original-uuid")
require.NoError(t, err)
require.Equal(t, fleet.SoftwareInstallPending, originalResult.Status)
require.Nil(t, originalResult.InstallScriptExitCode)
// Verify new failed record exists with failure details
failedResult, err := ds.GetSoftwareInstallResults(ctx, failedExecID)
require.NoError(t, err)
require.Equal(t, fleet.SoftwareInstallFailed, failedResult.Status)
require.NotNil(t, failedResult.InstallScriptExitCode)
require.Equal(t, 1, *failedResult.InstallScriptExitCode)
require.NotNil(t, failedResult.Output)
require.Equal(t, "network timeout", *failedResult.Output)
// Verify the created_at timestamp was preserved from the original record
var failedRecordCreatedAt time.Time
err = ds.writer(ctx).GetContext(ctx, &failedRecordCreatedAt, `
SELECT created_at FROM host_software_installs WHERE execution_id = ?`, failedExecID)
require.NoError(t, err)
require.Equal(t, originalCreatedAt, failedRecordCreatedAt, "Failed record should preserve original created_at timestamp")
// Verify that we now have 2 distinct records (this still needs raw SQL as there's no method for counting)
var count int
err = ds.writer(ctx).GetContext(ctx, &count, `
SELECT COUNT(*) FROM host_software_installs WHERE host_id = ?`, host.ID)
require.NoError(t, err)
require.Equal(t, 2, count)
// Test idempotency: calling again with same retries_remaining should return same UUID and not create new record
failedExecID2, installResult2, isNewRecord2, err := ds.CreateIntermediateInstallFailureRecord(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: "original-uuid",
InstallScriptExitCode: ptr.Int(1),
InstallScriptOutput: ptr.String("network timeout updated"),
RetriesRemaining: 2,
})
require.NoError(t, err)
require.Equal(t, failedExecID, failedExecID2, "Should generate same UUID for same retries_remaining")
require.NotNil(t, installResult2)
require.False(t, isNewRecord2, "Second call should update existing record")
// Verify still only 2 records (idempotent)
err = ds.writer(ctx).GetContext(ctx, &count, `
SELECT COUNT(*) FROM host_software_installs WHERE host_id = ?`, host.ID)
require.NoError(t, err)
require.Equal(t, 2, count, "Should not create duplicate record")
// Verify the output was updated
updatedResult, err := ds.GetSoftwareInstallResults(ctx, failedExecID2)
require.NoError(t, err)
require.Equal(t, "network timeout updated", *updatedResult.Output)
// Test with different retries_remaining creates new record
failedExecID3, _, isNewRecord3, err := ds.CreateIntermediateInstallFailureRecord(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: "original-uuid",
InstallScriptExitCode: ptr.Int(1),
InstallScriptOutput: ptr.String("network timeout"),
RetriesRemaining: 1,
})
require.NoError(t, err)
require.NotEqual(t, failedExecID, failedExecID3, "Should generate different UUID for different retries_remaining")
require.True(t, isNewRecord3, "Different retries_remaining should create new record")
// Verify now have 3 records
err = ds.writer(ctx).GetContext(ctx, &count, `
SELECT COUNT(*) FROM host_software_installs WHERE host_id = ?`, host.ID)
require.NoError(t, err)
require.Equal(t, 3, count, "Should create new record for different retries_remaining")
}
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()
uninstallScript := `echo 'bar'`
resUninstall, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`,
uninstallScript, uninstallScript)
if err != nil {
return err
}
uninstallScriptContentID, _ := resUninstall.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, extension, version, install_script_content_id, uninstall_script_content_id, storage_id, platform, package_ids)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
titleID, "installer.pkg", "pkg", "v1.0.0", scriptContentID, uninstallScriptContentID, []byte("test"), "darwin", "[]")
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"),
}
wasCanceled, err := ds.SetHostSoftwareInstallResult(ctx, want)
require.NoError(t, err)
require.False(t, wasCanceled)
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))
}
func testListHostSoftwareInstallThenTransferTeam(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
IncludeAvailableForInstall: true,
}
test.CreateInsertGlobalVPPToken(t, ds)
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &team1.ID
// add a single "externally-installed" software for that host
software := []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
// create a software installer for team 1
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installerTm1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
InstallerFile: tfr1,
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
TeamID: &team1.ID,
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// install it on the host
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerTm1, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)
// add a VPP app for team 1
vppTm1, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, &team1.ID)
require.NoError(t, err)
// fail to install it on the host
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vppTm1.AdamID, user)
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusError)
// add the successful installer to the reported installed software
software = []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
{Name: "file1", Version: "1.0", Source: "apps"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
// listing the host's software (including available for install) at this
// point lists "a", "file1" and "vpp1" (because of the install attempt)
sw, meta, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 3)
require.EqualValues(t, 3, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "file1")
require.Nil(t, sw[1].AppStoreApp)
require.NotNil(t, sw[1].SoftwarePackage)
require.Equal(t, sw[2].Name, "vpp1")
require.NotNil(t, sw[2].AppStoreApp)
require.Nil(t, sw[2].SoftwarePackage)
// move host to team 2
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team2.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &team2.ID
// listing the host's software (including available for install) should now
// only list "a" and "file1" (because they are actually installed) and not
// link them to the installer/VPP app. With and without available software
// should result in the same rows (no available software in that new team).
for _, b := range []bool{true, false} {
opts.IncludeAvailableForInstall = b
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.EqualValues(t, 2, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "file1")
require.Nil(t, sw[1].AppStoreApp)
require.Nil(t, sw[1].SoftwarePackage)
}
}
func testListHostSoftwareInstallThenDeleteInstallers(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
IncludeAvailableForInstall: true,
}
test.CreateInsertGlobalVPPToken(t, ds)
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &team1.ID
// add a single "externally-installed" software for that host
software := []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
// create a software installer for team 1
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installerTm1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
InstallerFile: tfr1,
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
TeamID: &team1.ID,
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// fail to install it on the host
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerTm1, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(1),
})
require.NoError(t, err)
// add a VPP app for team 1
vppTm1, err := ds.InsertVPPAppWithTeam(ctx,
&fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
BundleIdentifier: "com.app.vpp1", LatestVersion: "1.0",
}, &team1.ID)
require.NoError(t, err)
// install it on the host
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vppTm1.AdamID, user)
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// add the successful VPP app to the reported installed software
software = []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "chrome_extensions"},
{Name: "vpp1", Version: "1.0", Source: "apps", BundleIdentifier: "com.app.vpp1"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
// listing the host's software (including available for install) at this
// point lists "a", "file1" and "vpp1" (because of the install attempt)
sw, meta, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 3)
require.EqualValues(t, 3, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "file1")
require.Nil(t, sw[1].AppStoreApp)
require.NotNil(t, sw[1].SoftwarePackage)
require.Equal(t, sw[2].Name, "vpp1")
require.NotNil(t, sw[2].AppStoreApp)
require.Nil(t, sw[2].SoftwarePackage)
// delete both installers
err = ds.DeleteSoftwareInstaller(ctx, installerTm1)
require.NoError(t, err)
err = ds.DeleteVPPAppFromTeam(ctx, &team1.ID, vppTm1.VPPAppID)
require.NoError(t, err)
// listing the host's software (including available for install) should now
// only list "a" and "vpp1" (because they are actually installed) and not
// link them to the installer/VPP app. With and without available software
// should result in the same rows (no available software anymore).
for _, b := range []bool{true, false} {
opts.IncludeAvailableForInstall = b
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.EqualValues(t, 2, meta.TotalResults)
require.Equal(t, sw[0].Name, "a")
require.Nil(t, sw[0].AppStoreApp)
require.Nil(t, sw[0].SoftwarePackage)
require.Equal(t, sw[1].Name, "vpp1")
require.Nil(t, sw[1].AppStoreApp)
require.Nil(t, sw[1].SoftwarePackage)
}
}
func testListSoftwareVersionsVulnerabilityFilters(t *testing.T, ds *Datastore) {
ctx := context.Background()
host := test.NewHost(t, ds, "host", "", "hostkey", "hostuuid", time.Now())
software := []fleet.Software{
{Name: "chrome", Version: "0.0.1", Source: "apps", BundleIdentifier: "com.example.chrome"},
{Name: "chrome", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.chrome"},
{Name: "safari", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.safari"},
{Name: "safari", Version: "0.0.1", Source: "apps", BundleIdentifier: "com.example.safari"},
{Name: "firefox", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.firefox"},
{Name: "edge", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.edge"},
{Name: "brave", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.brave"},
{Name: "opera", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.opera"},
{Name: "internet explorer", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.ie"},
{Name: "netscape", Version: "0.0.3", Source: "apps", BundleIdentifier: "com.example.netscape"},
}
sw, err := ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
var chrome001 uint
var safari001 uint
var firefox003 uint
var edge003 uint
var brave003 uint
var opera003 uint
var ie003 uint
for s := range sw.Inserted {
switch {
case sw.Inserted[s].Name == "chrome" && sw.Inserted[s].Version == "0.0.1":
chrome001 = sw.Inserted[s].ID
case sw.Inserted[s].Name == "safari" && sw.Inserted[s].Version == "0.0.1":
safari001 = sw.Inserted[s].ID
case sw.Inserted[s].Name == "firefox" && sw.Inserted[s].Version == "0.0.3":
firefox003 = sw.Inserted[s].ID
case sw.Inserted[s].Name == "edge" && sw.Inserted[s].Version == "0.0.3":
edge003 = sw.Inserted[s].ID
case sw.Inserted[s].Name == "brave" && sw.Inserted[s].Version == "0.0.3":
brave003 = sw.Inserted[s].ID
case sw.Inserted[s].Name == "opera" && sw.Inserted[s].Version == "0.0.3":
opera003 = sw.Inserted[s].ID
case sw.Inserted[s].Name == "internet explorer" && sw.Inserted[s].Version == "0.0.3":
ie003 = sw.Inserted[s].ID
}
}
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: chrome001,
CVE: "CVE-2024-1234",
}, fleet.NVDSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: safari001,
CVE: "CVE-2024-1235",
}, fleet.NVDSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: firefox003,
CVE: "CVE-2024-1236",
}, fleet.NVDSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: edge003,
CVE: "CVE-2024-1237",
}, fleet.NVDSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: brave003,
CVE: "CVE-2024-1238",
}, fleet.NVDSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: opera003,
CVE: "CVE-2024-1239",
}, fleet.NVDSource)
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
SoftwareID: ie003,
CVE: "CVE-2024-1240",
}, fleet.NVDSource)
require.NoError(t, err)
err = ds.InsertCVEMeta(ctx, []fleet.CVEMeta{
{
// chrome
CVE: "CVE-2024-1234",
CVSSScore: ptr.Float64(7.5),
CISAKnownExploit: ptr.Bool(true),
},
{
// safari
CVE: "CVE-2024-1235",
CVSSScore: ptr.Float64(7.5),
CISAKnownExploit: ptr.Bool(false),
},
{
// firefox
CVE: "CVE-2024-1236",
CVSSScore: ptr.Float64(8.0),
CISAKnownExploit: ptr.Bool(true),
},
{
// edge
CVE: "CVE-2024-1237",
CVSSScore: ptr.Float64(8.0),
CISAKnownExploit: ptr.Bool(false),
},
{
// brave
CVE: "CVE-2024-1238",
CVSSScore: ptr.Float64(9.0),
CISAKnownExploit: ptr.Bool(true),
},
// CVE-2024-1239 for opera has no CVE Meta
{
// internet explorer
CVE: "CVE-2024-1240",
CVSSScore: nil,
CISAKnownExploit: nil,
},
})
require.NoError(t, err)
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
type swVersion struct {
Name string
Version string
}
tc := []struct {
name string
opts fleet.SoftwareListOptions
expected []swVersion
err error
}{
{
name: "vulnerable only",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name"},
VulnerableOnly: true,
},
expected: []swVersion{
{
Name: "brave",
Version: "0.0.3",
},
{
Name: "chrome",
Version: "0.0.1",
},
{
Name: "edge",
Version: "0.0.3",
},
{
Name: "firefox",
Version: "0.0.3",
},
{
Name: "internet explorer",
Version: "0.0.3",
},
{
Name: "opera",
Version: "0.0.3",
},
{
Name: "safari",
Version: "0.0.1",
},
},
},
{
name: "known exploit true",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
KnownExploit: true,
},
expected: []swVersion{
{
Name: "brave",
Version: "0.0.3",
},
{
Name: "chrome",
Version: "0.0.1",
},
{
Name: "firefox",
Version: "0.0.3",
},
},
},
{
name: "minimum cvss 8.0",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MinimumCVSS: 8.0,
},
expected: []swVersion{
{
Name: "brave",
Version: "0.0.3",
},
{
Name: "edge",
Version: "0.0.3",
},
{
Name: "firefox",
Version: "0.0.3",
},
},
},
{
name: "minimum cvss 7.9",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MinimumCVSS: 7.9,
},
expected: []swVersion{
{
Name: "brave",
Version: "0.0.3",
},
{
Name: "edge",
Version: "0.0.3",
},
{
Name: "firefox",
Version: "0.0.3",
},
},
},
{
name: "minimum cvss 8.0 and known exploit",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MinimumCVSS: 8.0,
KnownExploit: true,
},
expected: []swVersion{
{
Name: "brave",
Version: "0.0.3",
},
{
Name: "firefox",
Version: "0.0.3",
},
},
},
{
name: "minimum cvss 7.5 and known exploit",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MinimumCVSS: 7.5,
KnownExploit: true,
},
expected: []swVersion{
{
Name: "brave",
Version: "0.0.3",
},
{
Name: "chrome",
Version: "0.0.1",
},
{
Name: "firefox",
Version: "0.0.3",
},
},
},
{
name: "maximum cvss 7.5",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MaximumCVSS: 7.5,
},
expected: []swVersion{
{
Name: "chrome",
Version: "0.0.1",
},
{
Name: "safari",
Version: "0.0.1",
},
},
},
{
name: "maximum cvss 7.6",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MaximumCVSS: 7.6,
},
expected: []swVersion{
{
Name: "chrome",
Version: "0.0.1",
},
{
Name: "safari",
Version: "0.0.1",
},
},
},
{
name: "maximum cvss 7.5 and known exploit",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MaximumCVSS: 7.5,
KnownExploit: true,
},
expected: []swVersion{
{
Name: "chrome",
Version: "0.0.1",
},
},
},
{
name: "minimum cvss 7.5 and maximum cvss 8.0",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MinimumCVSS: 7.5,
MaximumCVSS: 8.0,
},
expected: []swVersion{
{
Name: "chrome",
Version: "0.0.1",
},
{
Name: "edge",
Version: "0.0.3",
},
{
Name: "firefox",
Version: "0.0.3",
},
{
Name: "safari",
Version: "0.0.1",
},
},
},
{
name: "minimum cvss 7.5 and maximum cvss 8.0 and known exploit",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{OrderKey: "name", OrderDirection: fleet.OrderAscending},
IncludeCVEScores: true,
VulnerableOnly: true,
MinimumCVSS: 7.5,
MaximumCVSS: 8.0,
KnownExploit: true,
},
expected: []swVersion{
{
Name: "chrome",
Version: "0.0.1",
},
{
Name: "firefox",
Version: "0.0.3",
},
},
},
{
name: "err if vulnerableOnly is not set with MinimumCVSS",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{},
MinimumCVSS: 7.5,
},
err: fleet.NewInvalidArgumentError("query", "min_cvss_score, max_cvss_score, and exploit can only be provided with vulnerable=true"),
},
{
name: "err if vulnerableOnly is not set with MaximumCVSS",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{},
MaximumCVSS: 7.5,
},
err: fleet.NewInvalidArgumentError("query", "min_cvss_score, max_cvss_score, and exploit can only be provided with vulnerable=true"),
},
{
name: "err if vulnerableOnly is not set with KnownExploit",
opts: fleet.SoftwareListOptions{
ListOptions: fleet.ListOptions{},
KnownExploit: true,
},
err: fleet.NewInvalidArgumentError("query", "min_cvss_score, max_cvss_score, and exploit can only be provided with vulnerable=true"),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
sw, _, err := ds.ListSoftware(ctx, tt.opts)
if tt.err != nil {
require.Error(t, err)
require.Equal(t, tt.err, err)
return
}
require.Len(t, sw, len(tt.expected))
for i, s := range sw {
require.Equal(t, tt.expected[i].Name, s.Name)
require.Equal(t, tt.expected[i].Version, s.Version)
}
count, err := ds.CountSoftware(ctx, tt.opts)
require.NoError(t, err)
require.Equal(t, len(tt.expected), count)
})
}
}
func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a host
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
anotherHost := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, anotherHost, false)
thirdHost := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, thirdHost, false)
time.Sleep(time.Second) // ensure the labels_updated_at timestamp is before labels creation
// create a software installer
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installer1 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
PreInstallQuery: "SELECT 1",
PostInstallScript: "world",
UninstallScript: "goodbye",
InstallerFile: tfr1,
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
UserID: user1.ID,
BundleIdentifier: "bi1",
Platform: "darwin",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID1, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, installer1)
require.NoError(t, err)
// self-service software installer
tfr2, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
selfServiceinstaller := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello 2",
PreInstallQuery: "SELECT 2",
PostInstallScript: "world 2",
UninstallScript: "goodbye 2",
InstallerFile: tfr2,
StorageID: "storage 2",
Filename: "file2",
Title: "file2",
Version: "1.0",
Source: "apps",
UserID: user1.ID,
BundleIdentifier: "bi2",
Platform: "darwin",
SelfService: true,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
selfServiceInstallerID, selfServiceTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, selfServiceinstaller)
require.NoError(t, err)
// install software installer on host via fleet
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, thirdHost.ID, selfServiceInstallerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: thirdHost.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)
installedSoftware := []fleet.Software{
{
Name: "file2",
Version: "1.0",
Source: "apps",
TitleID: &selfServiceTitleID,
},
}
mutationResults, err := ds.UpdateHostSoftware(ctx, thirdHost.ID, installedSoftware)
require.NoError(t, err)
assert.Len(t, mutationResults.Inserted, len(installedSoftware))
softwareAlreadyInstalled := fleet.Software{Name: "file1", Version: "1.0.1", Source: "apps", BundleIdentifier: "bi1"}
// Host has software installed, but not by Fleet, that matches the software installer available
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
titleIDUint := titleID
softwareAlreadyInstalled.TitleID = &titleIDUint
res, err := q.ExecContext(ctx, `INSERT INTO software (name, source, bundle_identifier, version, title_id, checksum) VALUES (?, ?, ?, ?, ?, ?)`,
softwareAlreadyInstalled.Name, softwareAlreadyInstalled.Source, softwareAlreadyInstalled.BundleIdentifier, softwareAlreadyInstalled.Version, titleID, []byte("checksum"))
if err != nil {
return err
}
softwareID, err := res.LastInsertId()
if err != nil {
return err
}
softwareAlreadyInstalled.ID = uint(softwareID)
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`,
anotherHost.ID, softwareID)
if err != nil {
return err
}
return nil
})
// we should see installer1, since it has no label associated yet
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
},
IncludeAvailableForInstall: true,
}
expectedInstallers := map[string]*fleet.SoftwarePackageOrApp{
installer1.Filename: {
Name: installer1.Filename,
Version: installer1.Version,
Platform: installer1.Platform,
SelfService: ptr.Bool(false),
},
selfServiceinstaller.Filename: {
Name: selfServiceinstaller.Filename,
Version: selfServiceinstaller.Version,
Platform: selfServiceinstaller.Platform,
SelfService: ptr.Bool(true),
},
}
checkSoftware := func(swList []*fleet.HostSoftwareWithInstaller, excludeNames ...string) {
for _, got := range swList {
want, ok := expectedInstallers[got.SoftwarePackage.Name]
if slices.Contains(excludeNames, got.SoftwarePackage.Name) {
require.False(t, ok)
continue
}
require.True(t, ok)
require.Equal(t, want, got.SoftwarePackage)
}
}
software, _, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software)
software, _, err = ds.ListHostSoftware(ctx, anotherHost, opts)
require.NoError(t, err)
require.Len(t, software, 2)
// software[0] was inserted into software/host_software in the ad-hoc SQL execution above, so has an installed version
require.Equal(t, softwareAlreadyInstalled.Name, software[0].Name)
require.Equal(t, softwareAlreadyInstalled.Version, software[0].InstalledVersions[0].Version)
require.Equal(t, installer1.Version, software[0].SoftwarePackage.Version)
require.Len(t, software[0].InstalledVersions, 1)
require.Equal(t, mutationResults.Inserted[0].Name, software[1].Name)
// software[1] is *not* installed but *is* available for install
require.Nil(t, software[1].InstalledVersions)
require.Equal(t, selfServiceinstaller.Version, software[1].SoftwarePackage.Version)
software, _, err = ds.ListHostSoftware(ctx, thirdHost, opts)
require.NoError(t, err)
require.Len(t, software, 2)
require.Equal(t, softwareAlreadyInstalled.Name, software[0].Name)
require.Nil(t, software[0].InstalledVersions)
require.Equal(t, installer1.Version, software[0].SoftwarePackage.Version)
require.Equal(t, mutationResults.Inserted[0].Name, software[1].Name)
require.Equal(t, mutationResults.Inserted[0].Version, software[1].InstalledVersions[0].Version)
require.Equal(t, selfServiceinstaller.Version, software[1].SoftwarePackage.Version)
// installer1 should be in scope since it has no labels
scoped, err := ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
hostsInScope, err := ds.GetIncludedHostIDMapForSoftwareInstaller(ctx, installerID1)
require.NoError(t, err)
require.Contains(t, hostsInScope, host.ID)
require.Contains(t, hostsInScope, anotherHost.ID)
require.Contains(t, hostsInScope, thirdHost.ID)
label1, err := ds.NewLabel(ctx, &fleet.Label{Name: "label1" + t.Name()})
require.NoError(t, err)
// assign the label to the hosts
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label1.ID}))
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
require.NoError(t, ds.AddLabelsToHost(ctx, anotherHost.ID, []uint{label1.ID}))
anotherHost.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, anotherHost)
require.NoError(t, err)
require.NoError(t, ds.AddLabelsToHost(ctx, thirdHost.ID, []uint{label1.ID}))
anotherHost.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, anotherHost)
require.NoError(t, err)
time.Sleep(time.Second)
// assign the label to the software installers
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID1, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), selfServiceInstallerID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
// should be empty as the installer label is "exclude any"
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Empty(t, software)
software, _, err = ds.ListHostSoftware(ctx, anotherHost, opts)
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, software[0].InstalledVersions[0].Version, softwareAlreadyInstalled.Version)
require.Nil(t, software[0].SoftwarePackage)
software, _, err = ds.ListHostSoftware(ctx, thirdHost, opts)
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, software[0].InstalledVersions[0].Version, selfServiceinstaller.Version)
require.Nil(t, software[0].SoftwarePackage)
hostsNotInScope, err := ds.GetExcludedHostIDMapForSoftwareInstaller(ctx, installerID1)
require.NoError(t, err)
require.Contains(t, hostsNotInScope, host.ID)
// installer1 should be out of scope since the label is "exclude any"
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.False(t, scoped)
// self-service flag
selfServiceOpts := fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 10}, IncludeAvailableForInstall: true, SelfServiceOnly: true, IsMDMEnrolled: true}
// softwareAlreadyInstalled should not be returned because it is not self service
software, _, err = ds.ListHostSoftware(ctx, anotherHost, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// selfServiceinstaller is self service however, excluded by label so should not be returned
software, _, err = ds.ListHostSoftware(ctx, thirdHost, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 0)
selfServiceOpts.OnlyAvailableForInstall = true
software, _, err = ds.ListHostSoftware(ctx, anotherHost, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 0)
software, _, err = ds.ListHostSoftware(ctx, thirdHost, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// Update the label to be "include any"
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID1, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software)
// Now installer1 is in scope again: label is "include any"
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// Add an installer. No label yet.
installer2 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
PreInstallQuery: "SELECT 1",
PostInstallScript: "world",
UninstallScript: "goodbye",
InstallerFile: tfr1,
StorageID: "storage3",
Filename: "file3",
Title: "file3",
Version: "2.0",
Source: "apps",
UserID: user1.ID,
BundleIdentifier: "bi3",
Platform: "darwin",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID2, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, installer2)
require.NoError(t, err)
expectedInstallers[installer2.Filename] = &fleet.SoftwarePackageOrApp{
Name: installer2.Filename,
Version: installer2.Version,
Platform: installer2.Platform,
SelfService: ptr.Bool(false),
}
// There's 2 installers now: installerID1 and installerID2 (because it has no labels associated)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software)
// Add "exclude any" labels to installer2
label2, err := ds.NewLabel(ctx, &fleet.Label{Name: "label2" + t.Name()})
require.NoError(t, err)
label3, err := ds.NewLabel(ctx, &fleet.Label{Name: "label3" + t.Name()})
require.NoError(t, err)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID2, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{
label2.Name: {LabelName: label2.Name, LabelID: label2.ID},
label3.Name: {LabelName: label3.Name, LabelID: label3.ID},
},
}, softwareTypeInstaller)
require.NoError(t, err)
// Now host has label1, label2
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label2.ID}))
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
time.Sleep(time.Second)
// List should be back to just installer1
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software, installer2.Filename)
// installer1 is still in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// installer2 is out of scope, because host has label2
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID2, host.ID)
require.NoError(t, err)
require.False(t, scoped)
// Add an installer. No label yet.
installer3 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
PreInstallQuery: "SELECT 1",
PostInstallScript: "world",
UninstallScript: "goodbye",
InstallerFile: tfr1,
StorageID: "storage4",
Filename: "file4",
Title: "file4",
Version: "3.0",
Source: "apps",
UserID: user1.ID,
BundleIdentifier: "bi4",
Platform: "darwin",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID3, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, installer3)
require.NoError(t, err)
time.Sleep(time.Second)
expectedInstallers[installer3.Filename] = &fleet.SoftwarePackageOrApp{
Name: installer3.Filename,
Version: installer3.Version,
Platform: installer3.Platform,
SelfService: ptr.Bool(false),
}
// Add a new label and apply it to the installer. There are no hosts with this label.
label4, err := ds.NewLabel(ctx, &fleet.Label{Name: "label4" + t.Name(), LabelMembershipType: fleet.LabelMembershipTypeDynamic})
require.NoError(t, err)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID3, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label4.Name: {LabelName: label4.Name, LabelID: label4.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
// We should have [installerID1, installerID3], but the exclude any label has
// no results for this host yet, so it's just installerID1 for now.
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, software, 1)
// installer1 is still in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// installer3 is not in scope yet, because label is "exclude any" and host doesn't have results
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID3, host.ID)
require.NoError(t, err)
require.False(t, scoped)
// mark as if label had been reported (but host is still not a member)
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
time.Sleep(time.Second)
// now has 2 software (installer1 and 3)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software, installer2.Filename)
// installer1 is still in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// installer3 is in scope, because label is "exclude any" and host doesn't have the label
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID3, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// Now include hosts with label4. No host has this label, so we shouldn't see installerID3 anymore.
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID3, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label4.Name: {LabelName: label4.Name, LabelID: label4.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
// We should have [installerID1]
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software, installer2.Filename, installer3.Filename)
// installer1 is still in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// installer3 is not in scope
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID3, host.ID)
require.NoError(t, err)
require.False(t, scoped)
}
func testListHostSoftwareVulnerabileAndVPP(t *testing.T, ds *Datastore) {
ctx := context.Background()
// filter by only vulnerable software
vulnerableOnlyOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
},
VulnerableOnly: true,
IncludeAvailableForInstall: true,
}
// filter by has known exploit
knownExploitOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
},
VulnerableOnly: true,
KnownExploit: true,
}
// filter by min CVSSScore
minCVSSScoreOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
},
VulnerableOnly: true,
MinimumCVSS: 2.0,
}
// filter by max CVSSScore
maxCVSSScoreOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
},
VulnerableOnly: true,
MaximumCVSS: 1.5,
}
now := time.Now().UTC().Truncate(time.Second)
// create a user
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
// create a team
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
// create a host on team
tmHost := test.NewHost(t, ds, "host", "", "hostkey", "hostuuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, tmHost, false)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{tmHost.ID}))
require.NoError(t, err)
tmHost.TeamID = &tm.ID
// add software to the host
software := []fleet.Software{
{Name: "a", Version: "0.0.1", Source: "ios_apps"},
{Name: "b", Version: "0.0.2", Source: "apps"},
{Name: "c", Version: "0.0.3", Source: "apps"},
}
byNSV := map[string]fleet.Software{}
for _, s := range software {
byNSV[s.Name+s.Source+s.Version] = s
}
mutationResults, err := ds.UpdateHostSoftware(ctx, tmHost.ID, software)
for _, m := range mutationResults.Inserted {
s, ok := byNSV[m.Name+m.Source+m.Version]
assert.True(t, ok)
s.ID = m.ID
byNSV[s.Name+s.Source+s.Version] = s
}
require.NoError(t, err)
assert.Len(t, mutationResults.Inserted, len(software))
bTitleId := mutationResults.Inserted[0].TitleID
// insert vulnerable software with the same software title as c, but this version is not added to host
result, err := ds.writer(ctx).ExecContext(
ctx,
`INSERT INTO software (name, version, source, title_id, checksum) VALUES (?, ?, ?, ?, ?)`,
"c", "0.0.1", "apps", &bTitleId, []byte("c.0.0.1apps"),
)
require.NoError(t, err)
insertedID, err := result.LastInsertId()
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(
ctx,
fleet.SoftwareVulnerability{SoftwareID: uint(insertedID), CVE: "CVE-c-00c1"},
fleet.NVDSource,
)
require.NoError(t, err)
getKey := func(i int) string {
return software[i].Name + software[i].Source + software[i].Version
}
a := getKey(0)
b := getKey(1)
// add vulnerabilities to a and b
vulns := []fleet.SoftwareVulnerability{
{SoftwareID: byNSV[a].ID, CVE: "CVE-a-0001"},
{SoftwareID: byNSV[b].ID, CVE: "CVE-b-0002"},
}
for _, v := range vulns {
_, err = ds.InsertSoftwareVulnerability(ctx, v, fleet.NVDSource)
require.NoError(t, err)
}
// add meta around vulnerabilities
cveMeta := []fleet.CVEMeta{
{
CVE: "CVE-a-0001",
CVSSScore: ptr.Float64(2.5),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(now.Add(-2 * time.Hour)),
Description: "description for CVE-a-0001",
},
{
CVE: "CVE-b-0002",
CVSSScore: ptr.Float64(1.0),
CISAKnownExploit: ptr.Bool(false),
Published: ptr.Time(now),
Description: "description for CVE-b-0002",
},
}
err = ds.InsertCVEMeta(context.Background(), cveMeta)
require.NoError(t, err)
swPaths := map[string]struct{}{}
err = ds.UpdateHostSoftwareInstalledPaths(ctx, tmHost.ID, swPaths, mutationResults)
require.NoError(t, err)
err = ds.ReconcileSoftwareTitles(ctx)
require.NoError(t, err)
var ensureVulnerableState []struct {
HostID *uint `db:"host_id"`
CVE *string `db:"cve"`
}
err = ds.writer(ctx).SelectContext(ctx, &ensureVulnerableState, `
SELECT
host_software.host_id as host_id,
software_cve.cve
FROM software_titles
LEFT JOIN software on software.title_id = software_titles.id
LEFT JOIN software_cve on software_cve.software_id = software.id
LEFT JOIN host_software on host_software.software_id = software.id
WHERE
software_titles.name = 'c'
ORDER BY host_software.host_id IS NOT NULL, host_software.host_id
`)
require.NoError(t, err)
require.Nil(t, ensureVulnerableState[0].HostID)
require.NotNil(t, ensureVulnerableState[0].CVE)
require.Equal(t, ensureVulnerableState[1].HostID, &tmHost.ID)
require.Nil(t, ensureVulnerableState[1].CVE)
// Ensure that software "a" & "b" are returned as they are vulnerable
require.NoError(t, ds.LoadHostSoftware(ctx, tmHost, false))
sw, _, err := ds.ListHostSoftware(ctx, tmHost, vulnerableOnlyOpts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, software[0].Name, sw[0].Name)
require.Equal(t, software[1].Name, sw[1].Name)
// last_software_install
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installerTm1, installerTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
InstallerFile: tfr1,
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
TeamID: &tm.ID,
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, tmHost.ID, installerTm1, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: tmHost.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)
software = append(software, fleet.Software{
Name: "file1",
Version: "1.0",
Source: "apps",
TitleID: &installerTitleID,
})
mutationResults, err = ds.UpdateHostSoftware(ctx, tmHost.ID, software)
require.NoError(t, err)
err = ds.ReconcileSoftwareTitles(ctx)
require.NoError(t, err)
// set up vpp
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
time.Sleep(time.Second) // ensure the labels_updated_at timestamp is before labels creation
// last_vpp_install
vPPApp := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}},
Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}
va1, err := ds.InsertVPPAppWithTeam(ctx, vPPApp, &tm.ID)
require.NoError(t, err)
vpp1 := va1.AdamID
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, tmHost, vpp1, user)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), tmHost.ID, "")
require.NoError(t, err)
createVPPAppInstallResult(t, ds, tmHost, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// Insert software entry for vpp app
res, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp.Name, "0.1.1", "apps", vPPApp.BundleIdentifier, vPPApp.TitleID, hex.EncodeToString([]byte("vpp1")),
)
require.NoError(t, err)
vppSoftwareID, err := res.LastInsertId()
require.NoError(t, err)
time.Sleep(time.Second) // ensure a different created_at timestamp
_, err = ds.InsertSoftwareVulnerability(
ctx,
fleet.SoftwareVulnerability{SoftwareID: uint(vppSoftwareID), CVE: "CVE-vpp1-0001"},
fleet.NVDSource,
)
require.NoError(t, err)
// Ensure that software "a" & "b" are returned as they are the only vulnerable apps at this point
// "vpp1" app is not in inventory yet, so it should not be returned
sw, _, err = ds.ListHostSoftware(ctx, tmHost, vulnerableOnlyOpts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, software[0].Name, sw[0].Name)
require.Equal(t, software[1].Name, sw[1].Name)
// "vpp1" is now in inventory
// "vpp1" although vpp, it is vulnerable software installed on host so make sure it is also returned
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, tmHost.ID, vppSoftwareID)
require.NoError(t, err)
sw, _, err = ds.ListHostSoftware(ctx, tmHost, vulnerableOnlyOpts)
require.NoError(t, err)
require.Len(t, sw, 3)
require.Equal(t, vPPApp.Name, sw[2].Name)
require.Len(t, sw[2].InstalledVersions, 1)
require.Equal(t, "0.1.1", sw[2].InstalledVersions[0].Version)
require.Equal(t, "adam_vpp_1", sw[2].AppStoreApp.AppStoreID)
// remove "vpp1" vulnerability
_, err = ds.writer(ctx).ExecContext(ctx, `
DELETE FROM software_cve
WHERE software_id = ? AND cve = ?
`, vppSoftwareID, "CVE-vpp1-0001")
require.NoError(t, err)
// upcoming_software_install
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "foo",
Source: "bar",
InstallScript: "echo",
TeamID: &tm.ID,
Filename: "foo.pkg",
UserID: user.ID,
Version: "1.0.0",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// insert non-vulnerable software without adding to host
_, err = ds.writer(ctx).ExecContext(
ctx,
`INSERT INTO software (name, version, source, title_id, checksum) VALUES (?, ?, ?, ?, ?)`,
"foo", "1.0.0", "bar", &titleID, []byte("foo1.0"),
)
require.NoError(t, err)
// insert vulnerable software with the same software title, but still not added to host
result, err = ds.writer(ctx).ExecContext(
ctx,
`INSERT INTO software (name, version, source, title_id, checksum) VALUES (?, ?, ?, ?, ?)`,
"foo", "0.5", "bar", &titleID, []byte("foo0.5"),
)
require.NoError(t, err)
insertedID, err = result.LastInsertId()
require.NoError(t, err)
_, err = ds.InsertSoftwareVulnerability(
ctx,
fleet.SoftwareVulnerability{SoftwareID: uint(insertedID), CVE: "CVE-bar-0001"},
fleet.NVDSource,
)
require.NoError(t, err)
// pending install request for foo1.0 (non-vulnerable version)
_, err = ds.InsertSoftwareInstallRequest(ctx, tmHost.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
// Ensure that software "a" & "b" are returned as they are the only vulnerable apps at this point
sw, _, err = ds.ListHostSoftware(ctx, tmHost, vulnerableOnlyOpts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, software[0].Name, sw[0].Name)
require.Equal(t, software[1].Name, sw[1].Name)
// upcoming_software_uninstall
installerID, titleID, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "foo2",
Source: "bar2",
InstallScript: "cat",
TeamID: &tm.ID,
Filename: "foo2.pkg",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// insert into software without adding to host
_, err = ds.writer(ctx).ExecContext(
ctx,
`INSERT INTO software (name, version, source, title_id, checksum) VALUES (?, ?, ?, ?, ?)`,
"foo2", "1.0.0", "bar2", &titleID, []byte("foo2"),
)
require.NoError(t, err)
// pending install request
err = ds.InsertSoftwareUninstallRequest(ctx, "abc123", tmHost.ID, installerID, true)
require.NoError(t, err)
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
// Ensure that software "a" & "b" are returned as they are the only vulnerable apps at this point
sw, _, err = ds.ListHostSoftware(ctx, tmHost, vulnerableOnlyOpts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, software[0].Name, sw[0].Name)
require.Equal(t, software[1].Name, sw[1].Name)
// upcoming_vpp_install
pendingVPPApp := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.MacOSPlatform}},
Name: "vpp2",
BundleIdentifier: "com.app.vpp2",
}
va2, err := ds.InsertVPPAppWithTeam(ctx, pendingVPPApp, &tm.ID)
require.NoError(t, err)
vpp2 := va2.AdamID
createVPPAppInstallRequest(t, ds, tmHost, vpp2, user)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), tmHost.ID, "")
require.NoError(t, err)
// Insert software entry for vpp app
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
pendingVPPApp.Name, pendingVPPApp.LatestVersion, "apps", pendingVPPApp.BundleIdentifier, pendingVPPApp.TitleID, hex.EncodeToString([]byte("vpp2")),
)
require.NoError(t, err)
time.Sleep(time.Second) // ensure a different created_at timestamp
// Ensure that software "a" & "b" are returned as they are the only vulnerable apps at this point
sw, _, err = ds.ListHostSoftware(ctx, tmHost, vulnerableOnlyOpts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, software[0].Name, sw[0].Name)
require.Equal(t, software[1].Name, sw[1].Name)
// host has vulnerable software installed (not by fleet) that happens to match a vpp app in the fleet catalog
hostInstalledVpps := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.MacOSPlatform}},
Name: "vpp3",
BundleIdentifier: "com.app.vpp3",
}
hvpp, err := ds.InsertVPPAppWithTeam(ctx, hostInstalledVpps, &tm.ID)
require.NoError(t, err)
res, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
hostInstalledVpps.Name, "0.1.0", "apps", hostInstalledVpps.BundleIdentifier, hvpp.TitleID, []byte("vpp3v0.1.0"),
)
require.NoError(t, err)
time.Sleep(time.Second)
vppSoftwareID, err = res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, tmHost.ID, vppSoftwareID)
require.NoError(t, err)
// add vulnerabilities to last_software_install and last_vpp_install and host installed vpp app
vulns = []fleet.SoftwareVulnerability{
{SoftwareID: mutationResults.Inserted[0].ID, CVE: "CVE-file1-0003"},
{SoftwareID: uint(vppSoftwareID), CVE: "CVE-vpp3-0005"},
}
for _, v := range vulns {
_, err = ds.InsertSoftwareVulnerability(ctx, v, fleet.NVDSource)
require.NoError(t, err)
}
// add meta around vulnerabilities
cveMeta = []fleet.CVEMeta{
{
CVE: "CVE-file1-0003",
CVSSScore: ptr.Float64(1.7),
CISAKnownExploit: ptr.Bool(false),
Published: ptr.Time(now.Add(-2 * time.Hour)),
Description: "description for CVE-file1-0003",
},
{
CVE: "CVE-vpp1-0004",
CVSSScore: ptr.Float64(1.8),
CISAKnownExploit: ptr.Bool(false),
Published: ptr.Time(now),
Description: "description for CVE-vpp1-0004",
},
{
CVE: "CVE-vpp3-0005",
CVSSScore: ptr.Float64(1.7),
CISAKnownExploit: ptr.Bool(false),
Published: ptr.Time(now.Add(-2 * time.Hour)),
Description: "description for CVE-vpp3-0005",
},
}
err = ds.InsertCVEMeta(context.Background(), cveMeta)
require.NoError(t, err)
sw, _, err = ds.ListHostSoftware(ctx, tmHost, vulnerableOnlyOpts)
require.NoError(t, err)
require.Len(t, sw, 4)
require.Equal(t, software[0].Name, sw[0].Name) // "a"
require.Equal(t, software[1].Name, sw[1].Name) // "b"
require.Equal(t, software[3].Name, sw[2].Name) // "file1"
// "vpp3" although vpp, it is vulnerable software installed on host
require.Equal(t, hostInstalledVpps.Name, sw[3].Name)
require.Len(t, sw[3].InstalledVersions, 1)
require.Equal(t, "0.1.0", sw[3].InstalledVersions[0].Version)
require.Equal(t, "adam_vpp_3", sw[3].AppStoreApp.AppStoreID)
sw, _, err = ds.ListHostSoftware(ctx, tmHost, knownExploitOpts)
require.NoError(t, err)
require.Len(t, sw, 1)
require.Equal(t, software[0].Name, sw[0].Name) // should only return "a"
sw, _, err = ds.ListHostSoftware(ctx, tmHost, minCVSSScoreOpts)
require.NoError(t, err)
require.Len(t, sw, 1)
require.Equal(t, software[0].Name, sw[0].Name) // should only return "a"
sw, _, err = ds.ListHostSoftware(ctx, tmHost, maxCVSSScoreOpts)
require.NoError(t, err)
require.Len(t, sw, 1)
require.Equal(t, software[1].Name, sw[0].Name) // should only return "b"
sw, _, err = ds.ListHostSoftware(ctx, tmHost, fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
},
VulnerableOnly: true,
MinimumCVSS: 1.6,
IncludeAvailableForInstall: true,
})
require.NoError(t, err)
// should return "a" (2.5), "file1" (1.7), "vpp3" (1.7)
require.Len(t, sw, 3)
require.Equal(t, software[0].Name, sw[0].Name) // should only return "a"
require.Equal(t, software[3].Name, sw[1].Name) // should only return "file1"
require.Equal(t, hostInstalledVpps.Name, sw[2].Name) // should only return "vpp3"
matchingsOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
OrderKey: "name",
},
IncludeAvailableForInstall: true,
VulnerableOnly: true,
MinimumCVSS: 1.5,
MaximumCVSS: 2.0,
}
// should return "file1" & "vpp3" (vpp1 is not compatible with the platform)
sw, _, err = ds.ListHostSoftware(ctx, tmHost, matchingsOpts)
require.NoError(t, err)
require.Len(t, sw, 2)
require.Equal(t, "file1", sw[0].Name)
require.Equal(t, "vpp3", sw[1].Name)
require.Len(t, sw[1].InstalledVersions, 1)
require.Equal(t, "adam_vpp_3", sw[1].AppStoreApp.AppStoreID)
}
func testListHostSoftwareQuerySearching(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a user
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
// create a team
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
nanoEnroll(t, ds, host, false)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &tm.ID
software := []fleet.Software{
{Name: "microsoft office 2025", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.office"},
{Name: "1password", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.1password"},
{Name: "microsoft edge", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.edge"},
{Name: "chrome", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.chrome"},
{Name: "brave", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.example.brave"},
}
byName := map[string]fleet.Software{}
for _, s := range software {
byName[s.Name] = s
}
mutationResults, err := ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
require.Len(t, mutationResults.Inserted, len(software))
err = ds.ReconcileSoftwareTitles(ctx)
require.NoError(t, err)
for _, m := range mutationResults.Inserted {
s := byName[m.Name]
s.ID = m.ID
byName[s.Name] = s
}
vulns := []fleet.SoftwareVulnerability{
{SoftwareID: byName["1password"].ID, CVE: "CVE-2025-0001"},
{SoftwareID: byName["chrome"].ID, CVE: "CVE-2024-0001"},
{SoftwareID: byName["brave"].ID, CVE: "CVE-2024-0002"},
}
for _, v := range vulns {
_, err = ds.InsertSoftwareVulnerability(ctx, v, fleet.NVDSource)
require.NoError(t, err)
}
// no search term, make sure we get all software
sw, meta, err := ds.ListHostSoftware(
ctx,
host,
fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
},
},
)
require.NoError(t, err)
require.Len(t, sw, 5)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 5}, meta)
// search for microsoft
sw, meta, err = ds.ListHostSoftware(
ctx,
host,
fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
MatchQuery: "microsoft",
},
},
)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 2}, meta)
require.Equal(t, software[2].Name, sw[0].Name)
require.Equal(t, software[0].Name, sw[1].Name)
// search for 2025
sw, meta, err = ds.ListHostSoftware(
ctx,
host,
fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
MatchQuery: "2025",
},
},
)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 2}, meta)
require.Equal(t, software[1].Name, sw[0].Name)
require.Equal(t, software[0].Name, sw[1].Name)
// search for cve-2024
sw, meta, err = ds.ListHostSoftware(
ctx,
host,
fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
MatchQuery: "2024",
},
},
)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 2}, meta)
require.Equal(t, software[4].Name, sw[0].Name)
require.Equal(t, software[3].Name, sw[1].Name)
// search for brave and `VulnerableOnly` filter
sw, meta, err = ds.ListHostSoftware(
ctx,
host,
fleet.HostSoftwareTitleListOptions{
VulnerableOnly: true,
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
MatchQuery: "brave",
},
},
)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 1}, meta)
require.Equal(t, software[4].Name, sw[0].Name)
// set up vpp
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
time.Sleep(time.Second) // ensure the labels_updated_at timestamp is before labels creation
// last_vpp_install
vPPApp := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}},
Name: "microsoft teams",
BundleIdentifier: "com.app.teams",
}
va1, err := ds.InsertVPPAppWithTeam(ctx, vPPApp, &tm.ID)
require.NoError(t, err)
vpp1 := va1.AdamID
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host, vpp1, user)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host.ID, "")
require.NoError(t, err)
createVPPAppInstallResult(t, ds, host, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// Insert software entry for vpp app
res, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp.Name, vPPApp.LatestVersion, "apps", vPPApp.BundleIdentifier, vPPApp.TitleID, hex.EncodeToString([]byte("vpp1")),
)
require.NoError(t, err)
time.Sleep(time.Second) // ensure a different created_at timestamp
vppSoftwareID, err := res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, host.ID, vppSoftwareID)
require.NoError(t, err)
// search for microsoft
sw, meta, err = ds.ListHostSoftware(
ctx,
host,
fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
MatchQuery: "microsoft",
},
},
)
require.NoError(t, err)
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 3}, meta)
require.Equal(t, software[2].Name, sw[0].Name)
require.Equal(t, software[0].Name, sw[1].Name)
require.Equal(t, vPPApp.Name, sw[2].Name)
// search with self-service
vPPAppSlack := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{SelfService: true, VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.MacOSPlatform}},
Name: "slack",
BundleIdentifier: "com.app.slack",
}
_, err = ds.InsertVPPAppWithTeam(ctx, vPPAppSlack, &tm.ID)
require.NoError(t, err)
vPPApp1Password := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{SelfService: true, VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.MacOSPlatform}},
Name: "1password",
BundleIdentifier: "com.app.1password",
}
_, err = ds.InsertVPPAppWithTeam(ctx, vPPApp1Password, &tm.ID)
require.NoError(t, err)
opts := fleet.HostSoftwareTitleListOptions{
SelfServiceOnly: true,
IsMDMEnrolled: true,
IncludeAvailableForInstall: true,
ListOptions: fleet.ListOptions{
PerPage: 10,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
},
}
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, 2)
require.Equal(t, vPPApp1Password.Name, sw[0].Name)
require.Equal(t, vPPAppSlack.Name, sw[1].Name)
opts.ListOptions.MatchQuery = "1password"
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, 1)
require.Equal(t, vPPApp1Password.Name, sw[0].Name)
}
func testListHostSoftwareWithLabelScopingVPP(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a host
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
time.Sleep(time.Second) // ensure the labels_updated_at timestamp is before labels creation
vppApp := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{SelfService: true, VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b1"}
vppApp, err = ds.InsertVPPAppWithTeam(ctx, vppApp, nil)
require.NoError(t, err)
vppAppTeamID := vppApp.VPPAppTeam.AppTeamID
// create a software installer
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installer1 := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
PreInstallQuery: "SELECT 1",
PostInstallScript: "world",
UninstallScript: "goodbye",
InstallerFile: tfr1,
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
UserID: user1.ID,
BundleIdentifier: "bi1",
Platform: "darwin",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, installer1)
require.NoError(t, err)
// we should see installer1, since it has no label associated yet
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{
PerPage: 11,
IncludeMetadata: true,
OrderKey: "name",
TestSecondaryOrderKey: "source",
},
IncludeAvailableForInstall: true,
IsMDMEnrolled: true,
}
expectedInstallers := map[string]*fleet.SoftwarePackageOrApp{
installer1.Filename: {
Name: installer1.Filename,
Version: installer1.Version,
SelfService: ptr.Bool(false),
Platform: "darwin",
},
vppApp.Name: {
AppStoreID: vppApp.AdamID,
SelfService: ptr.Bool(true),
Platform: "darwin",
},
}
checkSoftware := func(swList []*fleet.HostSoftwareWithInstaller, excludeNames ...string) {
expectedLen := len(expectedInstallers) - len(excludeNames)
require.Equal(t, len(swList), expectedLen)
for _, got := range swList {
if got.IsPackage() {
want, ok := expectedInstallers[got.SoftwarePackage.Name]
if slices.Contains(excludeNames, got.SoftwarePackage.Name) {
require.False(t, ok)
continue
}
require.True(t, ok)
require.Equal(t, want, got.SoftwarePackage)
}
if got.IsAppStoreApp() {
want, ok := expectedInstallers[got.Name]
if slices.Contains(excludeNames, got.AppStoreApp.AppStoreID) {
require.False(t, ok)
continue
}
require.True(t, ok)
require.Equal(t, want, got.AppStoreApp)
}
}
}
software, _, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software)
// installer1 should be in scope since it has no labels
scoped, err := ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// vppApp should be in scope since it has no labels
scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// Create a couple of labels
label1, err := ds.NewLabel(ctx, &fleet.Label{Name: "label1" + t.Name()})
require.NoError(t, err)
label2, err := ds.NewLabel(ctx, &fleet.Label{Name: "label2" + t.Name()})
require.NoError(t, err)
// assign the label to the host
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label1.ID}))
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
time.Sleep(time.Second)
// assign the label to the software installer
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID1, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
// should contain only the VPP app as the installer label is "exclude any"
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software, installer1.Filename)
hostsNotInScope, err := ds.GetExcludedHostIDMapForVPPApp(ctx, vppAppTeamID)
require.NoError(t, err)
require.Empty(t, hostsNotInScope)
hostsInScope, err := ds.GetIncludedHostIDMapForVPPApp(ctx, vppAppTeamID)
require.NoError(t, err)
require.Equal(t, map[uint]struct{}{host.ID: {}}, hostsInScope)
anotherHost := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, anotherHost, false)
// intall vpp app
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, anotherHost, vppApp.AdamID, user1)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), anotherHost.ID, "")
require.NoError(t, err)
createVPPAppInstallResult(t, ds, anotherHost, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// Insert software entry for vpp app
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vppApp.Name, vppApp.LatestVersion, "apps", vppApp.BundleIdentifier, vppApp.TitleID, hex.EncodeToString([]byte("vpp1")),
)
require.NoError(t, err)
time.Sleep(time.Second)
// update host label timestamp
require.NoError(t, ds.AddLabelsToHost(ctx, anotherHost.ID, []uint{label1.ID}))
anotherHost.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, anotherHost)
require.NoError(t, err)
time.Sleep(time.Second)
thirdHost := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, thirdHost, false)
// have a pre-installed vpp app
res, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vppApp.Name, "0.1.10", "apps", vppApp.BundleIdentifier, vppApp.TitleID, []byte("vpp1v0.1.10"),
)
require.NoError(t, err)
time.Sleep(time.Second)
softwareID, err := res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`, thirdHost.ID, softwareID)
require.NoError(t, err)
// update host label timestamp
require.NoError(t, ds.AddLabelsToHost(ctx, thirdHost.ID, []uint{label1.ID}))
thirdHost.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, thirdHost)
require.NoError(t, err)
time.Sleep(time.Second)
fourthHost := test.NewHost(t, ds, "host4", "", "host4key", "host4uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, fourthHost, false)
require.NoError(t, ds.AddLabelsToHost(ctx, fourthHost.ID, []uint{label1.ID}))
fourthHost.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, fourthHost)
require.NoError(t, err)
time.Sleep(time.Second)
scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, fourthHost.ID)
require.NoError(t, err)
require.True(t, scoped)
// Assign the label to the VPP app. Now we should have an empty list
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), vppAppTeamID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeVPP)
require.NoError(t, err)
// intall vpp app on fourth host
fourthHostVpp1CmdUUID := createVPPAppInstallRequest(t, ds, fourthHost, vppApp.AdamID, user1)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), fourthHost.ID, "")
require.NoError(t, err)
createVPPAppInstallResult(t, ds, fourthHost, fourthHostVpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// but inventory has not been updated yet so no software/host software records exist
software, _, err = ds.ListHostSoftware(
ctx,
fourthHost,
fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10},
IncludeAvailableForInstall: true,
SelfServiceOnly: true,
IsMDMEnrolled: true,
},
)
require.NoError(t, err)
// installer one is not self service, vpp app is but out of scope due to label
checkSoftware(software, installer1.Filename, vppApp.Name)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software, installer1.Filename, vppApp.Name)
hostsNotInScope, err = ds.GetExcludedHostIDMapForVPPApp(ctx, vppAppTeamID)
require.NoError(t, err)
require.Equal(t, map[uint]struct{}{host.ID: {}, anotherHost.ID: {}, thirdHost.ID: {}, fourthHost.ID: {}}, hostsNotInScope)
hostsInScope, err = ds.GetIncludedHostIDMapForVPPApp(ctx, vppAppTeamID)
require.NoError(t, err)
require.Empty(t, hostsInScope)
// vpp app is installed but not verified, so should still be pending
software, _, err = ds.ListHostSoftware(ctx, anotherHost, opts)
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, software[0].Name, vppApp.Name)
expectedStatus := fleet.SoftwareInstallPending
require.Equal(t, &expectedStatus, software[0].Status)
// but should not be available for install because of the ExcludeAny label
require.Nil(t, software[0].AppStoreApp)
// verify the install
require.NoError(t, ds.SetVPPInstallAsVerified(ctx, anotherHost.ID, vpp1CmdUUID, uuid.NewString()))
// Now the app should come back as installed
software, _, err = ds.ListHostSoftware(ctx, anotherHost, opts)
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, software[0].Name, vppApp.Name)
expectedStatus = fleet.SoftwareInstalled
require.Equal(t, &expectedStatus, software[0].Status)
// but should not be available for install because of the ExcludeAny label
require.Nil(t, software[0].AppStoreApp)
// vpp app is installed, so should come back in list of software
software, _, err = ds.ListHostSoftware(ctx, thirdHost, opts)
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, software[0].Name, vppApp.Name)
// nil because it was pre-installed on host
require.Nil(t, software[0].Status)
// but should not be available for install because of the ExcludeAny label
require.Nil(t, software[0].AppStoreApp)
// filtering by only available for install should exclude the vpp app
opts.OnlyAvailableForInstall = true
software, _, err = ds.ListHostSoftware(ctx, anotherHost, opts)
require.NoError(t, err)
checkSoftware(software, installer1.Filename, vppApp.Name)
software, _, err = ds.ListHostSoftware(ctx, thirdHost, opts)
require.NoError(t, err)
checkSoftware(software, installer1.Filename, vppApp.Name)
opts.OnlyAvailableForInstall = false
// Make the label include any. We should have both of them back.
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID1, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), vppAppTeamID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeVPP)
require.NoError(t, err)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software)
// Give the VPP app a different label. Only the installer should show up now, since the host
// only has label1.
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), vppAppTeamID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label2.Name: {LabelName: label2.Name, LabelID: label2.ID}},
}, softwareTypeVPP)
require.NoError(t, err)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software, vppApp.Name)
scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID)
require.NoError(t, err)
require.False(t, scoped)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), vppAppTeamID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label2.Name: {LabelName: label2.Name, LabelID: label2.ID}, label1.Name: {LabelName: label1.Name, LabelID: label1.ID}},
}, softwareTypeVPP)
require.NoError(t, err)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software)
scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID)
require.NoError(t, err)
require.True(t, scoped)
// Create another label.
time.Sleep(time.Second)
label3, err := ds.NewLabel(ctx, &fleet.Label{Name: "label3" + t.Name()})
require.NoError(t, err)
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), vppAppTeamID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{label3.Name: {LabelName: label3.Name, LabelID: label3.ID}},
}, softwareTypeVPP)
require.NoError(t, err)
// the VPP app is still out of scope, because label3 was added as exclude any and the host's
// LabelUpdatedAt isn't fresh enough.
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software, vppApp.Name)
scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID)
require.NoError(t, err)
require.False(t, scoped)
// mark as if label had been reported (but host is still not a member). This should bring the
// VPP app back in scope, since it's exclude any and the host doesn't have label 3.
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
time.Sleep(time.Second)
software, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
checkSoftware(software)
scoped, err = ds.IsVPPAppLabelScoped(ctx, vppApp.VPPAppTeam.AppTeamID, host.ID)
require.NoError(t, err)
require.True(t, scoped)
}
func testListHostSoftwareLastOpenedAt(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a host
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
hostInventoryOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 0, PerPage: 20},
SelfServiceOnly: false,
IncludeAvailableForInstall: false,
IncludeAvailableForInstallExplicitlySet: true,
OnlyAvailableForInstall: false,
IsMDMEnrolled: true,
}
var err error
var parsedTimeOne time.Time
var parsedTimeTwo time.Time
updateSoftwareForHost := func(host *fleet.Host, software []fleet.Software) {
mutationResults, err := ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
require.Len(t, mutationResults.Inserted, len(software))
err = ds.ReconcileSoftwareTitles(ctx)
require.NoError(t, err)
require.NoError(t, ds.LoadHostSoftware(ctx, host, false))
query := `INSERT INTO host_software_installed_paths (host_id, software_id, installed_path) VALUES (?, ?, ?)`
for i := range host.Software {
args := []interface{}{host.ID, host.Software[i].ID, fmt.Sprintf("/Applications/%s-%s.app", host.Software[i].Name, host.Software[i].Version)}
_, err = ds.writer(ctx).ExecContext(ctx, query, args...)
require.NoError(t, err)
}
require.NoError(t, err)
}
testCases := []struct {
name string
before func(ds *Datastore)
testFunc func(*testing.T, *Datastore)
}{
{
name: "older version has been opened, newer version not opened",
before: func(ds *Datastore) {
parsedTimeOne, err = time.Parse("2006-01-02 15:04", "2025-01-01 12:00")
require.NoError(t, err)
software := []fleet.Software{
{ID: 1, Name: "Spotify", Version: "1.2.69.449", LastOpenedAt: &parsedTimeOne, TitleID: ptr.Uint(1), Source: "apps", BundleIdentifier: "com.spotify"},
{ID: 2, Name: "Spotify", Version: "1.2.70.409", LastOpenedAt: nil, TitleID: ptr.Uint(1), Source: "apps", BundleIdentifier: "com.spotify"},
}
updateSoftwareForHost(host, software)
},
testFunc: func(t *testing.T, ds *Datastore) {
expectedTimeStamps := map[string]*time.Time{
"1.2.69.449": &parsedTimeOne,
"1.2.70.409": nil,
}
sw, _, err := ds.ListHostSoftware(ctx, host, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, sw, 1)
require.Len(t, sw[0].InstalledVersions, 2)
for _, installedVersion := range sw[0].InstalledVersions {
version := installedVersion.Version
if expectedTime, ok := expectedTimeStamps[version]; ok {
require.Equal(t, expectedTime, installedVersion.LastOpenedAt, "LastOpenedAt for version %s does not match expected value", version)
} else {
require.Nil(t, installedVersion.LastOpenedAt, "LastOpenedAt for version %s is not nil", version)
}
}
},
},
{
name: "Both versions have been opened",
before: func(ds *Datastore) {
parsedTimeOne, err = time.Parse("2006-01-02 15:04", "2025-01-01 12:00")
require.NoError(t, err)
parsedTimeTwo, err = time.Parse("2006-01-02 15:04", "2025-02-01 12:00")
require.NoError(t, err)
software := []fleet.Software{
{ID: 1, Name: "Spotify", Version: "1.2.69.449", LastOpenedAt: &parsedTimeOne, TitleID: ptr.Uint(1), Source: "apps", BundleIdentifier: "com.spotify"},
{ID: 2, Name: "Spotify", Version: "1.2.70.409", LastOpenedAt: &parsedTimeTwo, TitleID: ptr.Uint(1), Source: "apps", BundleIdentifier: "com.spotify"},
}
updateSoftwareForHost(host, software)
},
testFunc: func(t *testing.T, ds *Datastore) {
sw, _, err := ds.ListHostSoftware(ctx, host, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, sw, 1)
require.Len(t, sw[0].InstalledVersions, 2)
expectedTimeStamps := map[string]*time.Time{
"1.2.69.449": &parsedTimeOne,
"1.2.70.409": &parsedTimeTwo,
}
for _, installedVersion := range sw[0].InstalledVersions {
version := installedVersion.Version
if expectedTime, ok := expectedTimeStamps[version]; ok {
require.Equal(t, expectedTime, installedVersion.LastOpenedAt, "LastOpenedAt for version %s does not match expected value", version)
} else {
require.Nil(t, installedVersion.LastOpenedAt, "LastOpenedAt for version %s is not nil", version)
}
}
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer TruncateTables(t, ds)
tc.before(ds)
tc.testFunc(t, ds)
})
}
}
func testListHostSoftwareSelfServiceWithLabelScopingHostInstalled(t *testing.T, ds *Datastore) {
ctx := context.Background()
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &tm.ID
opts := fleet.HostSoftwareTitleListOptions{
SelfServiceOnly: true,
IsMDMEnrolled: true,
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: false,
VulnerableOnly: false,
KnownExploit: false,
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
}
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
time.Sleep(time.Second)
// self-service software installer
tfr2, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
selfServiceinstaller := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello 2",
PreInstallQuery: "SELECT 2",
PostInstallScript: "world 2",
UninstallScript: "goodbye 2",
InstallerFile: tfr2,
StorageID: "storage 2",
Filename: "file2",
Title: "file2",
Version: "1.0",
Source: "apps",
UserID: user1.ID,
BundleIdentifier: "bi2",
Platform: "darwin",
SelfService: true,
TeamID: &tm.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
selfServiceInstallerID, selfServiceTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, selfServiceinstaller)
require.NoError(t, err)
vPPApp := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{
SelfService: true,
VPPAppID: fleet.VPPAppID{
AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform,
},
},
Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
LatestVersion: "1.0.0",
}
_, err = ds.InsertVPPAppWithTeam(ctx, vPPApp, &tm.ID)
require.NoError(t, err)
// Install software on host
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
res, err := q.ExecContext(ctx, `INSERT INTO software (name, source, bundle_identifier, version, title_id, checksum) VALUES (?, ?, ?, ?, ?, ?)`,
selfServiceinstaller.Title, selfServiceinstaller.Source, selfServiceinstaller.BundleIdentifier, selfServiceinstaller.Version, selfServiceTitleID, []byte("checksum"))
if err != nil {
return err
}
softwareID, err := res.LastInsertId()
if err != nil {
return err
}
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`,
host.ID, softwareID)
if err != nil {
return err
}
res, err = q.ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp.Name, "1.2.3", "apps", vPPApp.BundleIdentifier, vPPApp.TitleID, hex.EncodeToString([]byte("vpp1")),
)
require.NoError(t, err)
time.Sleep(time.Second)
softwareID, err = res.LastInsertId()
require.NoError(t, err)
_, err = q.ExecContext(ctx, `
INSERT INTO host_software (host_id, software_id)
VALUES (?, ?)
`,
host.ID, softwareID)
require.NoError(t, err)
return nil
})
sw, _, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, 2)
// selfServiceIstaller should be in scope since it has no labels
scoped, err := ds.IsSoftwareInstallerLabelScoped(ctx, selfServiceInstallerID, host.ID)
require.NoError(t, err)
require.True(t, scoped)
hostsInScope, err := ds.GetIncludedHostIDMapForSoftwareInstaller(ctx, selfServiceInstallerID)
require.NoError(t, err)
require.Contains(t, hostsInScope, host.ID)
// vppApp should be in scope since it has no labels
scoped, err = ds.IsVPPAppLabelScoped(ctx, vPPApp.VPPAppTeam.AppTeamID, host.ID)
require.NoError(t, err)
require.True(t, scoped)
hostsInScope, err = ds.GetIncludedHostIDMapForVPPApp(ctx, vPPApp.VPPAppTeam.AppTeamID)
require.NoError(t, err)
require.Contains(t, hostsInScope, host.ID)
// exclude label
excludeLabel, err := ds.NewLabel(ctx, &fleet.Label{Name: "Exclude Label" + t.Name()})
require.NoError(t, err)
// label host
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{excludeLabel.ID}))
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
// label software
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), selfServiceInstallerID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{excludeLabel.Name: {LabelName: excludeLabel.Name, LabelID: excludeLabel.ID}},
}, softwareTypeInstaller)
require.NoError(t, err)
// label vpp app
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), vPPApp.VPPAppTeam.AppTeamID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{excludeLabel.Name: {LabelName: excludeLabel.Name, LabelID: excludeLabel.ID}},
}, softwareTypeVPP)
require.NoError(t, err)
// selfServiceIstaller should not be in scope since it has exclude any label
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, selfServiceInstallerID, host.ID)
require.NoError(t, err)
require.False(t, scoped)
hostsInScope, err = ds.GetIncludedHostIDMapForSoftwareInstaller(ctx, selfServiceInstallerID)
require.NoError(t, err)
require.Empty(t, hostsInScope)
// vppApp should not be in scope since it has exclude any label
scoped, err = ds.IsVPPAppLabelScoped(ctx, vPPApp.VPPAppTeam.AppTeamID, host.ID)
require.NoError(t, err)
require.False(t, scoped)
hostsInScope, err = ds.GetIncludedHostIDMapForVPPApp(ctx, vPPApp.VPPAppTeam.AppTeamID)
require.NoError(t, err)
require.Empty(t, hostsInScope)
// both apps are out of scope so we should get an empty list
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
assert.Len(t, sw, 0)
}
func testDeletedInstalledSoftware(t *testing.T, ds *Datastore) {
ctx := context.Background()
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "GoLand",
Source: "app",
InstallScript: "echo",
TeamID: &team.ID,
Filename: "foo.pkg",
UserID: user1.ID,
BundleIdentifier: "com.jetbrains.goland",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
_, err = ds.InsertSoftwareInstallRequest(ctx, host1.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(ctx, `UPDATE host_software_installs SET post_install_script_exit_code = 0`)
require.NoError(t, err)
return nil
})
software1 := []fleet.Software{
{Name: "GoLand", Version: "1.0.2", Source: "app", BundleIdentifier: "com.jetbrains.goland"},
{Name: "GoLand", Version: "1.0.2", Source: "app", BundleIdentifier: "com.jetbrains.goland2"},
}
_, err = ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
require.NoError(t, err)
// remove software with different bundle id same name as installed software
software1 = []fleet.Software{
{Name: "GoLand", Version: "1.0.2", Source: "app", BundleIdentifier: "com.jetbrains.goland"},
}
_, err = ds.UpdateHostSoftware(context.Background(), host1.ID, software1)
require.NoError(t, err)
var hostSoftwareInstalls []struct {
HostID uint `db:"host_id"`
SoftwareInstallerID uint `db:"software_installer_id"`
Removed bool `db:"removed"`
Status string `db:"status"`
}
err = sqlx.SelectContext(
ctx,
ds.writer(ctx),
&hostSoftwareInstalls,
`select host_id, software_installer_id, removed, status from host_software_installs where host_id = ?`,
host1.ID,
)
if err != nil {
fmt.Printf("error getting software titles: %v\n", err)
}
// Ensure installed software is not marked as removed
for _, value := range hostSoftwareInstalls {
assert.False(t, value.Removed)
}
}
func testSoftwareCategories(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1"+t.Name(), fmt.Sprintf("user1%s@example.com", t.Name()), false)
// create some categories
cat1, err := ds.NewSoftwareCategory(ctx, "category1")
require.NoError(t, err)
require.Equal(t, "category1", cat1.Name)
cat2, err := ds.NewSoftwareCategory(ctx, "category2")
require.NoError(t, err)
require.Equal(t, "category2", cat2.Name)
// get the IDs
ids, err := ds.GetSoftwareCategoryIDs(ctx, []string{cat1.Name, cat2.Name})
require.NoError(t, err)
require.Len(t, ids, 2)
require.Contains(t, ids, cat1.ID)
require.Contains(t, ids, cat2.ID)
// create a software installer for no team
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installerNoTeam, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
InstallerFile: tfr1,
StorageID: "storage1",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// Add the categories to the installer
err = ds.SaveInstallerUpdates(ctx, &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
InstallerID: installerNoTeam,
CategoryIDs: ids,
InstallScript: ptr.String("hello"),
SelfService: ptr.Bool(false),
UninstallScript: ptr.String(""),
PreInstallQuery: ptr.String(""),
UserID: user.ID,
})
require.NoError(t, err)
categories, err := ds.GetCategoriesForSoftwareTitles(ctx, []uint{titleID}, nil)
require.NoError(t, err)
require.Len(t, categories, 1)
require.Contains(t, categories[installerNoTeam], cat1.Name)
require.Contains(t, categories[installerNoTeam], cat2.Name)
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
vppApp := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}, CategoryIDs: ids}, BundleIdentifier: "b1"}
vppApp, err = ds.InsertVPPAppWithTeam(ctx, vppApp, nil)
require.NoError(t, err)
vppAppTitleID := vppApp.TitleID
categories, err = ds.GetCategoriesForSoftwareTitles(ctx, []uint{titleID, vppAppTitleID}, nil)
require.NoError(t, err)
require.Len(t, categories, 2)
require.Contains(t, categories[installerNoTeam], cat1.Name)
require.Contains(t, categories[installerNoTeam], cat2.Name)
require.Contains(t, categories[installerNoTeam], cat1.Name)
require.Contains(t, categories[installerNoTeam], cat2.Name)
}
func testLabelScopingTimestampLogic(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a host
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
tfr2, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
selfServiceinstaller := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello 2",
PreInstallQuery: "SELECT 2",
PostInstallScript: "world 2",
UninstallScript: "goodbye 2",
InstallerFile: tfr2,
StorageID: "storage 2",
Filename: "file2",
Title: "file2",
Version: "1.0",
Source: "apps",
UserID: user1.ID,
BundleIdentifier: "bi2",
Platform: "darwin",
SelfService: true,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
selfServiceInstallerID, selfServiceTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, selfServiceinstaller)
require.NoError(t, err)
softwareAlreadyInstalled := fleet.Software{Name: "file1", Version: "1.0.1", Source: "apps", BundleIdentifier: "bi1"}
// Host has software installed, but not by Fleet, that matches the software installer available
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
titleIDUint := selfServiceTitleID
softwareAlreadyInstalled.TitleID = &titleIDUint
res, err := q.ExecContext(ctx, `INSERT INTO software (name, source, bundle_identifier, version, title_id, checksum) VALUES (?, ?, ?, ?, ?, ?)`,
softwareAlreadyInstalled.Name, softwareAlreadyInstalled.Source, softwareAlreadyInstalled.BundleIdentifier, softwareAlreadyInstalled.Version, selfServiceTitleID, []byte("checksum"))
if err != nil {
return err
}
softwareID, err := res.LastInsertId()
if err != nil {
return err
}
softwareAlreadyInstalled.ID = uint(softwareID)
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`,
host.ID, softwareID)
if err != nil {
return err
}
return nil
})
// Dynamic label
label1, err := ds.NewLabel(ctx, &fleet.Label{Name: "label1" + t.Name(), LabelMembershipType: fleet.LabelMembershipTypeDynamic})
require.NoError(t, err)
// Manual label
label2, err := ds.NewLabel(ctx, &fleet.Label{Name: "label2" + t.Name(), LabelMembershipType: fleet.LabelMembershipTypeManual})
require.NoError(t, err)
// make sure the label is created after the host's labels_updated_at timestamp
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(ctx, `UPDATE labels SET created_at = ? WHERE id in (?, ?)`, host.LabelUpdatedAt.Add(time.Hour), label1.ID, label2.ID)
if err != nil {
return err
}
return nil
})
// refetch labels to ensure their state is correct
label1, _, err = ds.Label(ctx, label1.ID, fleet.TeamFilter{})
require.NoError(t, err)
label2, _, err = ds.Label(ctx, label2.ID, fleet.TeamFilter{})
require.NoError(t, err)
require.Greater(t, label1.CreatedAt, host.LabelUpdatedAt)
require.Greater(t, label2.CreatedAt, host.LabelUpdatedAt)
selfServiceOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10},
IncludeAvailableForInstall: true,
SelfServiceOnly: true,
IsMDMEnrolled: true,
}
hostLibraryOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10},
SelfServiceOnly: false,
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: true,
IsMDMEnrolled: true,
}
// Dynamic label exclude any
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), selfServiceInstallerID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{
label1.Name: {LabelName: label1.Name, LabelID: label1.ID},
},
}, softwareTypeInstaller)
require.NoError(t, err)
// self service
software, _, err := ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// host library
software, _, err = ds.ListHostSoftware(ctx, host, hostLibraryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// manual label exclude any
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), selfServiceInstallerID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeExcludeAny,
ByName: map[string]fleet.LabelIdent{
label2.Name: {LabelName: label2.Name, LabelID: label2.ID},
},
}, softwareTypeInstaller)
require.NoError(t, err)
// self service
software, _, err = ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// host library
software, _, err = ds.ListHostSoftware(ctx, host, hostLibraryOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// Add manual label to host
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label2.ID}))
host, err = ds.Host(ctx, host.ID)
require.NoError(t, err)
label1, _, err = ds.Label(ctx, label1.ID, fleet.TeamFilter{})
require.NoError(t, err)
label2, _, err = ds.Label(ctx, label2.ID, fleet.TeamFilter{})
require.NoError(t, err)
// ensure our timestamps are still correct
require.Greater(t, label1.CreatedAt, host.LabelUpdatedAt)
require.Greater(t, label2.CreatedAt, host.LabelUpdatedAt)
// Manual label added to host, so we should not see the software instantly
// self service
software, _, err = ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// host library
software, _, err = ds.ListHostSoftware(ctx, host, hostLibraryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// manual label include any
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), selfServiceInstallerID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{
label2.Name: {LabelName: label2.Name, LabelID: label2.ID},
},
}, softwareTypeInstaller)
require.NoError(t, err)
// self service
software, _, err = ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// host library
software, _, err = ds.ListHostSoftware(ctx, host, hostLibraryOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// add dynamic label to host
require.NoError(t, ds.RemoveLabelsFromHost(ctx, host.ID, []uint{label2.ID}))
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{label1.ID}))
host, err = ds.Host(ctx, host.ID)
require.NoError(t, err)
label1, _, err = ds.Label(ctx, label1.ID, fleet.TeamFilter{})
require.NoError(t, err)
label2, _, err = ds.Label(ctx, label2.ID, fleet.TeamFilter{})
require.NoError(t, err)
// ensure our timestamps are still correct
require.Greater(t, label1.CreatedAt, host.LabelUpdatedAt)
require.Greater(t, label2.CreatedAt, host.LabelUpdatedAt)
// Dynamic label include any
err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), selfServiceInstallerID, fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{
label1.Name: {LabelName: label1.Name, LabelID: label1.ID},
},
}, softwareTypeInstaller)
require.NoError(t, err)
// self service
software, _, err = ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// host library
software, _, err = ds.ListHostSoftware(ctx, host, hostLibraryOpts)
require.NoError(t, err)
require.Len(t, software, 1)
}
func testInventoryPendingSoftware(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)
user := test.NewUser(t, ds, "user"+t.Name(), fmt.Sprintf("user%s@example.com", t.Name()), false)
// set up vpp
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
time.Sleep(time.Second) // ensure the labels_updated_at timestamp is before labels creation
selfServiceOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 0, PerPage: 20},
SelfServiceOnly: true,
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: false,
IsMDMEnrolled: true,
}
hostInventoryOpts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{Page: 0, PerPage: 20},
SelfServiceOnly: false,
IncludeAvailableForInstall: false,
OnlyAvailableForInstall: false,
IsMDMEnrolled: true,
}
tfr, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
softwareInstaller := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
PreInstallQuery: "SELECT",
PostInstallScript: "world",
UninstallScript: "goodbye",
InstallerFile: tfr,
StorageID: "storage",
Filename: "file1",
Title: "file1",
Version: "1.0",
Source: "apps",
UserID: user.ID,
BundleIdentifier: "bi1",
Platform: "darwin",
SelfService: true,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, softwareInstaller)
require.NoError(t, err)
// software installer is pending (all results are NULL)
hostSoftwareInstallUUID, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
// Pending installer with no results
software, _, err := ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// Set the result as installed
ds.testActivateSpecificNextActivities = []string{"-"}
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostSoftwareInstallUUID,
PreInstallConditionOutput: ptr.String("ok"),
InstallScriptExitCode: ptr.Int(0),
PostInstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)
// Successfully installed, however not inventoried by osquery
software, _, err = ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// Now we inventory the software, osquery returns it
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var title struct {
ID uint `db:"id"`
Name string `db:"name"`
Source string `db:"source"`
Version string `db:"version"`
}
err := sqlx.GetContext(ctx, q, &title, `
SELECT software_titles.id, software_titles.name, software_titles.source, software_installers.version
FROM software_installers
JOIN software_titles ON software_installers.title_id = software_titles.id
WHERE software_installers.id = ?`, installerID)
require.NoError(t, err)
res, err := q.ExecContext(ctx,
`INSERT INTO software (name, source, bundle_identifier, version, title_id, checksum) VALUES (?, ?, ?, ?, ?, ?)`,
title.Name,
title.Source,
"title.com.example",
title.Version,
title.ID,
[]byte("checksum"),
)
require.NoError(t, err)
softwareID, err := res.LastInsertId()
require.NoError(t, err)
_, err = q.ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`,
host.ID, softwareID)
require.NoError(t, err)
return nil
})
// Successfully installed and inventoried, so we should see it now
software, _, err = ds.ListHostSoftware(ctx, host, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// setup host 2
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host2, false)
host2SoftwareInstallUUID, err := ds.InsertSoftwareInstallRequest(ctx, host2.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
// Pending installer with no results
software, _, err = ds.ListHostSoftware(ctx, host2, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host2, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// fail the install
ds.testActivateSpecificNextActivities = []string{host2SoftwareInstallUUID}
activated, err := ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host2.ID, "")
require.NoError(t, err)
require.Equal(t, ds.testActivateSpecificNextActivities, activated)
ds.testActivateSpecificNextActivities = []string{"-"}
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host2.ID,
InstallUUID: host2SoftwareInstallUUID,
PreInstallConditionOutput: ptr.String("ok"),
InstallScriptExitCode: ptr.Int(1),
})
require.NoError(t, err)
// Failed install
software, _, err = ds.ListHostSoftware(ctx, host2, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host2, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// setup host 3
host3 := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host3, false)
// has software installed, but not by Fleet, that matches the software installer available
res, err := ds.writer(ctx).ExecContext(
ctx,
`INSERT INTO software (name, version, source, title_id, checksum) VALUES (?, ?, ?, ?, ?)`,
"foo2", "0.5", "bar2", &titleID, []byte("foo2"),
)
require.NoError(t, err)
installedSoftwareID, err := res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`, host3.ID, installedSoftwareID)
require.NoError(t, err)
// Installed software should show up
software, _, err = ds.ListHostSoftware(ctx, host3, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host3, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// Create a software install request
_, err = ds.InsertSoftwareInstallRequest(ctx, host3.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
// Pending request, but software is installed and inventoried by osquery, so we should see it
software, _, err = ds.ListHostSoftware(ctx, host3, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host3, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 1)
// setup host 4
host4 := test.NewHost(t, ds, "host4", "", "host4key", "host4uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host4, false)
// create a team
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
// Add host to team
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
require.NoError(t, err)
host4.TeamID = &tm.ID
vPPApp := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{
SelfService: true,
VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform},
},
Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}
va1, err := ds.InsertVPPAppWithTeam(ctx, vPPApp, &tm.ID)
require.NoError(t, err)
vpp1 := va1.AdamID
vpp1CmdUUID := createVPPAppInstallRequest(t, ds, host4, vpp1, user)
// vpp app is pending, no results
software, _, err = ds.ListHostSoftware(ctx, host4, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host4, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// vpp app comes back as installed
ds.testActivateSpecificNextActivities = []string{vpp1CmdUUID}
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host4.ID, "")
require.NoError(t, err)
createVPPAppInstallResult(t, ds, host4, vpp1CmdUUID, fleet.MDMAppleStatusAcknowledged)
// vpp app is installed, but not inventoried by osquery
software, _, err = ds.ListHostSoftware(ctx, host4, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host4, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 0)
// inventory by osquery
res, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO software (name, version, source, bundle_identifier, title_id, checksum)
VALUES (?, ?, ?, ?, ?, ?)
`,
vPPApp.Name, "0.1.1", "apps", vPPApp.BundleIdentifier, vPPApp.TitleID, hex.EncodeToString([]byte("vpp1")),
)
require.NoError(t, err)
vppSoftwareID, err := res.LastInsertId()
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, `INSERT INTO host_software (host_id, software_id) VALUES (?, ?)`, host4.ID, vppSoftwareID)
require.NoError(t, err)
// should see it now because it is inventoried by osquery
software, _, err = ds.ListHostSoftware(ctx, host4, selfServiceOpts)
require.NoError(t, err)
require.Len(t, software, 1)
software, _, err = ds.ListHostSoftware(ctx, host4, hostInventoryOpts)
require.NoError(t, err)
require.Len(t, software, 1)
}
func testCheckForDeletedInstalledSoftware(t *testing.T, ds *Datastore) {
ctx := context.Background()
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host1.ID})))
existingSw, err := fleet.SoftwareFromOsqueryRow("htop", "3.4.0-2", "deb_packages", "", "", "", "", "", "", "", "")
require.NoError(t, err)
updateSw, err := fleet.SoftwareFromOsqueryRow("htop", "3.4.1-5", "deb_packages", "", "", "", "", "", "", "", "")
require.NoError(t, err)
_, err = ds.UpdateHostSoftware(ctx, host1.ID, []fleet.Software{*existingSw})
require.NoError(t, err)
tfr, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
softwareInstaller := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
PreInstallQuery: "SELECT",
PostInstallScript: "world",
UninstallScript: "goodbye",
InstallerFile: tfr,
StorageID: "storage",
Filename: "file1",
Title: "htop",
Version: "3.4.1-5",
Source: "deb_packages",
UserID: user.ID,
TeamID: &team1.ID,
BundleIdentifier: "",
Platform: "linux",
SelfService: true,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, softwareInstaller)
require.NoError(t, err)
t.Run("host_software_installs row isnt removed", func(t *testing.T) {
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host1.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)
_, err = ds.applyChangesForNewSoftwareDB(ctx, host1.ID, []fleet.Software{*updateSw})
require.NoError(t, err)
var removed int
err = ds.writer(ctx).Get(&removed, `SELECT removed FROM host_software_installs
WHERE software_title_name = ? ORDER BY updated_at DESC LIMIT 1`, "htop")
require.NoError(t, err)
require.Equal(t, 0, removed)
})
numHosts := 5
var installHosts []*fleet.Host
for i := range 5 {
host := test.NewHost(t, ds, "host-"+strconv.Itoa(i), "", "key"+strconv.Itoa(i), "uuid"+strconv.Itoa(i), time.Now())
err := ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host.ID}))
require.NoError(t, err)
installHosts = append(installHosts, host)
}
t.Run("installer host count is correct", func(t *testing.T) {
for _, host := range installHosts {
_, err = ds.UpdateHostSoftware(ctx, host.ID, []fleet.Software{*existingSw})
require.NoError(t, err)
}
for _, host := range installHosts {
hostInstall1, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostInstall1,
InstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)
_, err = ds.applyChangesForNewSoftwareDB(ctx, host.ID, []fleet.Software{*updateSw})
require.NoError(t, err)
}
counts, err := ds.GetSummaryHostSoftwareInstalls(ctx, installerID)
require.NoError(t, err)
require.Equal(t, numHosts+1, int(counts.Installed)) // include first host's install
})
}
func testPreInsertSoftwareInventory(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Generate test software
softwareChecksums := make(map[string]fleet.Software)
for i := 0; i < 10; i++ {
checksum := make([]byte, 32)
_, _ = crand.Read(checksum)
checksumStr := hex.EncodeToString(checksum)
softwareChecksums[checksumStr] = fleet.Software{
Name: fmt.Sprintf("idempotent-test-%d", i),
Version: fmt.Sprintf("1.0.%d", i),
Source: "test",
Checksum: checksumStr,
}
}
// Pre-insert once
err := ds.preInsertSoftwareInventory(ctx, nil, softwareChecksums, nil)
require.NoError(t, err)
// Count inserted software
var count int
err = ds.writer(ctx).GetContext(ctx, &count,
"SELECT COUNT(*) FROM software WHERE name LIKE 'idempotent-test-%'")
require.NoError(t, err)
require.Equal(t, 10, count)
// Pre-insert again (should be idempotent)
err = ds.preInsertSoftwareInventory(ctx, nil, softwareChecksums, nil)
require.NoError(t, err)
// Count should remain the same
err = ds.writer(ctx).GetContext(ctx, &count,
"SELECT COUNT(*) FROM software WHERE name LIKE 'idempotent-test-%'")
require.NoError(t, err)
require.Equal(t, 10, count)
}
// testUpdateHostBundleIDRenameOnlyNoNewSoftware tests if a host reports ONLY renamed software
// (same bundle ID, different name) with NO new software
func testUpdateHostBundleIDRenameOnlyNoNewSoftware(t *testing.T, ds *Datastore) {
// TEMPORARILY SKIPPED: updateTargetedBundleIDs is commented out for performance reasons
// TODO: Re-enable when updateTargetedBundleIDs is re-enabled
t.Skip("Skipping test: updateTargetedBundleIDs is temporarily disabled for performance reasons")
ctx := t.Context()
host := test.NewHost(t, ds, "rename-test-host", "", "renamekey", "renameuuid", time.Now())
// Initial software with bundle ID
initialSoftware := []fleet.Software{
{Name: "Original.app", Version: "1.0", Source: "apps", BundleIdentifier: "com.example.app"},
{Name: "Another.app", Version: "2.0", Source: "apps", BundleIdentifier: "com.example.another"},
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, initialSoftware)
require.NoError(t, err)
// Verify initial state
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 2)
originalIDs := map[string]uint{}
for _, s := range host.Software {
originalIDs[s.BundleIdentifier] = s.ID
}
// Report ONLY renamed software (same bundle IDs, different names)
// This is the edge case: NO new software, ONLY renames
renamedSoftware := []fleet.Software{
{Name: "Renamed.app", Version: "1.0", Source: "apps", BundleIdentifier: "com.example.app"},
{Name: "AlsoRenamed.app", Version: "2.0", Source: "apps", BundleIdentifier: "com.example.another"},
}
// Complete the update and verify the software was linked correctly
_, err = ds.UpdateHostSoftware(ctx, host.ID, renamedSoftware)
require.NoError(t, err)
// Verify the software entries were reused (not duplicated)
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 2, "Should still have exactly 2 software entries")
// Verify the IDs are the same (software was reused, not recreated)
for _, s := range host.Software {
originalID, ok := originalIDs[s.BundleIdentifier]
require.True(t, ok, "Bundle ID %s should exist", s.BundleIdentifier)
require.Equal(t, originalID, s.ID,
"Software ID should be reused for bundle ID %s", s.BundleIdentifier)
}
// Verify no duplicate software entries were created
var softwareCount int
err = ds.writer(ctx).GetContext(ctx, &softwareCount,
`SELECT COUNT(DISTINCT id) FROM software
WHERE bundle_identifier IN ('com.example.app', 'com.example.another')`)
require.NoError(t, err)
require.Equal(t, 2, softwareCount, "Should have exactly 2 software entries, not duplicates")
}
// testUpdateHostBundleIDRenameWithNewSoftware tests the edge case where a host reports BOTH:
// 1. New software that needs to be inserted
// 2. Existing software with renamed bundle IDs that needs updating
// This tests that both operations work correctly in the same update.
func testUpdateHostBundleIDRenameWithNewSoftware(t *testing.T, ds *Datastore) {
// TEMPORARILY SKIPPED: updateTargetedBundleIDs is commented out for performance reasons
// TODO: Re-enable when updateTargetedBundleIDs is re-enabled
t.Skip("Skipping test: updateTargetedBundleIDs is temporarily disabled for performance reasons")
ctx := t.Context()
host := test.NewHost(t, ds, "mixed-test-host", "", "mixedkey", "mixeduuid", time.Now())
// Step 1: Initial software setup - just one app with bundle ID
initialSoftware := []fleet.Software{
{Name: "Slack.app", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.tinyspeck.slackmacgap"},
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, initialSoftware)
require.NoError(t, err)
// Verify initial state
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 1)
slackOriginalID := host.Software[0].ID
// Step 2: Report BOTH renamed software AND new software in the same update
mixedUpdate := []fleet.Software{
// Renamed existing software (same bundle ID, different name)
{Name: "Slack 2.app", Version: "1.0.0", Source: "apps", BundleIdentifier: "com.tinyspeck.slackmacgap"},
// Brand new software
{Name: "Chrome.app", Version: "110.0", Source: "apps", BundleIdentifier: "com.google.Chrome"},
// Another new software without bundle ID
{Name: "CustomTool", Version: "3.2.1", Source: "programs"},
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, mixedUpdate)
require.NoError(t, err)
// Verify the results
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 3, "Should have 3 software entries total")
// Check each software entry
foundSlack := false
foundChrome := false
foundCustomTool := false
for _, s := range host.Software {
switch s.BundleIdentifier {
case "com.tinyspeck.slackmacgap":
foundSlack = true
// Verify Slack was renamed and ID was reused
require.Equal(t, "Slack 2.app", s.Name, "Slack should be renamed")
require.Equal(t, slackOriginalID, s.ID, "Slack should reuse the same ID")
// Verify name_source was updated
var nameSource string
err = ds.writer(ctx).GetContext(ctx, &nameSource,
`SELECT name_source FROM software WHERE id = ?`, s.ID)
require.NoError(t, err)
require.Equal(t, "bundle_4.67", nameSource, "Name source should indicate bundle ID match")
case "com.google.Chrome":
foundChrome = true
require.Equal(t, "Chrome.app", s.Name)
require.NotEqual(t, slackOriginalID, s.ID, "Chrome should have a different ID")
case "":
if s.Name == "CustomTool" {
foundCustomTool = true
require.Equal(t, "CustomTool", s.Name)
require.NotEqual(t, slackOriginalID, s.ID, "CustomTool should have a different ID")
}
}
}
require.True(t, foundSlack, "Should find renamed Slack")
require.True(t, foundChrome, "Should find new Chrome")
require.True(t, foundCustomTool, "Should find new CustomTool")
// Verify no duplicate software entries were created
var softwareCount int
err = ds.writer(ctx).GetContext(ctx, &softwareCount,
`SELECT COUNT(DISTINCT id) FROM software WHERE bundle_identifier = 'com.tinyspeck.slackmacgap'`)
require.NoError(t, err)
require.Equal(t, 1, softwareCount, "Should have exactly 1 Slack software entry")
// Verify titles were created correctly
var titleCount int
err = ds.writer(ctx).GetContext(ctx, &titleCount,
`SELECT COUNT(DISTINCT id) FROM software_titles`)
require.NoError(t, err)
require.Equal(t, 3, titleCount, "Should have 3 software titles")
}
// testUpdateHostBrowserExtensions explicitly tests browser extension handling.
// Browser extensions have extension IDs and browser fields, but no bundle identifiers.
func testUpdateHostBrowserExtensions(t *testing.T, ds *Datastore) {
ctx := t.Context()
host := test.NewHost(t, ds, "ext-test-host", "", "extkey", "extuuid", time.Now())
// Test Case 1: Add multiple browser extensions
extensions := []fleet.Software{
{Name: "1Password", Version: "1.45.0", Source: "browser_plugins", ExtensionID: "cjpalhdlnbpafiamejdnhcphjbkeiagm", Browser: "chrome"},
{Name: "React Developer Tools", Version: "3.14.2", Source: "browser_plugins", ExtensionID: "cfhdojbkjhnklbpkdaibdccddilifddb", Browser: "chrome"},
{Name: "Grammarly", Version: "14.1.0", Source: "browser_plugins", ExtensionID: "kbfnbcaeplbcioakkpcpgfkobkghlhen", Browser: "chrome"},
{Name: "uBlock Origin", Version: "1.52.2", Source: "browser_plugins", ExtensionID: "cjpalhdlnbpafiamejdnhcphjbkeiagm", Browser: "firefox"}, // Same ext ID, different browser
}
_, err := ds.UpdateHostSoftware(ctx, host.ID, extensions)
require.NoError(t, err)
// Verify extensions were added correctly
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 4, "Should have 4 browser extensions")
// Verify each extension
extensionsByName := make(map[string]fleet.HostSoftwareEntry)
for _, s := range host.Software {
extensionsByName[s.Name] = s
// All browser extensions should have empty bundle identifier
require.Empty(t, s.BundleIdentifier, "Browser extensions should not have bundle identifier")
require.Equal(t, "browser_plugins", s.Source)
}
// Verify specific extensions
require.Contains(t, extensionsByName, "1Password")
require.Contains(t, extensionsByName, "React Developer Tools")
require.Contains(t, extensionsByName, "Grammarly")
require.Contains(t, extensionsByName, "uBlock Origin")
// Test Case 2: Update extension version (same extension ID, new version)
updatedExtensions := []fleet.Software{
{Name: "1Password", Version: "1.46.0", Source: "browser_plugins", ExtensionID: "cjpalhdlnbpafiamejdnhcphjbkeiagm", Browser: "chrome"}, // Version update
{Name: "React Developer Tools", Version: "3.14.2", Source: "browser_plugins", ExtensionID: "cfhdojbkjhnklbpkdaibdccddilifddb", Browser: "chrome"}, // No change
// Grammarly removed
{Name: "uBlock Origin", Version: "1.52.2", Source: "browser_plugins", ExtensionID: "cjpalhdlnbpafiamejdnhcphjbkeiagm", Browser: "firefox"},
{Name: "Bitwarden", Version: "2023.10.1", Source: "browser_plugins", ExtensionID: "nngceckbapebfimnlniiiahkandclblb", Browser: "chrome"}, // New extension
}
_, err = ds.UpdateHostSoftware(ctx, host.ID, updatedExtensions)
require.NoError(t, err)
// Verify update results
err = ds.LoadHostSoftware(ctx, host, false)
require.NoError(t, err)
require.Len(t, host.Software, 4, "Should have 4 browser extensions after update")
// Check version update
extensionsByName = make(map[string]fleet.HostSoftwareEntry)
for _, s := range host.Software {
extensionsByName[s.Name] = s
}
require.Equal(t, "1.46.0", extensionsByName["1Password"].Version, "1Password version should be updated")
require.Contains(t, extensionsByName, "Bitwarden", "New extension should be added")
require.NotContains(t, extensionsByName, "Grammarly", "Removed extension should not be present")
// Test Case 3: Extensions with same name but different browsers/extension IDs
// This simulates cross-browser extensions
crossBrowserExtensions := []fleet.Software{
{Name: "Adblock", Version: "5.0", Source: "browser_plugins", ExtensionID: "gighmmpiobklfepjocnamgkkbiglidom", Browser: "chrome"},
{Name: "Adblock", Version: "5.0", Source: "browser_plugins", ExtensionID: "jid1-NIfFY2CA8fy1tg", Browser: "firefox"},
{Name: "Adblock", Version: "4.9", Source: "browser_plugins", ExtensionID: "pdffkfellgipmhklpdmokmckkkfcopbh", Browser: "edge"},
}
host2 := test.NewHost(t, ds, "ext-test-host2", "", "extkey2", "extuuid2", time.Now())
_, err = ds.UpdateHostSoftware(ctx, host2.ID, crossBrowserExtensions)
require.NoError(t, err)
err = ds.LoadHostSoftware(ctx, host2, false)
require.NoError(t, err)
require.Len(t, host2.Software, 3, "Should have 3 Adblock variants")
// Verify they're tracked as separate software entries
var adblockCount int
for _, s := range host2.Software {
if s.Name == "Adblock" {
adblockCount++
require.Equal(t, "browser_plugins", s.Source)
require.Empty(t, s.BundleIdentifier)
}
}
require.Equal(t, 3, adblockCount, "Should have 3 distinct Adblock entries for different browsers")
// Verify database has correct number of browser extension entries
var totalExtensions int
err = ds.writer(ctx).GetContext(ctx, &totalExtensions,
`SELECT COUNT(DISTINCT id) FROM software WHERE source = 'browser_plugins'`)
require.NoError(t, err)
require.GreaterOrEqual(t, totalExtensions, 7, "Should have at least 7 distinct browser extensions in database")
// Verify extensions don't interfere with bundle ID logic
// Add a macOS app with a bundle ID alongside extensions
mixedSoftware := crossBrowserExtensions
mixedSoftware = append(mixedSoftware,
fleet.Software{Name: "Safari.app", Version: "17.0", Source: "apps", BundleIdentifier: "com.apple.Safari"},
)
host3 := test.NewHost(t, ds, "ext-test-host3", "", "extkey3", "extuuid3", time.Now())
_, err = ds.UpdateHostSoftware(ctx, host3.ID, mixedSoftware)
require.NoError(t, err)
err = ds.LoadHostSoftware(ctx, host3, false)
require.NoError(t, err)
require.Len(t, host3.Software, 4, "Should have 3 extensions + 1 app")
// Verify Safari has bundle ID while extensions don't
for _, s := range host3.Software {
if s.Name == "Safari.app" {
require.Equal(t, "com.apple.Safari", s.BundleIdentifier, "Safari should have bundle ID")
require.Equal(t, "apps", s.Source)
} else if s.Name == "Adblock" {
require.Empty(t, s.BundleIdentifier, "Extensions should not have bundle ID")
require.Equal(t, "browser_plugins", s.Source)
}
}
}