mirror of
https://github.com/fleetdm/fleet
synced 2026-05-21 07:58:31 +00:00
#15565 Replace the use of the isFederated registry key with a keys that check for AAD (Azure Active Directory, now Entra ID) Federated enrollment (`isFederated`) seems to be when windows uses a Discovery MDM endpoint to get its policy and management endpoint configuration. This is always the case when a client is enrolled with fleet, so installations always show up as automatic. It's being replaced by a different key, `AADResourceID`, which appears to identify the resource that controls the automated deployment. In my tests it only appears to be populated when the computer is enrolled through automated deployments. This key appears on both Windows 10 and 11. There is a similar key, `AADTenantID`, which appears to identify the client (tenant) to the Azure cloud. I haven't seen this ID in our systems, so it is likely exclusively used in Azure. Both this key and `AADResourceID` seem to always be set at the same time, so we only check for the `AADResourceID`. I've also added documentation on the registry keys I've analyzed for future reference.
1921 lines
59 KiB
Go
1921 lines
59 KiB
Go
package osquery_utils
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/WatchBeam/clock"
|
|
"github.com/fleetdm/fleet/v4/server/config"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/publicip"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/service/async"
|
|
"github.com/go-kit/log"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
func TestDetailQueryNetworkInterfaces(t *testing.T) {
|
|
var initialHost fleet.Host
|
|
host := initialHost
|
|
|
|
ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["network_interface_unix"].IngestFunc
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
|
|
assert.Equal(t, initialHost, host)
|
|
|
|
var rows []map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(`
|
|
[
|
|
{"address":"10.0.1.2","mac":"bc:d0:74:4b:10:6d"}
|
|
]`),
|
|
&rows,
|
|
))
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
|
|
assert.Equal(t, "10.0.1.2", host.PrimaryIP)
|
|
assert.Equal(t, "bc:d0:74:4b:10:6d", host.PrimaryMac)
|
|
}
|
|
|
|
func TestDetailQueryScheduledQueryStats(t *testing.T) {
|
|
host := fleet.Host{ID: 1}
|
|
ds := new(mock.Store)
|
|
task := async.NewTask(ds, nil, clock.C, config.OsqueryConfig{EnableAsyncHostProcessing: "false"})
|
|
|
|
var gotPackStats []fleet.PackStats
|
|
ds.SaveHostPackStatsFunc = func(ctx context.Context, teamID *uint, hostID uint, stats []fleet.PackStats) error {
|
|
if hostID != host.ID {
|
|
return errors.New("not found")
|
|
}
|
|
gotPackStats = stats
|
|
return nil
|
|
}
|
|
|
|
ingest := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, nil)["scheduled_query_stats"].DirectTaskIngestFunc
|
|
|
|
ctx := context.Background()
|
|
assert.NoError(t, ingest(ctx, log.NewNopLogger(), &host, task, nil))
|
|
assert.Len(t, host.PackStats, 0)
|
|
|
|
resJSON := `
|
|
[
|
|
{
|
|
"average_memory":"33",
|
|
"delimiter":"/",
|
|
"denylisted":"0",
|
|
"executions":"1",
|
|
"interval":"33",
|
|
"last_executed":"1620325191",
|
|
"name":"pack/pack-2/time",
|
|
"output_size":"",
|
|
"query":"SELECT * FROM time",
|
|
"system_time":"100",
|
|
"user_time":"60",
|
|
"wall_time":"180"
|
|
},
|
|
{
|
|
"average_memory":"8000",
|
|
"delimiter":"/",
|
|
"denylisted":"0",
|
|
"executions":"164",
|
|
"interval":"30",
|
|
"last_executed":"1620325191",
|
|
"name":"pack/test/osquery info",
|
|
"output_size":"1337",
|
|
"query":"SELECT * FROM osquery_info",
|
|
"system_time":"150",
|
|
"user_time":"180",
|
|
"wall_time_ms":"0"
|
|
},
|
|
{
|
|
"average_memory":"50400",
|
|
"delimiter":"/",
|
|
"denylisted":"1",
|
|
"executions":"188",
|
|
"interval":"30",
|
|
"last_executed":"1620325203",
|
|
"name":"pack/test/processes?",
|
|
"output_size":"",
|
|
"query":"SELECT * FROM processes",
|
|
"system_time":"140",
|
|
"user_time":"190",
|
|
"wall_time":"1111",
|
|
"wall_time_ms":"1"
|
|
},
|
|
{
|
|
"average_memory":"0",
|
|
"delimiter":"/",
|
|
"denylisted":"0",
|
|
"executions":"1",
|
|
"interval":"3600",
|
|
"last_executed":"1620323381",
|
|
"name":"pack/test/processes?-1",
|
|
"output_size":"",
|
|
"query":"SELECT * FROM processes",
|
|
"system_time":"0",
|
|
"user_time":"0",
|
|
"wall_time_ms":"0"
|
|
},
|
|
{
|
|
"average_memory":"0",
|
|
"delimiter":"/",
|
|
"denylisted":"0",
|
|
"executions":"105",
|
|
"interval":"47",
|
|
"last_executed":"1620325190",
|
|
"name":"pack/test/time",
|
|
"output_size":"",
|
|
"query":"SELECT * FROM time",
|
|
"system_time":"70",
|
|
"user_time":"50",
|
|
"wall_time_ms":"1"
|
|
}
|
|
]
|
|
`
|
|
|
|
var rows []map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(resJSON), &rows))
|
|
|
|
assert.NoError(t, ingest(ctx, log.NewNopLogger(), &host, task, rows))
|
|
assert.Len(t, gotPackStats, 2)
|
|
sort.Slice(gotPackStats, func(i, j int) bool {
|
|
return gotPackStats[i].PackName < gotPackStats[j].PackName
|
|
})
|
|
assert.Equal(t, gotPackStats[0].PackName, "pack-2")
|
|
assert.ElementsMatch(t, gotPackStats[0].QueryStats,
|
|
[]fleet.ScheduledQueryStats{
|
|
{
|
|
ScheduledQueryName: "time",
|
|
PackName: "pack-2",
|
|
AverageMemory: 33,
|
|
Denylisted: false,
|
|
Executions: 1,
|
|
Interval: 33,
|
|
LastExecuted: time.Unix(1620325191, 0).UTC(),
|
|
OutputSize: 0,
|
|
SystemTime: 100,
|
|
UserTime: 60,
|
|
WallTimeMs: 180 * 1000,
|
|
},
|
|
},
|
|
)
|
|
assert.Equal(t, gotPackStats[1].PackName, "test")
|
|
assert.ElementsMatch(t, gotPackStats[1].QueryStats,
|
|
[]fleet.ScheduledQueryStats{
|
|
{
|
|
ScheduledQueryName: "osquery info",
|
|
PackName: "test",
|
|
AverageMemory: 8000,
|
|
Denylisted: false,
|
|
Executions: 164,
|
|
Interval: 30,
|
|
LastExecuted: time.Unix(1620325191, 0).UTC(),
|
|
OutputSize: 1337,
|
|
SystemTime: 150,
|
|
UserTime: 180,
|
|
WallTimeMs: 0,
|
|
},
|
|
{
|
|
ScheduledQueryName: "processes?",
|
|
PackName: "test",
|
|
AverageMemory: 50400,
|
|
Denylisted: true,
|
|
Executions: 188,
|
|
Interval: 30,
|
|
LastExecuted: time.Unix(1620325203, 0).UTC(),
|
|
OutputSize: 0,
|
|
SystemTime: 140,
|
|
UserTime: 190,
|
|
WallTimeMs: 1,
|
|
},
|
|
{
|
|
ScheduledQueryName: "processes?-1",
|
|
PackName: "test",
|
|
AverageMemory: 0,
|
|
Denylisted: false,
|
|
Executions: 1,
|
|
Interval: 3600,
|
|
LastExecuted: time.Unix(1620323381, 0).UTC(),
|
|
OutputSize: 0,
|
|
SystemTime: 0,
|
|
UserTime: 0,
|
|
WallTimeMs: 0,
|
|
},
|
|
{
|
|
ScheduledQueryName: "time",
|
|
PackName: "test",
|
|
AverageMemory: 0,
|
|
Denylisted: false,
|
|
Executions: 105,
|
|
Interval: 47,
|
|
LastExecuted: time.Unix(1620325190, 0).UTC(),
|
|
OutputSize: 0,
|
|
SystemTime: 70,
|
|
UserTime: 50,
|
|
WallTimeMs: 1,
|
|
},
|
|
},
|
|
)
|
|
|
|
assert.NoError(t, ingest(ctx, log.NewNopLogger(), &host, task, nil))
|
|
assert.Len(t, gotPackStats, 0)
|
|
}
|
|
|
|
func sortedKeysCompare(t *testing.T, m map[string]DetailQuery, expectedKeys []string) {
|
|
var keys []string
|
|
for key := range m {
|
|
keys = append(keys, key)
|
|
}
|
|
assert.ElementsMatch(t, keys, expectedKeys)
|
|
}
|
|
|
|
func TestGetDetailQueries(t *testing.T) {
|
|
queriesNoConfig := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)
|
|
|
|
baseQueries := []string{
|
|
"network_interface_unix",
|
|
"network_interface_windows",
|
|
"network_interface_chrome",
|
|
"os_version",
|
|
"os_version_windows",
|
|
"osquery_flags",
|
|
"osquery_info",
|
|
"system_info",
|
|
"uptime",
|
|
"disk_space_unix",
|
|
"disk_space_windows",
|
|
"mdm",
|
|
"mdm_windows",
|
|
"munki_info",
|
|
"google_chrome_profiles",
|
|
"battery",
|
|
"os_windows",
|
|
"os_unix_like",
|
|
"os_chrome",
|
|
"windows_update_history",
|
|
"kubequery_info",
|
|
"orbit_info",
|
|
"disk_encryption_darwin",
|
|
"disk_encryption_linux",
|
|
"disk_encryption_windows",
|
|
"chromeos_profile_user_info",
|
|
}
|
|
|
|
require.Len(t, queriesNoConfig, len(baseQueries))
|
|
sortedKeysCompare(t, queriesNoConfig, baseQueries)
|
|
|
|
queriesWithoutWinOSVuln := GetDetailQueries(context.Background(), config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil, nil)
|
|
require.Len(t, queriesWithoutWinOSVuln, 25)
|
|
|
|
queriesWithUsers := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true})
|
|
qs := append(baseQueries, "users", "users_chrome", "scheduled_query_stats")
|
|
require.Len(t, queriesWithUsers, len(qs))
|
|
sortedKeysCompare(t, queriesWithUsers, qs)
|
|
|
|
queriesWithUsersAndSoftware := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true})
|
|
qs = append(baseQueries, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions", "software_chrome", "scheduled_query_stats")
|
|
require.Len(t, queriesWithUsersAndSoftware, len(qs))
|
|
sortedKeysCompare(t, queriesWithUsersAndSoftware, qs)
|
|
|
|
// test that appropriate mdm queries are added based on app config
|
|
var mdmQueriesBase, mdmQueriesWindows []string
|
|
for k, q := range mdmQueries {
|
|
switch {
|
|
case slices.Equal(q.Platforms, []string{"windows"}):
|
|
mdmQueriesWindows = append(mdmQueriesWindows, k)
|
|
default:
|
|
mdmQueriesBase = append(mdmQueriesBase, k)
|
|
}
|
|
}
|
|
ac := fleet.AppConfig{}
|
|
ac.MDM.EnabledAndConfigured = true
|
|
// windows mdm is disabled by default, windows mdm queries should not be present
|
|
gotQueries := GetDetailQueries(context.Background(), config.FleetConfig{}, &ac, nil)
|
|
wantQueries := append(baseQueries, mdmQueriesBase...)
|
|
require.Len(t, gotQueries, len(wantQueries))
|
|
sortedKeysCompare(t, gotQueries, wantQueries)
|
|
// enable windows mdm, windows mdm queries should be present
|
|
ac.MDM.WindowsEnabledAndConfigured = true
|
|
gotQueries = GetDetailQueries(context.Background(), config.FleetConfig{}, &ac, nil)
|
|
wantQueries = append(wantQueries, mdmQueriesWindows...)
|
|
require.Len(t, gotQueries, len(wantQueries))
|
|
sortedKeysCompare(t, gotQueries, wantQueries)
|
|
}
|
|
|
|
func TestDetailQueriesOSVersionUnixLike(t *testing.T) {
|
|
var initialHost fleet.Host
|
|
host := initialHost
|
|
|
|
ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["os_version"].IngestFunc
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
|
|
assert.Equal(t, initialHost, host)
|
|
|
|
// Rolling release for archlinux
|
|
var rows []map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(`
|
|
[{
|
|
"hostname": "kube2",
|
|
"arch": "x86_64",
|
|
"build": "rolling",
|
|
"codename": "",
|
|
"major": "0",
|
|
"minor": "0",
|
|
"name": "Arch Linux",
|
|
"patch": "0",
|
|
"platform": "arch",
|
|
"platform_like": "",
|
|
"version": ""
|
|
}]`),
|
|
&rows,
|
|
))
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
|
|
assert.Equal(t, "Arch Linux rolling", host.OSVersion)
|
|
|
|
// Simulate a linux with a proper version
|
|
require.NoError(t, json.Unmarshal([]byte(`
|
|
[{
|
|
"hostname": "kube2",
|
|
"arch": "x86_64",
|
|
"build": "rolling",
|
|
"codename": "",
|
|
"major": "1",
|
|
"minor": "2",
|
|
"name": "Arch Linux",
|
|
"patch": "3",
|
|
"platform": "arch",
|
|
"platform_like": "",
|
|
"version": ""
|
|
}]`),
|
|
&rows,
|
|
))
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
|
|
assert.Equal(t, "Arch Linux 1.2.3", host.OSVersion)
|
|
|
|
// Simulate Ubuntu host with incorrect `patch` number
|
|
require.NoError(t, json.Unmarshal([]byte(`
|
|
[{
|
|
"hostname": "kube2",
|
|
"arch": "x86_64",
|
|
"build": "",
|
|
"codename": "bionic",
|
|
"major": "18",
|
|
"minor": "4",
|
|
"name": "Ubuntu",
|
|
"patch": "0",
|
|
"platform": "ubuntu",
|
|
"platform_like": "debian",
|
|
"version": "18.04.5 LTS (Bionic Beaver)"
|
|
}]`),
|
|
&rows,
|
|
))
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
|
|
assert.Equal(t, "Ubuntu 18.04.5 LTS", host.OSVersion)
|
|
}
|
|
|
|
func TestDetailQueriesOSVersionWindows(t *testing.T) {
|
|
var initialHost fleet.Host
|
|
host := initialHost
|
|
|
|
ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["os_version_windows"].IngestFunc
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
|
|
assert.Equal(t, initialHost, host)
|
|
|
|
var rows []map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(`
|
|
[{
|
|
"hostname": "WinBox",
|
|
"arch": "64-bit",
|
|
"build": "22000",
|
|
"codename": "Microsoft Windows 11 Enterprise",
|
|
"major": "10",
|
|
"minor": "0",
|
|
"name": "Microsoft Windows 11 Enterprise",
|
|
"patch": "",
|
|
"platform": "windows",
|
|
"platform_like": "windows",
|
|
"version": "10.0.22000",
|
|
"display_version": "21H2",
|
|
"release_id": ""
|
|
}]`),
|
|
&rows,
|
|
))
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
|
|
assert.Equal(t, "Windows 11 Enterprise 21H2 10.0.22000", host.OSVersion)
|
|
|
|
require.NoError(t, json.Unmarshal([]byte(`
|
|
[{
|
|
"hostname": "WinBox",
|
|
"arch": "64-bit",
|
|
"build": "17763",
|
|
"codename": "Microsoft Windows 10 Enterprise LTSC",
|
|
"major": "10",
|
|
"minor": "0",
|
|
"name": "Microsoft Windows 10 Enterprise LTSC",
|
|
"patch": "",
|
|
"platform": "windows",
|
|
"platform_like": "windows",
|
|
"version": "10.0.17763",
|
|
"display_version": "",
|
|
"release_id": "1809"
|
|
}]`),
|
|
&rows,
|
|
))
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
|
|
assert.Equal(t, "Windows 10 Enterprise LTSC 10.0.17763", host.OSVersion)
|
|
}
|
|
|
|
func TestDetailQueriesOSVersionChrome(t *testing.T) {
|
|
var initialHost fleet.Host
|
|
host := initialHost
|
|
|
|
ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["os_version"].IngestFunc
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
|
|
assert.Equal(t, initialHost, host)
|
|
|
|
var rows []map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(`
|
|
[{
|
|
"hostname": "chromeo",
|
|
"arch": "x86_64",
|
|
"build": "chrome-build",
|
|
"codename": "",
|
|
"major": "1",
|
|
"minor": "3",
|
|
"name": "chromeos",
|
|
"patch": "7",
|
|
"platform": "chrome",
|
|
"platform_like": "chrome",
|
|
"version": "1.3.3.7"
|
|
}]`),
|
|
&rows,
|
|
))
|
|
|
|
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
|
|
assert.Equal(t, "chromeos 1.3.3.7", host.OSVersion)
|
|
}
|
|
|
|
func TestDirectIngestMDMMac(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
var host fleet.Host
|
|
|
|
cases := []struct {
|
|
name string
|
|
got map[string]string
|
|
wantParams []any
|
|
wantErr string
|
|
}{
|
|
{
|
|
"empty server URL",
|
|
map[string]string{
|
|
"enrolled": "false",
|
|
"installed_from_dep": "",
|
|
"server_url": "",
|
|
},
|
|
[]any{false, false, "", false, fleet.UnknownMDMName},
|
|
"",
|
|
},
|
|
{
|
|
"with Fleet payload identifier",
|
|
map[string]string{
|
|
"enrolled": "true",
|
|
"installed_from_dep": "true",
|
|
"server_url": "https://test.example.com",
|
|
"payload_identifier": apple_mdm.FleetPayloadIdentifier,
|
|
},
|
|
[]any{false, true, "https://test.example.com", true, fleet.WellKnownMDMFleet},
|
|
"",
|
|
},
|
|
{
|
|
"with a query string on the server URL",
|
|
map[string]string{
|
|
"enrolled": "true",
|
|
"installed_from_dep": "true",
|
|
"server_url": "https://jamf.com/1/some/path?one=1&two=2",
|
|
},
|
|
[]any{false, true, "https://jamf.com/1/some/path", true, fleet.WellKnownMDMJamf},
|
|
"",
|
|
},
|
|
{
|
|
"with invalid installed_from_dep",
|
|
map[string]string{
|
|
"enrolled": "true",
|
|
"installed_from_dep": "invalid",
|
|
"server_url": "https://jamf.com/1/some/path?one=1&two=2",
|
|
},
|
|
[]any{},
|
|
"parsing installed_from_dep",
|
|
},
|
|
{
|
|
"with invalid enrolled",
|
|
map[string]string{
|
|
"enrolled": "invalid",
|
|
"installed_from_dep": "false",
|
|
"server_url": "https://jamf.com/1/some/path?one=1&two=2",
|
|
},
|
|
[]any{},
|
|
"parsing enrolled",
|
|
},
|
|
{
|
|
"with invalid server_url",
|
|
map[string]string{
|
|
"enrolled": "false",
|
|
"installed_from_dep": "false",
|
|
"server_url": "ht tp://foo.com",
|
|
},
|
|
[]any{},
|
|
"parsing server_url",
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
ds.SetOrUpdateMDMDataFunc = func(ctx context.Context, hostID uint, isServer, enrolled bool, serverURL string, installedFromDep bool, name string, fleetEnrollmentRef string) error {
|
|
require.Equal(t, isServer, c.wantParams[0])
|
|
require.Equal(t, enrolled, c.wantParams[1])
|
|
require.Equal(t, serverURL, c.wantParams[2])
|
|
require.Equal(t, installedFromDep, c.wantParams[3])
|
|
require.Equal(t, name, c.wantParams[4])
|
|
require.Empty(t, fleetEnrollmentRef)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
|
|
return nil
|
|
}
|
|
|
|
err := directIngestMDMMac(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{c.got})
|
|
if c.wantErr != "" {
|
|
require.ErrorContains(t, err, c.wantErr)
|
|
require.False(t, ds.SetOrUpdateMDMDataFuncInvoked)
|
|
|
|
} else {
|
|
require.True(t, ds.SetOrUpdateMDMDataFuncInvoked)
|
|
require.NoError(t, err)
|
|
ds.SetOrUpdateMDMDataFuncInvoked = false
|
|
require.False(t, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDirectIngestMDMFleetEnrollRef(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
var host fleet.Host
|
|
|
|
generateRows := func(serverURL, payloadIdentifier string) []map[string]string {
|
|
return []map[string]string{
|
|
{
|
|
"enrolled": "true",
|
|
"installed_from_dep": "true",
|
|
"server_url": serverURL,
|
|
"payload_identifier": payloadIdentifier,
|
|
},
|
|
}
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
mdmData []map[string]string
|
|
wantServerURL string
|
|
wantEnrollRef string
|
|
wantHostEmailsCalled bool
|
|
}
|
|
|
|
for _, tc := range []testCase{
|
|
{
|
|
name: "Fleet enroll_reference",
|
|
mdmData: generateRows("https://test.example.com?enroll_reference=test-reference", apple_mdm.FleetPayloadIdentifier),
|
|
wantServerURL: "https://test.example.com",
|
|
wantEnrollRef: "test-reference",
|
|
wantHostEmailsCalled: true,
|
|
},
|
|
{
|
|
name: "Fleet no enroll_reference",
|
|
mdmData: generateRows("https://test.example.com", apple_mdm.FleetPayloadIdentifier),
|
|
wantServerURL: "https://test.example.com",
|
|
wantEnrollRef: "",
|
|
wantHostEmailsCalled: false,
|
|
},
|
|
{
|
|
name: "Fleet enrollment_reference",
|
|
mdmData: generateRows("https://test.example.com?enrollment_reference=test-reference", apple_mdm.FleetPayloadIdentifier),
|
|
wantServerURL: "https://test.example.com",
|
|
wantEnrollRef: "test-reference",
|
|
wantHostEmailsCalled: true,
|
|
},
|
|
{
|
|
name: "Fleet enroll_reference with other query params",
|
|
mdmData: generateRows("https://test.example.com?token=abcdefg&enroll_reference=test-reference", apple_mdm.FleetPayloadIdentifier),
|
|
wantServerURL: "https://test.example.com",
|
|
wantEnrollRef: "test-reference",
|
|
wantHostEmailsCalled: true,
|
|
},
|
|
{
|
|
name: "non-Fleet enroll_reference",
|
|
mdmData: generateRows("https://test.example.com?enroll_reference=test-reference", "com.unknown.mdm"),
|
|
wantServerURL: "https://test.example.com",
|
|
wantEnrollRef: "",
|
|
wantHostEmailsCalled: false,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
|
|
require.Equal(t, tc.wantEnrollRef, fleetEnrollmentRef)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateMDMDataFunc = func(ctx context.Context, hostID uint, isServer, enrolled bool, serverURL string, installedFromDep bool, name string, fleetEnrollmentRef string) error {
|
|
require.False(t, isServer)
|
|
require.True(t, enrolled)
|
|
require.True(t, installedFromDep)
|
|
|
|
require.Equal(t, tc.wantServerURL, serverURL)
|
|
require.Equal(t, tc.wantEnrollRef, fleetEnrollmentRef)
|
|
if tc.wantEnrollRef != "" {
|
|
require.NotContains(t, serverURL, tc.wantEnrollRef) // query string is removed
|
|
}
|
|
if tc.mdmData[0]["payload_identifier"] == apple_mdm.FleetPayloadIdentifier {
|
|
require.Equal(t, name, fleet.WellKnownMDMFleet)
|
|
} else {
|
|
require.Equal(t, name, fleet.UnknownMDMName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
err := directIngestMDMMac(context.Background(), log.NewNopLogger(), &host, ds, tc.mdmData)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateMDMDataFuncInvoked)
|
|
ds.SetOrUpdateMDMDataFuncInvoked = false
|
|
require.Equal(t, tc.wantHostEmailsCalled, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
|
|
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked = false
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDirectIngestMDMWindows(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
cases := []struct {
|
|
name string
|
|
data []map[string]string
|
|
wantEnrolled bool
|
|
wantInstalledFromDep bool
|
|
wantIsServer bool
|
|
wantServerURL string
|
|
wantMDMSolName string
|
|
}{
|
|
{
|
|
name: "off empty server URL",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "",
|
|
"aad_resource_id": "https://example.com",
|
|
"provider_id": "Some_ID",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: false,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "",
|
|
},
|
|
{
|
|
name: "off missing aad_resource_id and server url",
|
|
data: []map[string]string{
|
|
{
|
|
"provider_id": "Some_ID",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: false,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "",
|
|
},
|
|
{
|
|
name: "off no rows",
|
|
data: []map[string]string{},
|
|
wantEnrolled: false,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "",
|
|
},
|
|
{
|
|
name: "on automatic",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://example.com",
|
|
"aad_resource_id": "https://example.com",
|
|
"provider_id": "Some_ID",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: true,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://example.com",
|
|
},
|
|
{
|
|
name: "on manual",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://example.com",
|
|
"aad_resource_id": "",
|
|
"provider_id": "Local_Management",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://example.com",
|
|
},
|
|
{
|
|
name: "on manual missing aad_resource_id",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://example.com",
|
|
"provider_id": "Some_ID",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://example.com",
|
|
},
|
|
{
|
|
name: "is_server",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://example.com",
|
|
"aad_resource_id": "https://example.com",
|
|
"provider_id": "Some_ID",
|
|
"installation_type": "Windows SeRvEr 99.9",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: true,
|
|
wantIsServer: true,
|
|
wantServerURL: "https://example.com",
|
|
},
|
|
|
|
// Test that names are being calculated correctly
|
|
|
|
{
|
|
name: "on manual jumpcloud",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://jumpcloud.com",
|
|
"aad_resource_id": "",
|
|
"provider_id": "Local_Management",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://jumpcloud.com",
|
|
wantMDMSolName: fleet.WellKnownMDMJumpCloud,
|
|
},
|
|
{
|
|
name: "on manual airwatch",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://airwatch.com",
|
|
"aad_resource_id": "",
|
|
"provider_id": "Local_Management",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://airwatch.com",
|
|
wantMDMSolName: fleet.WellKnownMDMVMWare,
|
|
},
|
|
{
|
|
name: "on manual awmdm",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://awmdm.com",
|
|
"aad_resource_id": "",
|
|
"provider_id": "Local_Management",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://awmdm.com",
|
|
wantMDMSolName: fleet.WellKnownMDMVMWare,
|
|
},
|
|
{
|
|
name: "on manual microsoft",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://microsoft.com",
|
|
"aad_resource_id": "",
|
|
"provider_id": "Local_Management",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://microsoft.com",
|
|
wantMDMSolName: fleet.WellKnownMDMIntune,
|
|
},
|
|
{
|
|
name: "on manual fleetdm cloud hosted",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://fleetdm.com",
|
|
"aad_resource_id": "",
|
|
"provider_id": "Local_Management",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://fleetdm.com",
|
|
wantMDMSolName: fleet.WellKnownMDMFleet,
|
|
},
|
|
|
|
{
|
|
name: "on manual fleetdm self hosted",
|
|
data: []map[string]string{
|
|
{
|
|
"discovery_service_url": "https://myinstall.local",
|
|
"aad_resource_id": "",
|
|
"provider_id": "Fleet",
|
|
"installation_type": "Client",
|
|
},
|
|
},
|
|
wantEnrolled: true,
|
|
wantInstalledFromDep: false,
|
|
wantIsServer: false,
|
|
wantServerURL: "https://myinstall.local",
|
|
wantMDMSolName: fleet.WellKnownMDMFleet,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
ds.SetOrUpdateMDMDataFunc = func(ctx context.Context, hostID uint, isServer, enrolled bool, serverURL string, installedFromDep bool, name string, fleetEnrollmentRef string) error {
|
|
require.Equal(t, c.wantEnrolled, enrolled)
|
|
require.Equal(t, c.wantInstalledFromDep, installedFromDep)
|
|
require.Equal(t, c.wantIsServer, isServer)
|
|
require.Equal(t, c.wantServerURL, serverURL)
|
|
require.Equal(t, c.wantMDMSolName, name)
|
|
require.Empty(t, fleetEnrollmentRef)
|
|
return nil
|
|
}
|
|
ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFunc = func(ctx context.Context, hostID uint, fleetEnrollmentRef string) error {
|
|
return nil
|
|
}
|
|
})
|
|
err := directIngestMDMWindows(context.Background(), log.NewNopLogger(), &fleet.Host{}, ds, c.data)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateMDMDataFuncInvoked)
|
|
ds.SetOrUpdateMDMDataFuncInvoked = false
|
|
require.False(t, ds.SetOrUpdateHostEmailsFromMdmIdpAccountsFuncInvoked)
|
|
}
|
|
}
|
|
|
|
func TestDirectIngestChromeProfiles(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.ReplaceHostDeviceMappingFunc = func(ctx context.Context, hostID uint, mapping []*fleet.HostDeviceMapping, source string) error {
|
|
require.Equal(t, hostID, uint(1))
|
|
require.Equal(t, mapping, []*fleet.HostDeviceMapping{
|
|
{HostID: hostID, Email: "test@example.com", Source: "google_chrome_profiles"},
|
|
{HostID: hostID, Email: "test+2@example.com", Source: "google_chrome_profiles"},
|
|
})
|
|
require.Equal(t, source, "google_chrome_profiles")
|
|
return nil
|
|
}
|
|
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
}
|
|
|
|
err := directIngestChromeProfiles(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
|
|
{"email": "test@example.com"},
|
|
{"email": "test+2@example.com"},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, ds.ReplaceHostDeviceMappingFuncInvoked)
|
|
}
|
|
|
|
func TestDirectIngestBattery(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.ReplaceHostBatteriesFunc = func(ctx context.Context, id uint, mappings []*fleet.HostBattery) error {
|
|
require.Equal(t, mappings, []*fleet.HostBattery{
|
|
{HostID: uint(1), SerialNumber: "a", CycleCount: 2, Health: "Good"},
|
|
{HostID: uint(1), SerialNumber: "c", CycleCount: 3, Health: strings.Repeat("z", 40)},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
}
|
|
|
|
err := directIngestBattery(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
|
|
{"serial_number": "a", "cycle_count": "2", "health": "Good"},
|
|
{"serial_number": "c", "cycle_count": "3", "health": strings.Repeat("z", 100)},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, ds.ReplaceHostBatteriesFuncInvoked)
|
|
}
|
|
|
|
func TestDirectIngestOSWindows(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
testCases := []struct {
|
|
expected fleet.OperatingSystem
|
|
data []map[string]string
|
|
}{
|
|
{
|
|
expected: fleet.OperatingSystem{
|
|
Name: "Microsoft Windows 11 Enterprise 21H2",
|
|
Version: "10.0.22000.795",
|
|
Arch: "64-bit",
|
|
KernelVersion: "10.0.22000.795",
|
|
DisplayVersion: "21H2",
|
|
},
|
|
data: []map[string]string{
|
|
{"name": "Microsoft Windows 11 Enterprise", "display_version": "21H2", "version": "10.0.22000.795", "release_id": "", "arch": "64-bit", "kernel_version": "10.0.22000.795"},
|
|
},
|
|
},
|
|
{
|
|
expected: fleet.OperatingSystem{
|
|
Name: "Microsoft Windows 10 Enterprise", // no display_version
|
|
Version: "10.0.17763.2183",
|
|
Arch: "64-bit",
|
|
KernelVersion: "10.0.17763.2183",
|
|
DisplayVersion: "",
|
|
},
|
|
data: []map[string]string{
|
|
{"name": "Microsoft Windows 10 Enterprise", "display_version": "", "version": "10.0.17763", "release_id": "1809", "arch": "64-bit", "kernel_version": "10.0.17763.2183"},
|
|
},
|
|
},
|
|
}
|
|
|
|
host := fleet.Host{ID: 1}
|
|
|
|
for _, tt := range testCases {
|
|
ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error {
|
|
require.Equal(t, host.ID, hostID)
|
|
require.Equal(t, tt.expected, hostOS)
|
|
return nil
|
|
}
|
|
|
|
err := directIngestOSWindows(context.Background(), log.NewNopLogger(), &host, ds, tt.data)
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, ds.UpdateHostOperatingSystemFuncInvoked)
|
|
ds.UpdateHostOperatingSystemFuncInvoked = false
|
|
}
|
|
}
|
|
|
|
func TestDirectIngestOSUnixLike(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
for i, tc := range []struct {
|
|
data []map[string]string
|
|
expected fleet.OperatingSystem
|
|
}{
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "macOS",
|
|
"version": "12.5",
|
|
"major": "12",
|
|
"minor": "5",
|
|
"patch": "0",
|
|
"build": "21G72",
|
|
"arch": "x86_64",
|
|
"kernel_version": "21.6.0",
|
|
},
|
|
},
|
|
expected: fleet.OperatingSystem{
|
|
Name: "macOS",
|
|
Version: "12.5.0",
|
|
Arch: "x86_64",
|
|
KernelVersion: "21.6.0",
|
|
},
|
|
},
|
|
// macOS with Rapid Security Response version
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "macOS",
|
|
"version": "13.4.1",
|
|
"major": "13",
|
|
"minor": "4",
|
|
"patch": "1",
|
|
"build": "22F82",
|
|
"arch": "arm64",
|
|
"kernel_version": "21.6.0",
|
|
"extra": "(c) ",
|
|
},
|
|
},
|
|
expected: fleet.OperatingSystem{
|
|
Name: "macOS",
|
|
Version: "13.4.1 (c)",
|
|
Arch: "arm64",
|
|
KernelVersion: "21.6.0",
|
|
},
|
|
},
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "Ubuntu",
|
|
"version": "20.04.2 LTS (Focal Fossa)",
|
|
"major": "20",
|
|
"minor": "4",
|
|
"patch": "0",
|
|
"build": "",
|
|
"arch": "x86_64",
|
|
"kernel_version": "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
expected: fleet.OperatingSystem{
|
|
Name: "Ubuntu",
|
|
Version: "20.04.2 LTS",
|
|
Arch: "x86_64",
|
|
KernelVersion: "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "CentOS Linux",
|
|
"version": "CentOS Linux release 7.9.2009 (Core)",
|
|
"major": "7",
|
|
"minor": "9",
|
|
"patch": "2009",
|
|
"build": "",
|
|
"arch": "x86_64",
|
|
"kernel_version": "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
expected: fleet.OperatingSystem{
|
|
Name: "CentOS Linux",
|
|
Version: "7.9.2009",
|
|
Arch: "x86_64",
|
|
KernelVersion: "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "Debian GNU/Linux",
|
|
"version": "10 (buster)",
|
|
"major": "10",
|
|
"minor": "0",
|
|
"patch": "0",
|
|
"build": "",
|
|
"arch": "x86_64",
|
|
"kernel_version": "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
expected: fleet.OperatingSystem{
|
|
Name: "Debian GNU/Linux",
|
|
Version: "10.0.0",
|
|
Arch: "x86_64",
|
|
KernelVersion: "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "CentOS Linux",
|
|
"version": "CentOS Linux release 7.9.2009 (Core)",
|
|
"major": "7",
|
|
"minor": "9",
|
|
"patch": "2009",
|
|
"build": "",
|
|
"arch": "x86_64",
|
|
"kernel_version": "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
expected: fleet.OperatingSystem{
|
|
Name: "CentOS Linux",
|
|
Version: "7.9.2009",
|
|
Arch: "x86_64",
|
|
KernelVersion: "5.10.76-linuxkit",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.expected.Name, func(t *testing.T) {
|
|
ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error {
|
|
require.Equal(t, uint(i), hostID)
|
|
require.Equal(t, tc.expected, hostOS)
|
|
return nil
|
|
}
|
|
|
|
err := directIngestOSUnixLike(context.Background(), log.NewNopLogger(), &fleet.Host{ID: uint(i)}, ds, tc.data)
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, ds.UpdateHostOperatingSystemFuncInvoked)
|
|
ds.UpdateHostOperatingSystemFuncInvoked = false
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppConfigReplaceQuery(t *testing.T) {
|
|
queries := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true})
|
|
originalQuery := queries["users"].Query
|
|
|
|
replacementMap := make(map[string]*string)
|
|
replacementMap["users"] = ptr.String("select 1 from blah")
|
|
queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
|
|
assert.NotEqual(t, originalQuery, queries["users"].Query)
|
|
assert.Equal(t, "select 1 from blah", queries["users"].Query)
|
|
|
|
replacementMap["users"] = nil
|
|
queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
|
|
_, exists := queries["users"]
|
|
assert.False(t, exists)
|
|
|
|
// put the query back again
|
|
replacementMap["users"] = ptr.String("select 1 from blah")
|
|
queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
|
|
assert.NotEqual(t, originalQuery, queries["users"].Query)
|
|
assert.Equal(t, "select 1 from blah", queries["users"].Query)
|
|
|
|
// empty strings are also ignored
|
|
replacementMap["users"] = ptr.String("")
|
|
queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
|
|
_, exists = queries["users"]
|
|
assert.False(t, exists)
|
|
}
|
|
|
|
func TestDirectIngestSoftware(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ctx := context.Background()
|
|
logger := log.NewNopLogger()
|
|
host := fleet.Host{ID: uint(1)}
|
|
|
|
t.Run("ingesting installed software path", func(t *testing.T) {
|
|
data := []map[string]string{
|
|
{
|
|
"name": "Software 1",
|
|
"version": "12.5.0",
|
|
"source": "apps",
|
|
"bundle_identifier": "com.bundle.com",
|
|
"vendor": "EvilCorp",
|
|
"installed_path": "",
|
|
},
|
|
{
|
|
"name": "Software 2",
|
|
"version": "0.0.1",
|
|
"source": "apps",
|
|
"bundle_identifier": "coms.widgets.com",
|
|
"vendor": "widgets",
|
|
"installed_path": "/tmp/some_path",
|
|
},
|
|
}
|
|
|
|
ds.UpdateHostSoftwareFunc = func(ctx context.Context, hostID uint, software []fleet.Software) (*fleet.UpdateHostSoftwareDBResult, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
t.Run("errors are reported back", func(t *testing.T) {
|
|
ds.UpdateHostSoftwareInstalledPathsFunc = func(ctx context.Context, hostID uint, sPaths map[string]struct{}, result *fleet.UpdateHostSoftwareDBResult) error {
|
|
return errors.New("some error")
|
|
}
|
|
require.Error(t, directIngestSoftware(ctx, logger, &host, ds, data), "some error")
|
|
ds.UpdateHostSoftwareInstalledPathsFuncInvoked = false
|
|
})
|
|
|
|
t.Run("only entries with installed_path set are persisted", func(t *testing.T) {
|
|
var calledWith map[string]struct{}
|
|
ds.UpdateHostSoftwareInstalledPathsFunc = func(ctx context.Context, hostID uint, sPaths map[string]struct{}, result *fleet.UpdateHostSoftwareDBResult) error {
|
|
calledWith = make(map[string]struct{})
|
|
for k, v := range sPaths {
|
|
calledWith[k] = v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
require.NoError(t, directIngestSoftware(ctx, logger, &host, ds, data))
|
|
require.True(t, ds.UpdateHostSoftwareFuncInvoked)
|
|
|
|
require.Len(t, calledWith, 1)
|
|
require.Contains(t, strings.Join(maps.Keys(calledWith), " "), fmt.Sprintf("%s%s%s", data[1]["installed_path"], fleet.SoftwareFieldSeparator, data[1]["name"]))
|
|
|
|
ds.UpdateHostSoftwareInstalledPathsFuncInvoked = false
|
|
})
|
|
})
|
|
|
|
t.Run("vendor gets truncated", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
data []map[string]string
|
|
expected string
|
|
}{
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "Software 1",
|
|
"version": "12.5",
|
|
"source": "My backyard",
|
|
"bundle_identifier": "",
|
|
"vendor": "Fleet",
|
|
},
|
|
},
|
|
expected: "Fleet",
|
|
},
|
|
{
|
|
data: []map[string]string{
|
|
{
|
|
"name": "Software 1",
|
|
"version": "12.5",
|
|
"source": "My backyard",
|
|
"bundle_identifier": "",
|
|
"vendor": `oFZTwTV5WxJt02EVHEBcnhLzuJ8wnxKwfbabPWy7yTSiQbabEcAGDVmoXKZEZJLWObGD0cVfYptInHYgKjtDeDsBh2a8669EnyAqyBECXbFjSh1111`,
|
|
},
|
|
},
|
|
expected: `oFZTwTV5WxJt02EVHEBcnhLzuJ8wnxKwfbabPWy7yTSiQbabEcAGDVmoXKZEZJLWObGD0cVfYptInHYgKjtDeDsBh2a8669EnyAqyBECXbFjSh1...`,
|
|
},
|
|
} {
|
|
ds.UpdateHostSoftwareFunc = func(ctx context.Context, hostID uint, software []fleet.Software) (*fleet.UpdateHostSoftwareDBResult, error) {
|
|
require.Len(t, software, 1)
|
|
require.Equal(t, tc.expected, software[0].Vendor)
|
|
return nil, nil
|
|
}
|
|
|
|
ds.UpdateHostSoftwareInstalledPathsFunc = func(ctx context.Context, hostID uint, sPaths map[string]struct{}, result *fleet.UpdateHostSoftwareDBResult) error {
|
|
// NOP - This functionality is tested elsewhere
|
|
return nil
|
|
}
|
|
|
|
require.NoError(t, directIngestSoftware(ctx, logger, &host, ds, tc.data))
|
|
require.True(t, ds.UpdateHostSoftwareFuncInvoked)
|
|
ds.UpdateHostSoftwareFuncInvoked = false
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDirectIngestWindowsUpdateHistory(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ds.InsertWindowsUpdatesFunc = func(ctx context.Context, hostID uint, updates []fleet.WindowsUpdate) error {
|
|
require.Len(t, updates, 6)
|
|
require.ElementsMatch(t, []fleet.WindowsUpdate{
|
|
{KBID: 2267602, DateEpoch: 1657929207},
|
|
{KBID: 890830, DateEpoch: 1658226954},
|
|
{KBID: 5013887, DateEpoch: 1658225364},
|
|
{KBID: 5005463, DateEpoch: 1658225225},
|
|
{KBID: 5010472, DateEpoch: 1658224963},
|
|
{KBID: 4052623, DateEpoch: 1657929544},
|
|
}, updates)
|
|
return nil
|
|
}
|
|
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
}
|
|
|
|
payload := []map[string]string{
|
|
{"date": "1659392951", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.1239.0)"},
|
|
{"date": "1658271402", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.442.0)"},
|
|
{"date": "1658228495", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.415.0)"},
|
|
{"date": "1658226954", "title": "Windows Malicious Software Removal Tool x64 - v5.103 (KB890830)"},
|
|
{"date": "1658225364", "title": "2022-06 Cumulative Update for .NET Framework 3.5 and 4.8 for Windows 10 Version 21H2 for x64 (KB5013887)"},
|
|
{"date": "1658225225", "title": "2022-04 Update for Windows 10 Version 21H2 for x64-based Systems (KB5005463)"},
|
|
{"date": "1658224963", "title": "2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8 for Windows 10 Version 21H2 for x64 (KB5010472)"},
|
|
{"date": "1658222131", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.400.0)"},
|
|
{"date": "1658189063", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.376.0)"},
|
|
{"date": "1658185542", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.386.0)"},
|
|
{"date": "1657929544", "title": "Update for Microsoft Defender Antivirus antimalware platform - KB4052623 (Version 4.18.2205.7)"},
|
|
{"date": "1657929207", "title": "Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.371.203.0)"},
|
|
}
|
|
|
|
err := directIngestWindowsUpdateHistory(context.Background(), log.NewNopLogger(), &host, ds, payload)
|
|
require.NoError(t, err)
|
|
require.True(t, ds.InsertWindowsUpdatesFuncInvoked)
|
|
}
|
|
|
|
func TestIngestKubequeryInfo(t *testing.T) {
|
|
err := ingestKubequeryInfo(context.Background(), log.NewNopLogger(), &fleet.Host{}, nil)
|
|
require.Error(t, err)
|
|
err = ingestKubequeryInfo(context.Background(), log.NewNopLogger(), &fleet.Host{}, []map[string]string{})
|
|
require.Error(t, err)
|
|
err = ingestKubequeryInfo(context.Background(), log.NewNopLogger(), &fleet.Host{}, []map[string]string{
|
|
{
|
|
"cluster_name": "foo",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestDirectDiskEncryption(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
var expectEncrypted bool
|
|
ds.SetOrUpdateHostDisksEncryptionFunc = func(ctx context.Context, id uint, encrypted bool) error {
|
|
assert.Equal(t, expectEncrypted, encrypted)
|
|
return nil
|
|
}
|
|
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
}
|
|
|
|
// set to true (osquery returned a row)
|
|
expectEncrypted = true
|
|
err := directIngestDiskEncryption(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
|
|
{"col1": "1"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDisksEncryptionFuncInvoked)
|
|
ds.SetOrUpdateHostDisksEncryptionFuncInvoked = false
|
|
|
|
// set to false (osquery returned nothing)
|
|
expectEncrypted = false
|
|
err = directIngestDiskEncryption(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDisksEncryptionFuncInvoked)
|
|
ds.SetOrUpdateHostDisksEncryptionFuncInvoked = false
|
|
}
|
|
|
|
func TestDirectIngestDiskEncryptionLinux(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
var expectEncrypted bool
|
|
ds.SetOrUpdateHostDisksEncryptionFunc = func(ctx context.Context, id uint, encrypted bool) error {
|
|
assert.Equal(t, expectEncrypted, encrypted)
|
|
return nil
|
|
}
|
|
host := fleet.Host{
|
|
ID: 1,
|
|
}
|
|
|
|
expectEncrypted = false
|
|
err := directIngestDiskEncryptionLinux(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDisksEncryptionFuncInvoked)
|
|
ds.SetOrUpdateHostDisksEncryptionFuncInvoked = false
|
|
|
|
expectEncrypted = true
|
|
err = directIngestDiskEncryptionLinux(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
|
|
{"path": "/etc/hosts", "encrypted": "0"},
|
|
{"path": "/tmp", "encrypted": "0"},
|
|
{"path": "/", "encrypted": "1"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDisksEncryptionFuncInvoked)
|
|
}
|
|
|
|
func TestDirectIngestDiskEncryptionKeyDarwin(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ctx := context.Background()
|
|
logger := log.NewNopLogger()
|
|
host := &fleet.Host{ID: 1}
|
|
|
|
var wantKey string
|
|
|
|
mockFileLines := func(wantKey string, wantEncrypted string) []map[string]string {
|
|
var output []map[string]string
|
|
scanner := bufio.NewScanner(bytes.NewBuffer([]byte(wantKey)))
|
|
scanner.Split(bufio.ScanLines)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
item := make(map[string]string)
|
|
item["hex_line"] = hex.EncodeToString([]byte(line))
|
|
item["encrypted"] = wantEncrypted
|
|
output = append(output, item)
|
|
}
|
|
return output
|
|
}
|
|
|
|
mockFilevaultPRK := func(wantKey string, wantEncrypted string) []map[string]string {
|
|
return []map[string]string{
|
|
{"filevault_key": base64.StdEncoding.EncodeToString([]byte(wantKey)), "encrypted": wantEncrypted},
|
|
}
|
|
}
|
|
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error {
|
|
if base64.StdEncoding.EncodeToString([]byte(wantKey)) != encryptedBase64Key {
|
|
return errors.New("key mismatch")
|
|
}
|
|
if host.ID != hostID {
|
|
return errors.New("host ID mismatch")
|
|
}
|
|
if encryptedBase64Key == "" && (decryptable == nil || *decryptable == true) {
|
|
return errors.New("decryptable should be false if the key is empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
t.Run("empty key", func(t *testing.T) {
|
|
err := directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{})
|
|
require.NoError(t, err)
|
|
require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
|
|
err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{})
|
|
require.NoError(t, err)
|
|
require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
|
|
err = directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "0"}})
|
|
require.NoError(t, err)
|
|
require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
|
|
err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "0"}})
|
|
require.NoError(t, err)
|
|
require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
|
|
err = directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1"}})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
|
|
err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1"}})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
|
|
err = directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1", "hex_line": ""}})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
|
|
err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1", "filevault_key": ""}})
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
})
|
|
|
|
t.Run("key contains new lines and carriage return", func(t *testing.T) {
|
|
wantKey = "This is only a \n\r\n\n test."
|
|
|
|
err := directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, mockFileLines(wantKey, "1"))
|
|
// it is a known limitation with the current file_lines implementation that causes this to fail
|
|
// because it relies on bufio.ScanLines, which drops "\r" from "\r\n"
|
|
require.ErrorContains(t, err, "key mismatch")
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
|
|
err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, mockFilevaultPRK(wantKey, "1"))
|
|
// filevault_prk does not have the scan lines limitation
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
})
|
|
|
|
t.Run("key contains new lines", func(t *testing.T) {
|
|
wantKey = "This is only a \n\n\n test."
|
|
|
|
err := directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, mockFileLines(wantKey, "1"))
|
|
// new lines are not a problem if they are not preceded by carriage return
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
|
|
err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, mockFilevaultPRK(wantKey, "1"))
|
|
// filevault_prk does not have the scan lines limitation
|
|
require.NoError(t, err)
|
|
require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked)
|
|
ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false
|
|
})
|
|
}
|
|
|
|
func TestDirectIngestHostMacOSProfiles(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ctx := context.Background()
|
|
logger := log.NewNopLogger()
|
|
h := &fleet.Host{ID: 1}
|
|
|
|
toRows := func(profs []*fleet.HostMacOSProfile) []map[string]string {
|
|
rows := make([]map[string]string, len(profs))
|
|
for i, p := range profs {
|
|
rows[i] = map[string]string{
|
|
"identifier": p.Identifier,
|
|
"display_name": p.DisplayName,
|
|
"install_date": p.InstallDate.Format("2006-01-02 15:04:05 -0700"),
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
var installedProfiles []*fleet.HostMacOSProfile
|
|
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
|
|
require.Equal(t, h.ID, host.ID)
|
|
expected := make(map[string]*fleet.ExpectedMDMProfile, len(installedProfiles))
|
|
for _, p := range installedProfiles {
|
|
expected[p.Identifier] = &fleet.ExpectedMDMProfile{
|
|
Identifier: p.Identifier,
|
|
EarliestInstallDate: p.InstallDate,
|
|
}
|
|
}
|
|
return expected, nil
|
|
}
|
|
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify, toFailed, toRetry []string) error {
|
|
require.Equal(t, h.UUID, host.UUID)
|
|
require.Equal(t, len(installedProfiles), len(toVerify))
|
|
require.Len(t, toFailed, 0)
|
|
require.Len(t, toRetry, 0)
|
|
for _, p := range installedProfiles {
|
|
require.Contains(t, toVerify, p.Identifier)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// expect no error: happy path
|
|
installedProfiles = []*fleet.HostMacOSProfile{
|
|
{
|
|
Identifier: "com.example.test",
|
|
DisplayName: "Test Profile",
|
|
InstallDate: time.Now().Truncate(time.Second),
|
|
},
|
|
}
|
|
rows := toRows(installedProfiles)
|
|
require.NoError(t, directIngestMacOSProfiles(ctx, logger, h, ds, rows))
|
|
|
|
// expect no error: identifer or display name is empty
|
|
installedProfiles = append(installedProfiles, &fleet.HostMacOSProfile{
|
|
Identifier: "",
|
|
DisplayName: "",
|
|
InstallDate: time.Now().Truncate(time.Second),
|
|
})
|
|
rows = toRows(installedProfiles)
|
|
require.NoError(t, directIngestMacOSProfiles(ctx, logger, h, ds, rows))
|
|
|
|
// expect no error: empty rows
|
|
require.NoError(t, directIngestMacOSProfiles(ctx, logger, h, ds, []map[string]string{}))
|
|
|
|
// expect error: install date format is not "2006-01-02 15:04:05 -0700"
|
|
rows[0]["install_date"] = time.Now().Format(time.UnixDate)
|
|
require.ErrorContains(t, directIngestMacOSProfiles(ctx, logger, h, ds, rows), "parsing time")
|
|
}
|
|
|
|
func TestDirectIngestMDMDeviceIDWindows(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
ctx := context.Background()
|
|
logger := log.NewNopLogger()
|
|
host := &fleet.Host{ID: 1, UUID: "mdm-windows-hw-uuid"}
|
|
|
|
ds.UpdateMDMWindowsEnrollmentsHostUUIDFunc = func(ctx context.Context, hostUUID string, deviceID string) error {
|
|
require.NotEmpty(t, deviceID)
|
|
require.Equal(t, host.UUID, hostUUID)
|
|
return nil
|
|
}
|
|
|
|
// if no rows, assume the registry key is not present (i.e. mdm is turned off) and do nothing
|
|
require.NoError(t, directIngestMDMDeviceIDWindows(ctx, logger, host, ds, []map[string]string{}))
|
|
require.False(t, ds.UpdateMDMWindowsEnrollmentsHostUUIDFuncInvoked)
|
|
|
|
// if multiple rows, expect error
|
|
require.Error(t, directIngestMDMDeviceIDWindows(ctx, logger, host, ds, []map[string]string{
|
|
{"name": "mdm-windows-hostname", "data": "mdm-windows-device-id"},
|
|
{"name": "mdm-windows-hostname2", "data": "mdm-windows-device-id2"},
|
|
}))
|
|
require.False(t, ds.UpdateMDMWindowsEnrollmentsHostUUIDFuncInvoked)
|
|
|
|
// happy path
|
|
require.NoError(t, directIngestMDMDeviceIDWindows(ctx, logger, host, ds, []map[string]string{
|
|
{"name": "mdm-windows-hostname", "data": "mdm-windows-device-id"},
|
|
}))
|
|
require.True(t, ds.UpdateMDMWindowsEnrollmentsHostUUIDFuncInvoked)
|
|
}
|
|
|
|
func TestSanitizeSoftware(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
h *fleet.Host
|
|
s *fleet.Software
|
|
sanitized *fleet.Software
|
|
}{
|
|
{
|
|
name: "Microsoft Teams.app on macOS",
|
|
h: &fleet.Host{
|
|
Platform: "darwin",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Microsoft Teams.app",
|
|
Version: "1.00.622155",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Microsoft Teams.app",
|
|
Version: "1.6.00.22155",
|
|
},
|
|
},
|
|
{
|
|
name: "Microsoft Teams not on macOS",
|
|
h: &fleet.Host{
|
|
Platform: "windows",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Microsoft Teams",
|
|
Version: "1.6.00.22378",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Microsoft Teams",
|
|
Version: "1.6.00.22378",
|
|
},
|
|
},
|
|
{
|
|
name: "Other.app on macOS",
|
|
h: &fleet.Host{
|
|
Platform: "darwin",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Other.app",
|
|
Version: "1.2.3",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Other.app",
|
|
Version: "1.2.3",
|
|
},
|
|
},
|
|
{
|
|
name: "Cloudflare WARP on Windows, version not using full year",
|
|
h: &fleet.Host{
|
|
Platform: "windows",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "23.9.248.0",
|
|
Source: "programs",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "2023.9.248.0",
|
|
Source: "programs",
|
|
},
|
|
},
|
|
{
|
|
name: "Cloudflare WARP on Windows, version using full year",
|
|
h: &fleet.Host{
|
|
Platform: "windows",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "2023.9.248.0",
|
|
Source: "programs",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "2023.9.248.0",
|
|
Source: "programs",
|
|
},
|
|
},
|
|
{
|
|
name: "Cloudflare WARP on Windows with invalid version",
|
|
h: &fleet.Host{
|
|
Platform: "windows",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "foobar",
|
|
Source: "programs",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "foobar",
|
|
Source: "programs",
|
|
},
|
|
},
|
|
{
|
|
name: "Cloudflare WARP on Windows with invalid version",
|
|
h: &fleet.Host{
|
|
Platform: "windows",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "foo.bar",
|
|
Source: "programs",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Cloudflare WARP",
|
|
Version: "foo.bar",
|
|
Source: "programs",
|
|
},
|
|
},
|
|
{
|
|
name: "Other on Windows",
|
|
h: &fleet.Host{
|
|
Platform: "windows",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Other",
|
|
Version: "1.2.3",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Other",
|
|
Version: "1.2.3",
|
|
},
|
|
},
|
|
{
|
|
name: "Citrix Workspace on Windows",
|
|
h: &fleet.Host{
|
|
Platform: "windows",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Citrix Workspace 2309",
|
|
Version: "23.9.1.104",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Citrix Workspace 2309",
|
|
Version: "2309.1.104",
|
|
},
|
|
},
|
|
{
|
|
name: "Citrix Workspace on Mac",
|
|
h: &fleet.Host{
|
|
Platform: "darwin",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Citrix Workspace.app",
|
|
Version: "23.9.1.104",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Citrix Workspace.app",
|
|
Version: "2309.1.104",
|
|
},
|
|
},
|
|
{
|
|
name: "Citrix Workspace with correct versioning",
|
|
h: &fleet.Host{
|
|
Platform: "darwin",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Citrix Workspace.app",
|
|
Version: "2400.1.104",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Citrix Workspace.app",
|
|
Version: "2400.1.104",
|
|
},
|
|
},
|
|
{
|
|
name: "MS Teams classic on MacOS",
|
|
h: &fleet.Host{
|
|
Platform: "darwin",
|
|
},
|
|
s: &fleet.Software{
|
|
Name: "Microsoft Teams classic.app",
|
|
Version: "1.00.634263",
|
|
},
|
|
sanitized: &fleet.Software{
|
|
Name: "Microsoft Teams classic.app",
|
|
Version: "1.6.00.34263",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
sanitizeSoftware(tc.h, tc.s, log.NewNopLogger())
|
|
require.Equal(t, tc.sanitized, tc.s)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDirectIngestWindowsProfiles(t *testing.T) {
|
|
ctx := context.Background()
|
|
logger := log.NewNopLogger()
|
|
ds := new(mock.Store)
|
|
|
|
for _, tc := range []struct {
|
|
hostProfiles []*fleet.ExpectedMDMProfile
|
|
want string
|
|
}{
|
|
{nil, ""},
|
|
{
|
|
[]*fleet.ExpectedMDMProfile{
|
|
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{})},
|
|
},
|
|
"",
|
|
},
|
|
{
|
|
[]*fleet.ExpectedMDMProfile{
|
|
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
|
|
},
|
|
"SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '<SyncBody><Get><CmdID>1255198959</CmdID><Item><Target><LocURI>L1</LocURI></Target></Item></Get></SyncBody>';",
|
|
},
|
|
{
|
|
[]*fleet.ExpectedMDMProfile{
|
|
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
|
|
{Name: "N2", RawProfile: syncml.ForTestWithData(map[string]string{"L2": "D2"})},
|
|
{Name: "N3", RawProfile: syncml.ForTestWithData(map[string]string{"L3": "D3", "L3.1": "D3.1"})},
|
|
},
|
|
"SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '<SyncBody><Get><CmdID>1255198959</CmdID><Item><Target><LocURI>L1</LocURI></Target></Item></Get><Get><CmdID>2736786183</CmdID><Item><Target><LocURI>L2</LocURI></Target></Item></Get><Get><CmdID>894211447</CmdID><Item><Target><LocURI>L3</LocURI></Target></Item></Get><Get><CmdID>3410477854</CmdID><Item><Target><LocURI>L3.1</LocURI></Target></Item></Get></SyncBody>';",
|
|
},
|
|
} {
|
|
|
|
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
|
|
result := map[string]*fleet.ExpectedMDMProfile{}
|
|
for _, p := range tc.hostProfiles {
|
|
result[p.Name] = p
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
gotQuery := buildConfigProfilesWindowsQuery(ctx, logger, &fleet.Host{}, ds)
|
|
if tc.want != "" {
|
|
require.Contains(t, gotQuery, "SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input =")
|
|
re := regexp.MustCompile(`'<(.*?)>'`)
|
|
gotMatches := re.FindStringSubmatch(gotQuery)
|
|
require.NotEmpty(t, gotMatches)
|
|
wantMatches := re.FindStringSubmatch(tc.want)
|
|
require.NotEmpty(t, wantMatches)
|
|
|
|
var extractedStruct, expectedStruct fleet.SyncBody
|
|
err := xml.Unmarshal([]byte(gotMatches[0]), &extractedStruct)
|
|
require.NoError(t, err)
|
|
|
|
err = xml.Unmarshal([]byte(wantMatches[0]), &expectedStruct)
|
|
require.NoError(t, err)
|
|
|
|
require.ElementsMatch(t, expectedStruct.Get, extractedStruct.Get)
|
|
} else {
|
|
require.Equal(t, gotQuery, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShouldRemoveSoftware(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
want bool
|
|
s *fleet.Software
|
|
h *fleet.Host
|
|
}{
|
|
{
|
|
name: "parallels windows software on MacOS host",
|
|
want: true,
|
|
h: &fleet.Host{Platform: "darwin"},
|
|
s: &fleet.Software{BundleIdentifier: "com.parallels.winapp.notepad", Name: "Notepad.app"},
|
|
},
|
|
{
|
|
name: "regular macos software",
|
|
want: false,
|
|
h: &fleet.Host{Platform: "darwin"},
|
|
s: &fleet.Software{BundleIdentifier: "com.apple.dock", Name: "Dock.app"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.want, shouldRemoveSoftware(tt.h, tt.s))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIngestNetworkInterface(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// NOTE: It was decided that we should allow ingesting private IPs on the PublicIP field,
|
|
// see https://github.com/fleetdm/fleet/issues/11102.
|
|
for _, tc := range []struct {
|
|
name string
|
|
ip string
|
|
valid bool
|
|
}{
|
|
{"public IPv6", "598b:6910:e935:63ff:54db:1753:9c01:4c84", true},
|
|
{"private IPv6", "fd42:fdaa:1234:5678::1a2b", true},
|
|
{"public IPv4", "190.18.97.12", true},
|
|
{"private IPv4", "127.0.0.1", true},
|
|
{"IP could not be determined", "", true},
|
|
{"invalid value ends up in the context", "invalid-ip", false},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := fleet.Host{PublicIP: "190.18.97.3"} // set to some old value that should always be overriden
|
|
err := ingestNetworkInterface(publicip.NewContext(context.Background(), tc.ip), log.NewNopLogger(), &h, nil)
|
|
require.NoError(t, err)
|
|
if tc.valid {
|
|
require.Equal(t, tc.ip, h.PublicIP)
|
|
} else {
|
|
require.Empty(t, h.PublicIP)
|
|
}
|
|
})
|
|
}
|
|
}
|