mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
3544 lines
120 KiB
Go
3544 lines
120 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/bzip2"
|
|
cryptorand "crypto/rand"
|
|
"crypto/sha1" // nolint:gosec
|
|
"crypto/tls"
|
|
"embed"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"maps"
|
|
"math/rand"
|
|
"net/http"
|
|
_ "net/http/pprof"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/hostidentity"
|
|
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/installer_cache"
|
|
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/osquery_perf"
|
|
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/softwaredb"
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
|
"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/mdm/nanomdm/mdm"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/service"
|
|
"github.com/fleetdm/fleet/v4/server/service/contract"
|
|
"github.com/google/uuid"
|
|
"github.com/micromdm/plist"
|
|
"github.com/remitly-oss/httpsig-go"
|
|
)
|
|
|
|
var (
|
|
//go:embed *.tmpl
|
|
templatesFS embed.FS
|
|
|
|
//go:embed macos_vulnerable-software.json.bz2
|
|
macOSVulnerableSoftwareFS embed.FS
|
|
|
|
//go:embed vscode_extensions_vulnerable.software
|
|
vsCodeExtensionsVulnerableSoftwareFS embed.FS
|
|
|
|
//go:embed ubuntu_2204-software.json.bz2
|
|
ubuntuSoftwareFS embed.FS
|
|
//go:embed ubuntu_2204-kernels.json
|
|
ubuntuKernelsFS embed.FS
|
|
//go:embed windows_11-software.json.bz2
|
|
windowsSoftwareFS embed.FS
|
|
|
|
macosVulnerableSoftware []fleet.Software
|
|
vsCodeExtensionsVulnerableSoftware []fleet.Software
|
|
windowsSoftware []map[string]string
|
|
ubuntuSoftware []map[string]string
|
|
ubuntuKernels []string
|
|
|
|
// Software library database (loaded from SQLite if --software_db_path is specified)
|
|
softwareDB *softwaredb.DB
|
|
|
|
installerMetadataCache installer_cache.Metadata
|
|
|
|
linuxRandomBuildNumber = randomString(8)
|
|
)
|
|
|
|
func loadMacOSVulnerableSoftware() {
|
|
bz2, err := macOSVulnerableSoftwareFS.Open("macos_vulnerable-software.json.bz2")
|
|
if err != nil {
|
|
log.Fatal("open vulnerable macOS software file: ", err)
|
|
}
|
|
|
|
type vulnerableSoftware struct {
|
|
Software []fleet.Software `json:"software"`
|
|
}
|
|
|
|
var vs vulnerableSoftware
|
|
if err := json.NewDecoder(bzip2.NewReader(bz2)).Decode(&vs); err != nil { //nolint:gosec
|
|
log.Fatal("unmarshaling vulnerable macOS software: ", err)
|
|
}
|
|
|
|
macosVulnerableSoftware = vs.Software
|
|
|
|
log.Printf("Loaded %d vulnerable macOS software", len(macosVulnerableSoftware))
|
|
}
|
|
|
|
func loadExtraVulnerableSoftware() {
|
|
vsCodeExtensionsVulnerableSoftwareData, err := vsCodeExtensionsVulnerableSoftwareFS.ReadFile("vscode_extensions_vulnerable.software")
|
|
if err != nil {
|
|
log.Fatal("reading vulnerable vscode_extensions software file: ", err)
|
|
}
|
|
lines := bytes.Split(vsCodeExtensionsVulnerableSoftwareData, []byte("\n"))
|
|
for _, line := range lines {
|
|
parts := bytes.Split(line, []byte("##"))
|
|
if len(parts) < 3 {
|
|
log.Println("skipping", string(line))
|
|
continue
|
|
}
|
|
vsCodeExtensionsVulnerableSoftware = append(vsCodeExtensionsVulnerableSoftware, fleet.Software{
|
|
Vendor: strings.TrimSpace(string(parts[0])),
|
|
Name: strings.TrimSpace(string(parts[1])),
|
|
Version: strings.TrimSpace(string(parts[2])),
|
|
Source: "vscode_extensions",
|
|
})
|
|
}
|
|
log.Printf("Loaded %d vulnerable vscode_extensions software", len(vsCodeExtensionsVulnerableSoftware))
|
|
}
|
|
|
|
func loadSoftwareItems(fs embed.FS, path string, source string) []map[string]string {
|
|
bz2, err := fs.Open(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
type softwareJSON struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
UpgradeCode string `json:"upgrade_code"`
|
|
Release string `json:"release,omitempty"`
|
|
Arch string `json:"arch,omitempty"`
|
|
}
|
|
var softwareList []softwareJSON
|
|
// ignoring "G110: Potential DoS vulnerability via decompression bomb", as this is test code.
|
|
if err := json.NewDecoder(bzip2.NewReader(bz2)).Decode(&softwareList); err != nil { //nolint:gosec
|
|
panic(err)
|
|
}
|
|
|
|
softwareRows := make([]map[string]string, 0, len(softwareList))
|
|
for _, s := range softwareList {
|
|
softwareRows = append(softwareRows, map[string]string{
|
|
"name": s.Name,
|
|
"version": s.Version,
|
|
"source": source,
|
|
"upgrade_code": s.UpgradeCode,
|
|
})
|
|
}
|
|
return softwareRows
|
|
}
|
|
|
|
func loadKernelList(fs embed.FS, path string) []string {
|
|
data, err := fs.ReadFile(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var kernels []string
|
|
if err := json.Unmarshal(data, &kernels); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return kernels
|
|
}
|
|
|
|
func init() {
|
|
loadMacOSVulnerableSoftware()
|
|
loadExtraVulnerableSoftware()
|
|
windowsSoftware = loadSoftwareItems(windowsSoftwareFS, "windows_11-software.json.bz2", "programs")
|
|
ubuntuSoftware = loadSoftwareItems(ubuntuSoftwareFS, "ubuntu_2204-software.json.bz2", "deb_packages")
|
|
ubuntuKernels = loadKernelList(ubuntuKernelsFS, "ubuntu_2204-kernels.json")
|
|
}
|
|
|
|
type nodeKeyManager struct {
|
|
filepath string
|
|
|
|
l sync.Mutex
|
|
nodekeys []string
|
|
}
|
|
|
|
func (n *nodeKeyManager) LoadKeys() {
|
|
if n.filepath == "" {
|
|
return
|
|
}
|
|
|
|
n.l.Lock()
|
|
defer n.l.Unlock()
|
|
|
|
data, err := os.ReadFile(n.filepath)
|
|
if err != nil {
|
|
log.Println("WARNING (ignore if creating a new node key file): error loading nodekey file:", err)
|
|
return
|
|
}
|
|
n.nodekeys = strings.Split(string(data), "\n")
|
|
n.nodekeys = n.nodekeys[:len(n.nodekeys)-1] // remove last empty node key due to new line.
|
|
log.Printf("loaded %d node keys", len(n.nodekeys))
|
|
}
|
|
|
|
func (n *nodeKeyManager) Get(i int) string {
|
|
n.l.Lock()
|
|
defer n.l.Unlock()
|
|
|
|
if len(n.nodekeys) > i {
|
|
return n.nodekeys[i]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (n *nodeKeyManager) Add(nodekey string) {
|
|
if n.filepath == "" {
|
|
return
|
|
}
|
|
|
|
// we lock just to make sure we write one at a time
|
|
n.l.Lock()
|
|
defer n.l.Unlock()
|
|
|
|
f, err := os.OpenFile(n.filepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
log.Printf("error opening nodekey file: %s", err.Error())
|
|
return
|
|
}
|
|
defer f.Close()
|
|
if _, err := f.WriteString(nodekey + "\n"); err != nil {
|
|
log.Printf("error writing nodekey file: %s", err)
|
|
}
|
|
}
|
|
|
|
type mdmAgent struct {
|
|
agentIndex int
|
|
MDMCheckInInterval time.Duration
|
|
model string
|
|
serverAddress string
|
|
softwareCount softwareEntityCount
|
|
stats *osquery_perf.Stats
|
|
strings map[string]string
|
|
softwareVersionMap map[rune]int // Maps first char to version option: 0=base, 1=alternate, 2-31=patch versions 0-29
|
|
mdmProfileFailureProb float64
|
|
}
|
|
|
|
// stats, model, *serverURL, *mdmSCEPChallenge, *mdmCheckInInterval
|
|
|
|
func (a *mdmAgent) CachedString(key string) string {
|
|
if val, ok := a.strings[key]; ok {
|
|
return val
|
|
}
|
|
val := randomString(12)
|
|
a.strings[key] = val
|
|
return val
|
|
}
|
|
|
|
// selectSoftwareVersion returns a consistent version for a software package based on its name.
|
|
// Same implementation as agent.selectSoftwareVersion.
|
|
func (a *mdmAgent) selectSoftwareVersion(softwareName, baseVersion, alternateVersion string) string {
|
|
if len(softwareName) == 0 {
|
|
return baseVersion
|
|
}
|
|
|
|
firstChar := rune(softwareName[0])
|
|
versionOption, exists := a.softwareVersionMap[firstChar]
|
|
if !exists {
|
|
r := rand.Float64()
|
|
switch {
|
|
case r < 0.99:
|
|
versionOption = 0
|
|
case r < 0.999:
|
|
versionOption = 1
|
|
default:
|
|
versionOption = 2 + rand.Intn(30)
|
|
}
|
|
a.softwareVersionMap[firstChar] = versionOption
|
|
}
|
|
|
|
switch versionOption {
|
|
case 0:
|
|
return baseVersion
|
|
case 1:
|
|
return alternateVersion
|
|
default:
|
|
patchNum := versionOption - 2
|
|
return fmt.Sprintf("%s.%d", baseVersion, patchNum)
|
|
}
|
|
}
|
|
|
|
// adamIDsToSoftware is the set of VPP apps that we support in our mock VPP install flow.
|
|
var adamIDsToSoftware = map[int]*fleet.Software{
|
|
406056744: {
|
|
Name: "Evernote",
|
|
BundleIdentifier: "com.evernote.Evernote",
|
|
Version: "10.147.1",
|
|
Installed: false,
|
|
},
|
|
1091189122: {
|
|
Name: "Bear: Markdown Notes",
|
|
BundleIdentifier: "net.shinyfrog.bear",
|
|
Version: "2.4.5",
|
|
Installed: false,
|
|
},
|
|
1487937127: {
|
|
Name: "Craft: Write docs, AI editing",
|
|
BundleIdentifier: "com.lukilabs.lukiapp",
|
|
Version: "3.1.7",
|
|
Installed: false,
|
|
},
|
|
1444383602: {
|
|
Name: "Goodnotes 6: AI Notes & Docs",
|
|
BundleIdentifier: "com.goodnotesapp.x",
|
|
Version: "6.7.2",
|
|
Installed: false,
|
|
},
|
|
}
|
|
|
|
type agent struct {
|
|
agentIndex int
|
|
hostCount int
|
|
totalHostCount int
|
|
hostIndexOffset int
|
|
softwareCount softwareEntityCount
|
|
softwareVSCodeExtensionsCount softwareExtraEntityCount
|
|
userCount entityCount
|
|
policyPassProb float64
|
|
munkiIssueProb float64
|
|
munkiIssueCount int
|
|
liveQueryFailProb float64
|
|
liveQueryNoResultsProb float64
|
|
strings map[string]string
|
|
serverAddress string
|
|
stats *osquery_perf.Stats
|
|
nodeKeyManager *nodeKeyManager
|
|
nodeKey string
|
|
templates *template.Template
|
|
os string
|
|
// deviceAuthToken holds Fleet Desktop device authentication token.
|
|
//
|
|
// Non-nil means the agent is identified as orbit osquery,
|
|
// nil means the agent is identified as vanilla osquery.
|
|
deviceAuthToken *string
|
|
orbitNodeKey *string
|
|
|
|
// macMDMClient and winMDMClient simulate a device running the MDM protocol
|
|
// (client side) against Fleet MDM.
|
|
macMDMClient *mdmtest.TestAppleMDMClient
|
|
winMDMClient *mdmtest.TestWindowsMDMClient
|
|
|
|
// isEnrolledToMDM is true when the mdmDevice has enrolled.
|
|
isEnrolledToMDM bool
|
|
// isEnrolledToMDMMu protects isEnrolledToMDM.
|
|
isEnrolledToMDMMu sync.Mutex
|
|
|
|
disableScriptExec bool
|
|
disableFleetDesktop bool
|
|
loggerTLSMaxLines int
|
|
|
|
// atomic boolean is set to true when executing scripts, so that only a
|
|
// single goroutine at a time can execute scripts.
|
|
scriptExecRunning atomic.Bool
|
|
|
|
softwareQueryFailureProb float64
|
|
softwareVSCodeExtensionsFailProb float64
|
|
|
|
softwareInstaller softwareInstaller
|
|
|
|
linuxUniqueSoftwareVersion bool
|
|
linuxUniqueSoftwareTitle bool
|
|
|
|
// Software installed on the host via Fleet. Key is the software name + version + bundle identifier.
|
|
installedSoftware sync.Map
|
|
|
|
// Cached software indices (pointers into global softwareDB array for this agent's platform)
|
|
cachedSoftwareIndices []uint32
|
|
|
|
// Host identity client for HTTP message signatures
|
|
hostIdentityClient *hostidentity.Client
|
|
|
|
//
|
|
// The following are exported to be used by the templates.
|
|
//
|
|
|
|
EnrollSecret string
|
|
UUID string
|
|
SerialNumber string
|
|
defaultSerialProb float64
|
|
ConfigInterval time.Duration
|
|
LogInterval time.Duration
|
|
QueryInterval time.Duration
|
|
MDMCheckInInterval time.Duration
|
|
DiskEncryptionEnabled bool
|
|
mdmProfileFailureProb float64
|
|
OSPatchLevel int // For Linux patches
|
|
linuxKernels []map[string]string // Pre-selected kernels for this agent
|
|
softwareVersionMap map[rune]int // Maps first char to version option: 0=base, 1=alternate, 2-31=patch versions 0-29
|
|
|
|
// Note that a sync.Map is safe for concurrent use, but we still need a mutex
|
|
// because we read and write the field itself (not data in the map) from
|
|
// different goroutines (the write is in a.config).
|
|
scheduledQueryMapMutex sync.RWMutex
|
|
scheduledQueryData *sync.Map
|
|
// bufferedResults contains result logs that are buffered when
|
|
// /api/v1/osquery/log requests to the Fleet server fail.
|
|
//
|
|
// NOTE: We use a map instead of a slice to prevent the data structure to
|
|
// increase indefinitely (we sacrifice accuracy of logs but that's
|
|
// a-ok for osquery-perf and load testing).
|
|
bufferedResults map[resultLog]int
|
|
|
|
// cache of certificates returned by this agent. Note that this requires
|
|
// a mutex even though only used in a.processQuery, that's because both
|
|
// the runLoop and the live query goroutines may call DistributedWrite
|
|
// (which calls processQuery).
|
|
certificatesMutex sync.RWMutex
|
|
certificatesCache []map[string]string
|
|
commonSoftwareNameSuffix string
|
|
|
|
entraIDDeviceID string
|
|
entraIDUserPrincipalName string
|
|
installedAdamIDs []int
|
|
}
|
|
|
|
func (a *agent) GetSerialNumber() string {
|
|
if rand.Float64() <= a.defaultSerialProb {
|
|
return "-1"
|
|
}
|
|
return a.SerialNumber
|
|
}
|
|
|
|
type entityCount struct {
|
|
common int
|
|
unique int
|
|
}
|
|
|
|
type softwareEntityCount struct {
|
|
entityCount
|
|
vulnerable int
|
|
withLastOpened int
|
|
lastOpenedProb float64
|
|
commonSoftwareUninstallCount int
|
|
commonSoftwareUninstallProb float64
|
|
uniqueSoftwareUninstallCount int
|
|
uniqueSoftwareUninstallProb float64
|
|
duplicateBundleIdentifiersPercent int
|
|
softwareRenaming bool
|
|
}
|
|
type softwareExtraEntityCount struct {
|
|
entityCount
|
|
commonSoftwareUninstallCount int
|
|
commonSoftwareUninstallProb float64
|
|
uniqueSoftwareUninstallCount int
|
|
uniqueSoftwareUninstallProb float64
|
|
}
|
|
type softwareInstaller struct {
|
|
preInstallFailureProb float64
|
|
installFailureProb float64
|
|
postInstallFailureProb float64
|
|
mu *sync.Mutex
|
|
}
|
|
|
|
func newAgent(
|
|
agentIndex int,
|
|
hostCount int,
|
|
totalHostCount int,
|
|
hostIndexOffset int,
|
|
serverAddress, enrollSecret string,
|
|
templates *template.Template,
|
|
configInterval, logInterval, queryInterval, mdmCheckInInterval time.Duration,
|
|
softwareQueryFailureProb float64,
|
|
softwareVSCodeExtensionsQueryFailureProb float64,
|
|
softwareInstaller softwareInstaller,
|
|
softwareCount softwareEntityCount,
|
|
softwareVSCodeExtensionsCount softwareExtraEntityCount,
|
|
userCount entityCount,
|
|
policyPassProb float64,
|
|
orbitProb float64,
|
|
munkiIssueProb float64, munkiIssueCount int,
|
|
emptySerialProb float64,
|
|
defaultSerialProb float64,
|
|
mdmProb float64,
|
|
mdmSCEPChallenge string,
|
|
liveQueryFailProb float64,
|
|
liveQueryNoResultsProb float64,
|
|
disableScriptExec bool,
|
|
disableFleetDesktop bool,
|
|
loggerTLSMaxLines int,
|
|
linuxUniqueSoftwareVersion bool,
|
|
linuxUniqueSoftwareTitle bool,
|
|
commonSoftwareNameSuffix string,
|
|
mdmProfileFailureProb float64,
|
|
httpMessageSignatureProb float64,
|
|
httpMessageSignatureP384Prob float64,
|
|
) *agent {
|
|
var deviceAuthToken *string
|
|
if rand.Float64() <= orbitProb {
|
|
deviceAuthToken = ptr.String(uuid.NewString())
|
|
}
|
|
serialNumber := mdmtest.RandSerialNumber()
|
|
if rand.Float64() <= emptySerialProb {
|
|
serialNumber = ""
|
|
}
|
|
hostUUID := strings.ToUpper(uuid.New().String())
|
|
|
|
// determine the simulated host's OS based on the template name (see
|
|
// validTemplateNames below for the list of possible names, the OS is always
|
|
// the part before the underscore). Note that it is the OS and not the
|
|
// "normalized" platform, so "ubuntu" and not "linux", "macos" and not
|
|
// "darwin".
|
|
agentOS := strings.TrimRight(templates.Name(), ".tmpl")
|
|
agentOS, _, _ = strings.Cut(agentOS, "_")
|
|
|
|
var (
|
|
macMDMClient *mdmtest.TestAppleMDMClient
|
|
winMDMClient *mdmtest.TestWindowsMDMClient
|
|
)
|
|
if rand.Float64() < mdmProb {
|
|
switch agentOS {
|
|
case "macos":
|
|
macMDMClient = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
|
|
SCEPChallenge: mdmSCEPChallenge,
|
|
SCEPURL: serverAddress + apple_mdm.SCEPPath,
|
|
MDMURL: serverAddress + apple_mdm.MDMPath,
|
|
}, "MacBookPro16,1")
|
|
// Have the osquery agent match the MDM device serial number and UUID.
|
|
serialNumber = macMDMClient.SerialNumber
|
|
hostUUID = macMDMClient.UUID
|
|
|
|
case "windows":
|
|
// windows MDM enrollment requires orbit enrollment
|
|
if deviceAuthToken == nil {
|
|
deviceAuthToken = ptr.String(uuid.NewString())
|
|
}
|
|
// creating the Windows MDM client requires the orbit node key, but we
|
|
// only get it after orbit enrollment. So here we just set the value to a
|
|
// placeholder (non-nil) client, the actual usable client will be created
|
|
// after orbit enrollment, and after receiving the enrollment
|
|
// notification.
|
|
winMDMClient = new(mdmtest.TestWindowsMDMClient)
|
|
}
|
|
}
|
|
|
|
// Determine if this agent should use HTTP message signatures
|
|
useHTTPSig := rand.Float64() < httpMessageSignatureProb // nolint:gosec // ignore weak randomizer
|
|
|
|
agent := &agent{
|
|
agentIndex: agentIndex,
|
|
hostCount: hostCount,
|
|
totalHostCount: totalHostCount,
|
|
hostIndexOffset: hostIndexOffset,
|
|
serverAddress: serverAddress,
|
|
softwareCount: softwareCount,
|
|
softwareVSCodeExtensionsCount: softwareVSCodeExtensionsCount,
|
|
userCount: userCount,
|
|
strings: make(map[string]string),
|
|
policyPassProb: policyPassProb,
|
|
munkiIssueProb: munkiIssueProb,
|
|
munkiIssueCount: munkiIssueCount,
|
|
liveQueryFailProb: liveQueryFailProb,
|
|
liveQueryNoResultsProb: liveQueryNoResultsProb,
|
|
templates: templates,
|
|
deviceAuthToken: deviceAuthToken,
|
|
os: agentOS,
|
|
EnrollSecret: enrollSecret,
|
|
ConfigInterval: configInterval,
|
|
LogInterval: logInterval,
|
|
QueryInterval: queryInterval,
|
|
MDMCheckInInterval: mdmCheckInInterval,
|
|
UUID: hostUUID,
|
|
SerialNumber: serialNumber,
|
|
defaultSerialProb: defaultSerialProb,
|
|
OSPatchLevel: rand.Intn(25), // Random patch level 0-24
|
|
|
|
softwareQueryFailureProb: softwareQueryFailureProb,
|
|
softwareVSCodeExtensionsFailProb: softwareVSCodeExtensionsQueryFailureProb,
|
|
softwareInstaller: softwareInstaller,
|
|
|
|
linuxUniqueSoftwareVersion: linuxUniqueSoftwareVersion,
|
|
linuxUniqueSoftwareTitle: linuxUniqueSoftwareTitle,
|
|
|
|
macMDMClient: macMDMClient,
|
|
winMDMClient: winMDMClient,
|
|
|
|
disableScriptExec: disableScriptExec,
|
|
disableFleetDesktop: disableFleetDesktop,
|
|
loggerTLSMaxLines: loggerTLSMaxLines,
|
|
bufferedResults: make(map[resultLog]int),
|
|
scheduledQueryData: new(sync.Map),
|
|
softwareVersionMap: make(map[rune]int),
|
|
commonSoftwareNameSuffix: commonSoftwareNameSuffix,
|
|
mdmProfileFailureProb: mdmProfileFailureProb,
|
|
|
|
entraIDDeviceID: uuid.NewString(),
|
|
entraIDUserPrincipalName: fmt.Sprintf("fake-%s@example.com", randomString(5)),
|
|
}
|
|
|
|
// Initialize host identity client
|
|
agent.hostIdentityClient = hostidentity.NewClient(hostidentity.Config{
|
|
ServerAddress: serverAddress,
|
|
EnrollSecret: enrollSecret,
|
|
HostUUID: hostUUID,
|
|
AgentIndex: agentIndex,
|
|
}, useHTTPSig, httpMessageSignatureP384Prob)
|
|
|
|
// Pre-select kernels for Ubuntu agents to ensure consistency across queries
|
|
if agentOS == "ubuntu" {
|
|
agent.linuxKernels = selectKernels(ubuntuKernels)
|
|
}
|
|
|
|
return agent
|
|
}
|
|
|
|
type enrollResponse struct {
|
|
NodeKey string `json:"node_key"`
|
|
}
|
|
|
|
type distributedReadResponse struct {
|
|
Queries map[string]string `json:"queries"`
|
|
}
|
|
|
|
type scheduledQuery struct {
|
|
Query string `json:"query"`
|
|
Name string `json:"name"`
|
|
ScheduleInterval float64 `json:"interval"`
|
|
Platform string `json:"platform"`
|
|
Version string `json:"version"`
|
|
Snapshot bool `json:"snapshot"`
|
|
|
|
lastRun int64
|
|
numRows uint
|
|
packName string
|
|
}
|
|
|
|
func (a *agent) isOrbit() bool {
|
|
return a.deviceAuthToken != nil
|
|
}
|
|
|
|
func (a *agent) runLoop(i int, onlyAlreadyEnrolled bool) {
|
|
// Request host identity certificate if this agent uses HTTP message signatures
|
|
if a.hostIdentityClient.IsEnabled() && !onlyAlreadyEnrolled {
|
|
if err := a.hostIdentityClient.RequestCertificate(); err != nil {
|
|
log.Printf("Agent %d: Failed to request host identity certificate: %v", a.agentIndex, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if a.isOrbit() {
|
|
if err := a.orbitEnroll(); err != nil {
|
|
// clean-up any placeholder mdm client that depended on orbit enrollment
|
|
// - there's no concurrency yet for a given agent instance, runLoop is
|
|
// the place where the goroutines will be started later on.
|
|
a.winMDMClient = nil
|
|
return
|
|
}
|
|
if a.winMDMClient != nil {
|
|
a.winMDMClient = mdmtest.NewTestMDMClientWindowsProgramatic(a.serverAddress, *a.orbitNodeKey)
|
|
}
|
|
}
|
|
|
|
if err := a.enroll(i, onlyAlreadyEnrolled); err != nil {
|
|
return
|
|
}
|
|
|
|
_ = a.config()
|
|
|
|
resp, err := a.DistributedRead()
|
|
if err == nil {
|
|
if len(resp.Queries) > 0 {
|
|
_ = a.DistributedWrite(resp.Queries)
|
|
}
|
|
}
|
|
|
|
if a.isOrbit() {
|
|
go a.runOrbitLoop()
|
|
}
|
|
|
|
// NOTE: the windows MDM client enrollment is only done after receiving a
|
|
// notification via the config in the runOrbitLoop.
|
|
if a.macMDMClient != nil {
|
|
if err := a.macMDMClient.Enroll(); err != nil {
|
|
log.Printf("macOS MDM enroll failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
return
|
|
}
|
|
a.setMDMEnrolled()
|
|
a.stats.IncrementMDMEnrollments()
|
|
go a.runMacosMDMLoop()
|
|
}
|
|
|
|
//
|
|
// osquery runs three separate independent threads,
|
|
// - a thread for getting, running and submitting results for distributed queries (distributed).
|
|
// - a thread for getting configuration from a remote server (config).
|
|
// - a thread for submitting log results (logger).
|
|
//
|
|
// Thus we try to simulate that as much as we can.
|
|
|
|
// (1) distributed thread:
|
|
go func() {
|
|
liveQueryTicker := time.NewTicker(a.QueryInterval)
|
|
defer liveQueryTicker.Stop()
|
|
|
|
for range liveQueryTicker.C {
|
|
if resp, err := a.DistributedRead(); err == nil && len(resp.Queries) > 0 {
|
|
_ = a.DistributedWrite(resp.Queries)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// (2) config thread:
|
|
go func() {
|
|
configTicker := time.NewTicker(a.ConfigInterval)
|
|
defer configTicker.Stop()
|
|
|
|
for range configTicker.C {
|
|
_ = a.config()
|
|
}
|
|
}()
|
|
|
|
// (3) logger thread:
|
|
logTicker := time.NewTicker(a.LogInterval)
|
|
defer logTicker.Stop()
|
|
for range logTicker.C {
|
|
// check if we have any scheduled queries that should be returning results
|
|
var results []resultLog
|
|
now := time.Now().Unix()
|
|
prevCount := a.countBuffered()
|
|
|
|
// NOTE The goroutine that pulls in new configurations
|
|
// MAY replace this map if it happens to run at the
|
|
// exact same time. The result would be. The result
|
|
// would be that the query lastRun does not get
|
|
// updated and cause the query to run more times than
|
|
// expected.
|
|
a.scheduledQueryMapMutex.RLock()
|
|
queryData := a.scheduledQueryData
|
|
a.scheduledQueryMapMutex.RUnlock()
|
|
queryData.Range(func(key, value any) bool {
|
|
queryName := key.(string)
|
|
query := value.(scheduledQuery)
|
|
|
|
if query.lastRun == 0 || now >= (query.lastRun+int64(query.ScheduleInterval)) {
|
|
results = append(results, resultLog{
|
|
packName: query.packName,
|
|
queryName: query.Name,
|
|
numRows: int(query.numRows),
|
|
})
|
|
// Update lastRun
|
|
query.lastRun = now
|
|
queryData.Store(queryName, query)
|
|
}
|
|
|
|
return true
|
|
})
|
|
if prevCount+len(results) < 1_000_000 { // osquery buffered_log_max is 1M
|
|
a.addToBuffer(results)
|
|
}
|
|
a.sendLogsBatch()
|
|
newBufferedCount := a.countBuffered() - prevCount
|
|
a.stats.UpdateBufferedLogs(newBufferedCount)
|
|
}
|
|
}
|
|
|
|
func (a *agent) countBuffered() int {
|
|
var total int
|
|
for _, count := range a.bufferedResults {
|
|
total += count
|
|
}
|
|
return total
|
|
}
|
|
|
|
func (a *agent) addToBuffer(results []resultLog) {
|
|
for _, result := range results {
|
|
a.bufferedResults[result] += 1
|
|
}
|
|
}
|
|
|
|
// getBatch returns a random set of logs from the buffered logs.
|
|
// NOTE: We sacrifice some accuracy in the name of CPU and memory efficiency.
|
|
func (a *agent) getBatch(batchSize int) []resultLog {
|
|
results := make([]resultLog, 0, batchSize)
|
|
for result, count := range a.bufferedResults {
|
|
left := batchSize - len(results)
|
|
if left <= 0 {
|
|
return results
|
|
}
|
|
if count > left {
|
|
count = left
|
|
}
|
|
for i := 0; i < count; i++ {
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
type resultLog struct {
|
|
packName string
|
|
queryName string
|
|
numRows int
|
|
}
|
|
|
|
func (r resultLog) emit() []byte {
|
|
return scheduledQueryResults(r.packName, r.queryName, r.numRows)
|
|
}
|
|
|
|
// sendLogsBatch sends up to loggerTLSMaxLines logs and updates the buffer.
|
|
func (a *agent) sendLogsBatch() {
|
|
if len(a.bufferedResults) == 0 {
|
|
return
|
|
}
|
|
|
|
batchSize := a.loggerTLSMaxLines
|
|
if count := a.countBuffered(); count < batchSize {
|
|
batchSize = count
|
|
}
|
|
batch := a.getBatch(batchSize)
|
|
if err := a.submitLogs(batch); err != nil {
|
|
return
|
|
}
|
|
a.removeBuffered(batchSize)
|
|
}
|
|
|
|
// removeBuffered removes a random set of logs from the buffered logs.
|
|
// NOTE: We sacrifice some accuracy in the name of CPU and memory efficiency.
|
|
func (a *agent) removeBuffered(batchSize int) {
|
|
for b := batchSize; b > 0; {
|
|
for result, count := range a.bufferedResults {
|
|
if count > b {
|
|
a.bufferedResults[result] -= b
|
|
return
|
|
}
|
|
delete(a.bufferedResults, result)
|
|
b -= count
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *agent) runOrbitLoop() {
|
|
// Create signerWrapper if HTTP signatures are enabled
|
|
var signerWrapper func(*http.Client) *http.Client
|
|
if a.hostIdentityClient.IsEnabled() && a.hostIdentityClient.HasSigner() {
|
|
signer := a.hostIdentityClient.GetSigner()
|
|
signerWrapper = func(client *http.Client) *http.Client {
|
|
return httpsig.NewHTTPClient(client, signer, nil)
|
|
}
|
|
}
|
|
|
|
orbitClient, err := service.NewOrbitClient(
|
|
"",
|
|
a.serverAddress,
|
|
"",
|
|
true,
|
|
a.EnrollSecret,
|
|
nil,
|
|
fleet.OrbitHostInfo{
|
|
HardwareUUID: a.UUID,
|
|
HardwareSerial: a.SerialNumber,
|
|
Hostname: a.CachedString("hostname"),
|
|
},
|
|
nil,
|
|
signerWrapper,
|
|
"",
|
|
)
|
|
if err != nil {
|
|
log.Println("creating orbit client: ", err)
|
|
}
|
|
|
|
orbitClient.TestNodeKey = *a.orbitNodeKey
|
|
|
|
deviceClient, err := service.NewDeviceClient(a.serverAddress, true, "", nil, "")
|
|
if err != nil {
|
|
log.Fatal("creating device client: ", err)
|
|
}
|
|
|
|
// orbit does a config check when it starts
|
|
if _, err := orbitClient.GetConfig(); err != nil {
|
|
a.stats.IncrementOrbitErrors()
|
|
}
|
|
|
|
tokenRotationEnabled := false
|
|
if !a.disableFleetDesktop {
|
|
tokenRotationEnabled = orbitClient.GetServerCapabilities().Has(fleet.CapabilityOrbitEndpoints) &&
|
|
orbitClient.GetServerCapabilities().Has(fleet.CapabilityTokenRotation)
|
|
|
|
// it also writes and checks the device token
|
|
if tokenRotationEnabled {
|
|
if err := orbitClient.SetOrUpdateDeviceToken(*a.deviceAuthToken); err != nil {
|
|
a.stats.IncrementOrbitErrors()
|
|
log.Println("orbitClient.SetOrUpdateDeviceToken: ", err)
|
|
}
|
|
|
|
if err := deviceClient.CheckToken(*a.deviceAuthToken); err != nil {
|
|
a.stats.IncrementOrbitErrors()
|
|
log.Println("deviceClient.CheckToken: ", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkToken is used to simulate Fleet Desktop polling until a token is
|
|
// valid, we make a random number of requests to properly emulate what
|
|
// happens in the real world as there are delays that are not accounted by
|
|
// the way this simulation is arranged.
|
|
checkToken := func() {
|
|
minVal := 1
|
|
maxVal := 5
|
|
numberOfRequests := rand.Intn(maxVal-minVal+1) + minVal
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
<-ticker.C
|
|
numberOfRequests--
|
|
if err := deviceClient.CheckToken(*a.deviceAuthToken); err != nil {
|
|
log.Println("deviceClient.CheckToken: ", err)
|
|
}
|
|
if numberOfRequests == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fleet Desktop performs a burst of check token requests when it's initialized
|
|
if !a.disableFleetDesktop {
|
|
checkToken()
|
|
}
|
|
|
|
// orbit makes a call to check the config and update the CLI flags every 30
|
|
// seconds
|
|
orbitConfigTicker := time.Tick(30 * time.Second)
|
|
// orbit makes a call every 5 minutes to check the validity of the device
|
|
// token on the server
|
|
orbitTokenRemoteCheckTicker := time.Tick(5 * time.Minute)
|
|
// orbit pings the server every 1 hour to rotate the device token
|
|
orbitTokenRotationTicker := time.Tick(1 * time.Hour)
|
|
// orbit polls the /orbit/ping endpoint every 5 minutes to check if the
|
|
// server capabilities have changed
|
|
capabilitiesCheckerTicker := time.Tick(5 * time.Minute)
|
|
// fleet desktop polls for policy compliance every 5 minutes
|
|
fleetDesktopPolicyTicker := time.Tick(5 * time.Minute)
|
|
// fleet desktop pings every 10s for connectivity check.
|
|
fleetDesktopConnectivityCheck := time.Tick(10 * time.Second)
|
|
|
|
const windowsMDMEnrollmentAttemptFrequency = time.Hour
|
|
var lastEnrollAttempt time.Time
|
|
|
|
for {
|
|
select {
|
|
case <-orbitConfigTicker:
|
|
cfg, err := orbitClient.GetConfig()
|
|
if err != nil {
|
|
a.stats.IncrementOrbitErrors()
|
|
continue
|
|
}
|
|
if len(cfg.Notifications.PendingScriptExecutionIDs) > 0 {
|
|
// there are pending scripts to execute on this host, start a goroutine
|
|
// that will simulate executing them.
|
|
go a.execScripts(cfg.Notifications.PendingScriptExecutionIDs, orbitClient)
|
|
}
|
|
if len(cfg.Notifications.PendingSoftwareInstallerIDs) > 0 {
|
|
// there are pending software installations on this host, start a
|
|
// goroutine that will download the software
|
|
go a.installSoftware(cfg.Notifications.PendingSoftwareInstallerIDs, orbitClient)
|
|
}
|
|
if cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment &&
|
|
!a.mdmEnrolled() &&
|
|
a.winMDMClient != nil &&
|
|
time.Since(lastEnrollAttempt) > windowsMDMEnrollmentAttemptFrequency {
|
|
lastEnrollAttempt = time.Now()
|
|
if err := a.winMDMClient.Enroll(); err != nil {
|
|
log.Printf("Windows MDM enroll failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
} else {
|
|
a.setMDMEnrolled()
|
|
a.stats.IncrementMDMEnrollments()
|
|
go a.runWindowsMDMLoop()
|
|
}
|
|
}
|
|
case <-orbitTokenRemoteCheckTicker:
|
|
if !a.disableFleetDesktop && tokenRotationEnabled {
|
|
if err := deviceClient.CheckToken(*a.deviceAuthToken); err != nil {
|
|
a.stats.IncrementOrbitErrors()
|
|
log.Println("deviceClient.CheckToken: ", err)
|
|
continue
|
|
}
|
|
}
|
|
case <-orbitTokenRotationTicker:
|
|
if !a.disableFleetDesktop && tokenRotationEnabled {
|
|
newToken := ptr.String(uuid.NewString())
|
|
if err := orbitClient.SetOrUpdateDeviceToken(*newToken); err != nil {
|
|
a.stats.IncrementOrbitErrors()
|
|
log.Println("orbitClient.SetOrUpdateDeviceToken: ", err)
|
|
continue
|
|
}
|
|
a.deviceAuthToken = newToken
|
|
// fleet desktop performs a burst of check token requests after a token is rotated
|
|
checkToken()
|
|
}
|
|
case <-capabilitiesCheckerTicker:
|
|
if err := orbitClient.Ping(); err != nil {
|
|
a.stats.IncrementOrbitErrors()
|
|
continue
|
|
}
|
|
case <-fleetDesktopPolicyTicker:
|
|
if !a.disableFleetDesktop {
|
|
if _, err := deviceClient.DesktopSummary(*a.deviceAuthToken); err != nil {
|
|
a.stats.IncrementDesktopErrors()
|
|
log.Println("deviceClient.NumberOfFailingPolicies: ", err)
|
|
continue
|
|
}
|
|
}
|
|
case <-fleetDesktopConnectivityCheck:
|
|
if !a.disableFleetDesktop {
|
|
if err := deviceClient.Ping(); err != nil {
|
|
a.stats.IncrementDesktopErrors()
|
|
log.Println("deviceClient.Ping: ", err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *agent) runMacosMDMLoop() {
|
|
mdmCheckInTicker := time.Tick(a.MDMCheckInInterval)
|
|
|
|
for range mdmCheckInTicker {
|
|
mdmCommandPayload, err := a.macMDMClient.Idle()
|
|
if err != nil {
|
|
log.Printf("MDM Idle request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
continue
|
|
}
|
|
a.stats.IncrementMDMSessions()
|
|
|
|
INNER_FOR_LOOP:
|
|
for mdmCommandPayload != nil {
|
|
a.stats.IncrementMDMCommandsReceived()
|
|
|
|
switch mdmCommandPayload.Command.RequestType {
|
|
case "InstallProfile":
|
|
if a.mdmProfileFailureProb > 0.0 && rand.Float64() <= a.mdmProfileFailureProb {
|
|
errChain := []mdm.ErrorChain{
|
|
{
|
|
ErrorCode: 89,
|
|
ErrorDomain: "ErrorDomain",
|
|
LocalizedDescription: "The profile did not install",
|
|
},
|
|
}
|
|
mdmCommandPayload, err = a.macMDMClient.Err(mdmCommandPayload.CommandUUID, errChain)
|
|
if err != nil {
|
|
log.Printf("MDM Error request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
break INNER_FOR_LOOP
|
|
}
|
|
} else {
|
|
mdmCommandPayload, err = a.macMDMClient.Acknowledge(mdmCommandPayload.CommandUUID)
|
|
if err != nil {
|
|
log.Printf("MDM Acknowledge request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
break INNER_FOR_LOOP
|
|
}
|
|
}
|
|
case "DeclarativeManagement":
|
|
// Device immediately responds with Acknowledged status and then contacts the Declarations endpoints.
|
|
nextMdmCommandPayload, err := a.macMDMClient.Acknowledge(mdmCommandPayload.CommandUUID)
|
|
if err != nil {
|
|
log.Printf("MDM Acknowledge request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
break INNER_FOR_LOOP
|
|
}
|
|
// Note: Declarative management could happen async while other MDM commands proceed. This is a potential enhancement.
|
|
a.doDeclarativeManagement(mdmCommandPayload)
|
|
mdmCommandPayload = nextMdmCommandPayload
|
|
case "InstalledApplicationList":
|
|
var installedVPPSoftware []fleet.Software
|
|
// Our mock VPP apps start off as "not installed".
|
|
// The first time we get a verification command, we flip the flag to "installed",
|
|
// but don't include the software in the response.
|
|
// This ensures that 2 verification commands will be sent per VPP install.
|
|
for _, adamID := range a.installedAdamIDs {
|
|
if sw, ok := adamIDsToSoftware[adamID]; ok && sw != nil {
|
|
if sw.Installed {
|
|
installedVPPSoftware = append(installedVPPSoftware, *sw)
|
|
}
|
|
|
|
sw.Installed = true
|
|
}
|
|
}
|
|
nextMdmCommandPayload, err := a.macMDMClient.AcknowledgeInstalledApplicationList(
|
|
a.macMDMClient.UUID,
|
|
mdmCommandPayload.CommandUUID,
|
|
installedVPPSoftware,
|
|
)
|
|
if err != nil {
|
|
log.Printf("MDM Acknowledge InstalledApplicationList request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
break INNER_FOR_LOOP
|
|
}
|
|
|
|
mdmCommandPayload = nextMdmCommandPayload
|
|
case "InstallApplication":
|
|
var appRequest struct {
|
|
Command map[string]any `plist:"Command"`
|
|
}
|
|
|
|
err = plist.Unmarshal(mdmCommandPayload.Raw, &appRequest)
|
|
if err != nil {
|
|
log.Printf("parsing InstallApplication request: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
break INNER_FOR_LOOP
|
|
}
|
|
log.Printf("got install application command for %d", appRequest.Command["iTunesStoreID"])
|
|
|
|
if adamID, ok := appRequest.Command["iTunesStoreID"].(uint64); ok {
|
|
a.installedAdamIDs = append(a.installedAdamIDs, int(adamID))
|
|
}
|
|
|
|
mdmCommandPayload, err = a.macMDMClient.Acknowledge(mdmCommandPayload.CommandUUID)
|
|
if err != nil {
|
|
log.Printf("MDM Acknowledge request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
break INNER_FOR_LOOP
|
|
}
|
|
|
|
default:
|
|
mdmCommandPayload, err = a.macMDMClient.Acknowledge(mdmCommandPayload.CommandUUID)
|
|
if err != nil {
|
|
log.Printf("MDM Acknowledge request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
break INNER_FOR_LOOP
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *agent) doDeclarativeManagement(cmd *mdm.Command) {
|
|
// defer log.Printf("Exiting DeclarativeManagement for command %s", cmd.CommandUUID)
|
|
|
|
// get declaration-items endpoint
|
|
r, err := a.macMDMClient.DeclarativeManagement("declaration-items")
|
|
if err != nil {
|
|
log.Printf("DDM %s declaration-items request failed: %s", cmd.CommandUUID, err)
|
|
a.stats.IncrementDDMDeclarationItemsErrors()
|
|
return
|
|
}
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Printf("DDM %s declaration-items read body failed: %s", cmd.CommandUUID, err)
|
|
a.stats.IncrementDDMDeclarationItemsErrors()
|
|
return
|
|
}
|
|
var items fleet.MDMAppleDDMDeclarationItemsResponse
|
|
err = json.Unmarshal(body, &items)
|
|
if err != nil {
|
|
log.Printf("DDM %s declaration-items unmarshal failed: %s", cmd.CommandUUID, err)
|
|
a.stats.IncrementDDMDeclarationItemsErrors()
|
|
return
|
|
}
|
|
a.stats.IncrementDDMDeclarationItemsSuccess()
|
|
|
|
// get declaration/configuration/:identifer endpoint
|
|
for _, d := range items.Declarations.Configurations {
|
|
path := fmt.Sprintf("declaration/%s/%s", "configuration", d.Identifier)
|
|
r, err := a.macMDMClient.DeclarativeManagement(path)
|
|
if err != nil {
|
|
log.Printf("DDM %s request failed: %s", path, err)
|
|
a.stats.IncrementDDMConfigurationErrors()
|
|
return
|
|
}
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Printf("DDM %s read body failed: %s", path, err)
|
|
a.stats.IncrementDDMConfigurationErrors()
|
|
return
|
|
}
|
|
var decl fleet.MDMAppleDeclaration
|
|
err = json.Unmarshal(body, &decl)
|
|
if err != nil {
|
|
log.Printf("DDM %s unmarshal failed: %s", path, err)
|
|
a.stats.IncrementDDMConfigurationErrors()
|
|
return
|
|
}
|
|
}
|
|
a.stats.IncrementDDMConfigurationSuccess()
|
|
|
|
// get declaration/activation/:identifer endpoint
|
|
for _, d := range items.Declarations.Activations {
|
|
path := fmt.Sprintf("declaration/%s/%s", "activation", d.Identifier)
|
|
r, err := a.macMDMClient.DeclarativeManagement(path)
|
|
if err != nil {
|
|
log.Printf("DDM %s request failed: %s", path, err)
|
|
a.stats.IncrementDDMActivationErrors()
|
|
return
|
|
}
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Printf("DDM %s read body failed: %s", path, err)
|
|
a.stats.IncrementDDMActivationErrors()
|
|
return
|
|
}
|
|
var act fleet.MDMAppleDDMActivation
|
|
err = json.Unmarshal(body, &act)
|
|
if err != nil {
|
|
log.Printf("DDM %s unmarshal failed: %s", path, err)
|
|
a.stats.IncrementDDMActivationErrors()
|
|
return
|
|
}
|
|
}
|
|
a.stats.IncrementDDMActivationSuccess()
|
|
|
|
// sent status report
|
|
for _, d := range items.Declarations.Configurations {
|
|
report := fleet.MDMAppleDDMStatusReport{}
|
|
report.StatusItems.Management.Declarations.Configurations = []fleet.MDMAppleDDMStatusDeclaration{
|
|
{Active: true, Valid: fleet.MDMAppleDeclarationValid, Identifier: d.Identifier, ServerToken: d.ServerToken},
|
|
}
|
|
r, err := a.macMDMClient.DeclarativeManagement("status", report)
|
|
if err != nil {
|
|
log.Printf("DDM %s status request failed: %s", d.Identifier, err)
|
|
a.stats.IncrementDDMStatusErrors()
|
|
return
|
|
}
|
|
|
|
// Apple's documentation has some conflicting information about the expected status here so we'll
|
|
// just check for both.
|
|
//
|
|
// https://developer.apple.com/documentation/devicemanagement/get_the_device_status#response-codes
|
|
// https://developer.apple.com/documentation/devicemanagement/statusreport#discussion
|
|
if r.StatusCode != http.StatusOK && r.StatusCode != http.StatusNoContent {
|
|
log.Printf("DDM %s status response unexpected: %d", d.Identifier, r.StatusCode)
|
|
a.stats.IncrementDDMStatusErrors()
|
|
return
|
|
}
|
|
}
|
|
a.stats.IncrementDDMStatusSuccess()
|
|
}
|
|
|
|
func (a *agent) runWindowsMDMLoop() {
|
|
mdmCheckInTicker := time.Tick(a.MDMCheckInInterval)
|
|
|
|
for range mdmCheckInTicker {
|
|
cmds, err := a.winMDMClient.StartManagementSession()
|
|
if err != nil {
|
|
log.Printf("MDM check-in start session request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
continue
|
|
}
|
|
a.stats.IncrementMDMSessions()
|
|
|
|
// send a successful ack for each command
|
|
msgID, err := a.winMDMClient.GetCurrentMsgID()
|
|
if err != nil {
|
|
log.Printf("MDM get current MsgID failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
continue
|
|
}
|
|
|
|
for _, c := range cmds {
|
|
a.stats.IncrementMDMCommandsReceived()
|
|
|
|
status := syncml.CmdStatusOK
|
|
if a.mdmProfileFailureProb > 0.0 && rand.Float64() <= a.mdmProfileFailureProb {
|
|
status = syncml.CmdStatusBadRequest
|
|
}
|
|
a.winMDMClient.AppendResponse(fleet.SyncMLCmd{
|
|
XMLName: xml.Name{Local: fleet.CmdStatus},
|
|
MsgRef: &msgID,
|
|
CmdRef: &c.Cmd.CmdID.Value,
|
|
Cmd: ptr.String(c.Verb),
|
|
Data: &status,
|
|
Items: nil,
|
|
CmdID: fleet.CmdID{Value: uuid.NewString()},
|
|
})
|
|
}
|
|
if _, err := a.winMDMClient.SendResponse(); err != nil {
|
|
log.Printf("MDM send response request failed: %s", err)
|
|
a.stats.IncrementMDMErrors()
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *agent) execScripts(execIDs []string, orbitClient *service.OrbitClient) {
|
|
if a.scriptExecRunning.Swap(true) {
|
|
// if Swap returns true, the goroutine was already running, exit
|
|
return
|
|
}
|
|
defer a.scriptExecRunning.Store(false)
|
|
|
|
log.Printf("running scripts: %v", execIDs)
|
|
for _, execID := range execIDs {
|
|
if a.disableScriptExec {
|
|
// send a no-op result without executing if script exec is disabled
|
|
if err := orbitClient.SaveHostScriptResult(&fleet.HostScriptResultPayload{
|
|
ExecutionID: execID,
|
|
Output: "Scripts are disabled",
|
|
Runtime: 0,
|
|
ExitCode: -2,
|
|
}); err != nil {
|
|
log.Println("save disabled host script result:", err)
|
|
return
|
|
}
|
|
log.Printf("did save disabled host script result: id=%s", execID)
|
|
continue
|
|
}
|
|
|
|
a.stats.IncrementScriptExecs()
|
|
script, err := orbitClient.GetHostScript(execID)
|
|
if err != nil {
|
|
log.Println("get host script:", err)
|
|
a.stats.IncrementScriptExecErrs()
|
|
return
|
|
}
|
|
|
|
// simulate script execution
|
|
outputLen := rand.Intn(11000) // base64 encoding will make the actual output a bit bigger
|
|
buf := make([]byte, outputLen)
|
|
n, _ := io.ReadFull(cryptorand.Reader, buf)
|
|
exitCode := rand.Intn(2)
|
|
runtime := rand.Intn(5)
|
|
time.Sleep(time.Duration(runtime) * time.Second)
|
|
|
|
if err := orbitClient.SaveHostScriptResult(&fleet.HostScriptResultPayload{
|
|
HostID: script.HostID,
|
|
ExecutionID: script.ExecutionID,
|
|
Output: base64.StdEncoding.EncodeToString(buf[:n]),
|
|
Runtime: runtime,
|
|
ExitCode: exitCode,
|
|
}); err != nil {
|
|
log.Println("save host script result:", err)
|
|
a.stats.IncrementScriptExecErrs()
|
|
return
|
|
}
|
|
log.Printf("did exec and save host script result: id=%s, output size=%d, runtime=%d, exit code=%d", execID, base64.StdEncoding.EncodedLen(n), runtime, exitCode)
|
|
}
|
|
}
|
|
|
|
func (a *agent) installSoftware(installerIDs []string, orbitClient *service.OrbitClient) {
|
|
// Only allow one software install to happen at a time.
|
|
if a.softwareInstaller.mu.TryLock() {
|
|
defer a.softwareInstaller.mu.Unlock()
|
|
for _, installerID := range installerIDs {
|
|
a.installSoftwareItem(installerID, orbitClient)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *agent) installSoftwareItem(installerID string, orbitClient *service.OrbitClient) {
|
|
a.stats.IncrementSoftwareInstalls()
|
|
|
|
payload := &fleet.HostSoftwareInstallResultPayload{}
|
|
payload.InstallUUID = installerID
|
|
installer, err := orbitClient.GetInstallerDetails(installerID)
|
|
if err != nil {
|
|
log.Println("get installer details:", err)
|
|
a.stats.IncrementSoftwareInstallErrs()
|
|
return
|
|
}
|
|
failed := false
|
|
if installer.PreInstallCondition != "" {
|
|
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
|
if installer.PreInstallCondition == "select 1" { //nolint:gocritic // ignore ifElseChain
|
|
// Always pass
|
|
payload.PreInstallConditionOutput = ptr.String("1")
|
|
} else if installer.PreInstallCondition == "select 0" ||
|
|
a.softwareInstaller.preInstallFailureProb > 0.0 && rand.Float64() <= a.softwareInstaller.preInstallFailureProb {
|
|
// Fail
|
|
payload.PreInstallConditionOutput = ptr.String("")
|
|
failed = true
|
|
} else {
|
|
payload.PreInstallConditionOutput = ptr.String("1")
|
|
}
|
|
}
|
|
|
|
var meta *file.InstallerMetadata
|
|
if !failed {
|
|
var cacheMiss bool
|
|
// Download the file if needed to get its metadata
|
|
meta, cacheMiss, err = installerMetadataCache.Get(installer, orbitClient)
|
|
if err != nil {
|
|
a.stats.IncrementSoftwareInstallErrs()
|
|
return
|
|
}
|
|
|
|
if !cacheMiss && installer.SoftwareInstallerURL == nil {
|
|
// If we didn't download and analyze the file, AND we did not use a CDN URL to get the file,
|
|
// we do a download now and don't save the result. Doing this download adds realistic load on the server.
|
|
err = orbitClient.DownloadAndDiscardSoftwareInstaller(installer.InstallerID)
|
|
if err != nil {
|
|
log.Println("download and discard software installer:", err)
|
|
a.stats.IncrementSoftwareInstallErrs()
|
|
return
|
|
}
|
|
}
|
|
|
|
time.Sleep(time.Duration(rand.Intn(30)) * time.Second)
|
|
if installer.InstallScript == "exit 0" { //nolint:gocritic // ignore ifElseChain
|
|
// Always pass
|
|
payload.InstallScriptExitCode = ptr.Int(0)
|
|
payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (always pass)")
|
|
} else if installer.InstallScript == "exit 1" {
|
|
payload.InstallScriptExitCode = ptr.Int(1)
|
|
payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (always fail)")
|
|
failed = true
|
|
} else if a.softwareInstaller.installFailureProb > 0.0 && rand.Float64() <= a.softwareInstaller.installFailureProb {
|
|
payload.InstallScriptExitCode = ptr.Int(1)
|
|
payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (fail)")
|
|
failed = true
|
|
} else {
|
|
payload.InstallScriptExitCode = ptr.Int(0)
|
|
payload.InstallScriptOutput = ptr.String("Installed on osquery-perf (pass)")
|
|
}
|
|
}
|
|
if !failed {
|
|
if meta.Name == "" {
|
|
log.Printf("WARNING: installer metadata is missing a name for installer:%d\n", installer.InstallerID)
|
|
} else {
|
|
key := meta.Name + "+" + meta.Version + "+" + meta.BundleIdentifier
|
|
if _, ok := a.installedSoftware.Load(key); !ok {
|
|
source := ""
|
|
switch a.os {
|
|
case "macos":
|
|
source = "apps"
|
|
case "windows":
|
|
source = "programs"
|
|
case "ubuntu":
|
|
source = "deb_packages"
|
|
default:
|
|
log.Printf("unknown OS to software installer: %s", a.os)
|
|
return
|
|
}
|
|
a.installedSoftware.Store(key, map[string]string{
|
|
"name": meta.Name,
|
|
"version": meta.Version,
|
|
"bundle_identifier": meta.BundleIdentifier,
|
|
"source": source,
|
|
"installed_path": os.DevNull,
|
|
})
|
|
}
|
|
}
|
|
|
|
if installer.PostInstallScript != "" {
|
|
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
|
if installer.PostInstallScript == "exit 0" { //nolint:gocritic // ignore ifElseChain
|
|
// Always pass
|
|
payload.PostInstallScriptExitCode = ptr.Int(0)
|
|
payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (always pass)")
|
|
} else if installer.PostInstallScript == "exit 1" {
|
|
payload.PostInstallScriptExitCode = ptr.Int(1)
|
|
payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (always fail)")
|
|
} else if a.softwareInstaller.postInstallFailureProb > 0.0 && rand.Float64() <= a.softwareInstaller.postInstallFailureProb {
|
|
payload.PostInstallScriptExitCode = ptr.Int(1)
|
|
payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (fail)")
|
|
} else {
|
|
payload.PostInstallScriptExitCode = ptr.Int(0)
|
|
payload.PostInstallScriptOutput = ptr.String("PostInstall on osquery-perf (pass)")
|
|
}
|
|
}
|
|
}
|
|
|
|
err = orbitClient.SaveInstallerResult(payload)
|
|
if err != nil {
|
|
log.Println("save installer result:", err)
|
|
a.stats.IncrementSoftwareInstallErrs()
|
|
return
|
|
}
|
|
}
|
|
|
|
// shouldSignRequest determines if a request should be signed based on its path
|
|
func (a *agent) shouldSignRequest(req *http.Request) bool {
|
|
// Don't sign if HTTP signatures are not enabled
|
|
if !a.hostIdentityClient.IsEnabled() || !a.hostIdentityClient.HasSigner() {
|
|
return false
|
|
}
|
|
|
|
// Exclude ping endpoint from signing
|
|
if strings.HasSuffix(req.URL.Path, "/api/fleet/orbit/ping") {
|
|
return false
|
|
}
|
|
|
|
// Only sign specific API paths
|
|
return strings.Contains(req.URL.Path, "/api/fleet/orbit/") || strings.Contains(req.URL.Path, "/osquery/")
|
|
}
|
|
|
|
// sign applies HTTP message signature to a request if needed
|
|
func (a *agent) sign(req *http.Request) *http.Request {
|
|
// Apply HTTP message signature if this request should be signed
|
|
if a.shouldSignRequest(req) {
|
|
if err := a.hostIdentityClient.SignRequest(req); err != nil {
|
|
log.Printf("Agent %d: Failed to sign HTTP request: %v", a.agentIndex, err)
|
|
}
|
|
}
|
|
return req
|
|
}
|
|
|
|
func (a *agent) waitingDo(fn func() *http.Request) *http.Response {
|
|
response, err := http.DefaultClient.Do(a.sign(fn()))
|
|
for err != nil || response.StatusCode != http.StatusOK {
|
|
if err != nil {
|
|
log.Printf("failed to run request: %s", err)
|
|
} else { // res.StatusCode() != http.StatusOK
|
|
response.Body.Close()
|
|
log.Printf("request failed: %d", response.StatusCode)
|
|
}
|
|
a.stats.IncrementErrors(1)
|
|
<-time.Tick(time.Duration(rand.Intn(120)+1) * time.Second)
|
|
response, err = http.DefaultClient.Do(a.sign(fn()))
|
|
}
|
|
return response
|
|
}
|
|
|
|
// TODO: add support to `alreadyEnrolled` akin to the `enroll` function. for
|
|
// now, we assume that the agent is not already enrolled, if you kill the agent
|
|
// process then those Orbit node keys are gone.
|
|
func (a *agent) orbitEnroll() error {
|
|
params := contract.EnrollOrbitRequest{
|
|
EnrollSecret: a.EnrollSecret,
|
|
HardwareUUID: a.UUID,
|
|
HardwareSerial: a.SerialNumber,
|
|
Hostname: a.CachedString("hostname"),
|
|
}
|
|
jsonBytes, err := json.Marshal(params)
|
|
if err != nil {
|
|
log.Println("orbit json marshall:", err)
|
|
return err
|
|
}
|
|
|
|
response := a.waitingDo(func() *http.Request {
|
|
request, err := http.NewRequest("POST", a.serverAddress+"/api/fleet/orbit/enroll", bytes.NewReader(jsonBytes))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
request.Header.Add("Content-type", "application/json")
|
|
return request
|
|
})
|
|
defer response.Body.Close()
|
|
|
|
var parsedResp service.EnrollOrbitResponse
|
|
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
|
|
log.Println("orbit json parse:", err)
|
|
return err
|
|
}
|
|
|
|
a.orbitNodeKey = &parsedResp.OrbitNodeKey
|
|
a.stats.IncrementOrbitEnrollments()
|
|
return nil
|
|
}
|
|
|
|
// This is an osquery enroll as opposed to an orbit enroll
|
|
func (a *agent) enroll(i int, onlyAlreadyEnrolled bool) error {
|
|
a.nodeKey = a.nodeKeyManager.Get(i)
|
|
if a.nodeKey != "" {
|
|
a.stats.IncrementEnrollments()
|
|
return nil
|
|
}
|
|
|
|
if onlyAlreadyEnrolled {
|
|
return errors.New("not enrolled")
|
|
}
|
|
|
|
response := a.waitingDo(func() *http.Request {
|
|
var body bytes.Buffer
|
|
if err := a.templates.ExecuteTemplate(&body, "enroll", a); err != nil {
|
|
panic(err)
|
|
}
|
|
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/enroll", &body)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
request.Header.Add("Content-type", "application/json")
|
|
return request
|
|
})
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode != http.StatusOK {
|
|
log.Println("enroll status:", response.StatusCode)
|
|
return fmt.Errorf("status code: %d", response.StatusCode)
|
|
}
|
|
|
|
var parsedResp enrollResponse
|
|
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
|
|
log.Println("json parse:", err)
|
|
return err
|
|
}
|
|
|
|
a.nodeKey = parsedResp.NodeKey
|
|
a.stats.IncrementEnrollments()
|
|
|
|
a.nodeKeyManager.Add(a.nodeKey)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *agent) config() error {
|
|
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/config", bytes.NewReader([]byte(`{"node_key": "`+a.nodeKey+`"}`)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Header.Add("Content-type", "application/json")
|
|
|
|
response, err := http.DefaultClient.Do(a.sign(request))
|
|
if err != nil {
|
|
return fmt.Errorf("config request failed to run: %w", err)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
a.stats.IncrementConfigRequests()
|
|
|
|
statusCode := response.StatusCode
|
|
if statusCode != http.StatusOK {
|
|
a.stats.IncrementConfigErrors()
|
|
return fmt.Errorf("config request failed: %d", statusCode)
|
|
}
|
|
|
|
parsedResp := struct {
|
|
Packs map[string]struct {
|
|
Queries map[string]interface{} `json:"queries"`
|
|
} `json:"packs"`
|
|
}{}
|
|
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
|
|
a.stats.IncrementConfigErrors()
|
|
return fmt.Errorf("json parse at config: %w", err)
|
|
}
|
|
|
|
existingLastRunData := make(map[string]int64)
|
|
|
|
a.scheduledQueryMapMutex.RLock()
|
|
queryData := a.scheduledQueryData
|
|
a.scheduledQueryMapMutex.RUnlock()
|
|
queryData.Range(func(key, value any) bool {
|
|
existingLastRunData[key.(string)] = value.(scheduledQuery).lastRun
|
|
|
|
return true
|
|
})
|
|
|
|
newScheduledQueryData := new(sync.Map)
|
|
|
|
for packName, pack := range parsedResp.Packs {
|
|
for queryName, query := range pack.Queries {
|
|
m, ok := query.(map[string]interface{})
|
|
if !ok {
|
|
return fmt.Errorf("processing scheduled query failed: %v", query)
|
|
}
|
|
|
|
q := scheduledQuery{}
|
|
q.packName = packName
|
|
q.Name = queryName
|
|
|
|
// This allows us to set the number of rows returned by the query
|
|
// by appending a number to the query name, e.g. "queryName_10"
|
|
q.numRows = 1
|
|
parts := strings.Split(q.Name, "_")
|
|
if len(parts) == 2 {
|
|
num, err := strconv.ParseInt(parts[1], 10, 32)
|
|
if err != nil {
|
|
num = 1
|
|
}
|
|
q.numRows = uint(num)
|
|
}
|
|
|
|
q.ScheduleInterval = m["interval"].(float64)
|
|
q.Query = m["query"].(string)
|
|
|
|
scheduledQueryName := packName + "_" + queryName
|
|
if lastRun, ok := existingLastRunData[scheduledQueryName]; ok {
|
|
q.lastRun = lastRun
|
|
}
|
|
newScheduledQueryData.Store(scheduledQueryName, q)
|
|
}
|
|
}
|
|
|
|
a.scheduledQueryMapMutex.Lock()
|
|
a.scheduledQueryData = newScheduledQueryData
|
|
a.scheduledQueryMapMutex.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
const stringVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
|
|
|
|
func randomString(n int) string {
|
|
sb := strings.Builder{}
|
|
sb.Grow(n)
|
|
for i := 0; i < n; i++ {
|
|
sb.WriteByte(stringVals[rand.Int63()%int64(len(stringVals))])
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// selectSoftwareVersion returns a consistent version for a software package based on its name.
|
|
// Uses a per-agent map that assigns each first character to one of 32 version options:
|
|
// - 0: base version (99% probability)
|
|
// - 1: alternate version (0.9% probability)
|
|
// - 2-31: patch versions (0.1% probability, split among 30 different patch numbers)
|
|
// This ensures each agent consistently reports the same version for software with the same first letter.
|
|
func (a *agent) selectSoftwareVersion(softwareName, baseVersion, alternateVersion string) string {
|
|
if len(softwareName) == 0 {
|
|
return baseVersion
|
|
}
|
|
|
|
// Get first character as a rune
|
|
firstChar := rune(softwareName[0])
|
|
|
|
// Check if we've already assigned a version option for this character
|
|
versionOption, exists := a.softwareVersionMap[firstChar]
|
|
if !exists {
|
|
// Randomly assign option with distribution matching original randomizeVersion:
|
|
// 99% base (0), 0.9% alternate (1), 0.1% patch versions (2-31)
|
|
r := rand.Float64()
|
|
switch {
|
|
case r < 0.99:
|
|
versionOption = 0 // base version
|
|
case r < 0.999:
|
|
versionOption = 1 // alternate version
|
|
default:
|
|
versionOption = 2 + rand.Intn(30) // patch version: 2-31 maps to patch 0-29
|
|
}
|
|
a.softwareVersionMap[firstChar] = versionOption
|
|
}
|
|
|
|
// Return the appropriate version based on the stored option
|
|
switch versionOption {
|
|
case 0:
|
|
return baseVersion
|
|
case 1:
|
|
return alternateVersion
|
|
default: // 2-31
|
|
patchNum := versionOption - 2 // 0-29
|
|
return fmt.Sprintf("%s.%d", baseVersion, patchNum)
|
|
}
|
|
}
|
|
|
|
func (a *agent) CachedString(key string) string {
|
|
if val, ok := a.strings[key]; ok {
|
|
return val
|
|
}
|
|
val := randomString(12)
|
|
a.strings[key] = val
|
|
return val
|
|
}
|
|
|
|
func (a *agent) hostUsers() []map[string]string {
|
|
groupNames := []string{"staff", "nobody", "wheel", "tty", "daemon"}
|
|
shells := []string{"/bin/zsh", "/bin/sh", "/usr/bin/false", "/bin/bash"}
|
|
commonUsers := make([]map[string]string, a.userCount.common)
|
|
for i := 0; i < len(commonUsers); i++ {
|
|
commonUsers[i] = map[string]string{
|
|
"uid": fmt.Sprint(i),
|
|
"username": fmt.Sprintf("Common_%d", i),
|
|
"type": "", // Empty for macOS.
|
|
"groupname": groupNames[i%len(groupNames)],
|
|
"shell": shells[i%len(shells)],
|
|
}
|
|
}
|
|
uniqueUsers := make([]map[string]string, a.userCount.unique)
|
|
for i := 0; i < len(uniqueUsers); i++ {
|
|
uniqueUsers[i] = map[string]string{
|
|
"uid": fmt.Sprint(i),
|
|
"username": fmt.Sprintf("Unique_%d_%d", a.agentIndex, i),
|
|
"type": "", // Empty for macOS.
|
|
"groupname": groupNames[i%len(groupNames)],
|
|
"shell": shells[i%len(shells)],
|
|
}
|
|
}
|
|
users := commonUsers
|
|
users = append(users, uniqueUsers...)
|
|
rand.Shuffle(len(users), func(i, j int) {
|
|
users[i], users[j] = users[j], users[i]
|
|
})
|
|
return users
|
|
}
|
|
|
|
func (a *agent) softwareMacOS() []map[string]string {
|
|
var lastOpenedCount int
|
|
|
|
totalCommon := a.softwareCount.common
|
|
totalDuplicates := (a.softwareCount.common * a.softwareCount.duplicateBundleIdentifiersPercent) / 100
|
|
totalSoftware := totalCommon + totalDuplicates
|
|
|
|
var startIdx, endIdx int
|
|
|
|
if a.totalHostCount == 0 {
|
|
// non-distributed mode, all hosts get the same software count
|
|
startIdx = 0
|
|
endIdx = totalSoftware
|
|
} else {
|
|
// distributed mode, distribute software across hosts
|
|
globalAgentIndex := a.hostIndexOffset + (a.agentIndex - 1)
|
|
|
|
perHostCount := totalSoftware / a.totalHostCount
|
|
remainder := totalSoftware % a.totalHostCount
|
|
|
|
startIdx = globalAgentIndex * perHostCount
|
|
if globalAgentIndex < remainder {
|
|
startIdx += globalAgentIndex
|
|
} else {
|
|
startIdx += remainder
|
|
}
|
|
|
|
endIdx = startIdx + perHostCount
|
|
if globalAgentIndex < remainder {
|
|
endIdx++
|
|
}
|
|
}
|
|
|
|
commonSoftware := make([]map[string]string, 0)
|
|
duplicateBundleSoftware := make([]map[string]string, 0)
|
|
groupSize := 4
|
|
|
|
for i := startIdx; i < endIdx; i++ {
|
|
var lastOpenedAt string
|
|
if l := a.genLastOpenedAt(&lastOpenedCount); l != nil {
|
|
lastOpenedAt = fmt.Sprint(l.Unix())
|
|
}
|
|
|
|
if i < totalCommon {
|
|
name := fmt.Sprintf("Common_%d%s", i, a.commonSoftwareNameSuffix)
|
|
commonSoftware = append(commonSoftware, map[string]string{
|
|
"name": name,
|
|
"version": a.selectSoftwareVersion(name, "0.0.1", "0.0.2"),
|
|
"bundle_identifier": fmt.Sprintf("com.fleetdm.osquery-perf.common_%d", i),
|
|
"source": "apps",
|
|
"last_opened_at": lastOpenedAt,
|
|
"installed_path": fmt.Sprintf("/some/path/Common_%d.app", i),
|
|
})
|
|
} else {
|
|
duplicateIdx := i - totalCommon
|
|
bundleIDIndex := duplicateIdx / groupSize
|
|
bundleID := fmt.Sprintf("com.fleetdm.osquery-perf.common_%d", bundleIDIndex%totalCommon)
|
|
|
|
var name string
|
|
if a.softwareCount.softwareRenaming {
|
|
name = fmt.Sprintf("RENAMED_DuplicateBundle_%d", duplicateIdx)
|
|
} else {
|
|
name = fmt.Sprintf("DuplicateBundle_%d", duplicateIdx)
|
|
}
|
|
|
|
duplicateBundleSoftware = append(duplicateBundleSoftware, map[string]string{
|
|
"name": name,
|
|
"version": fmt.Sprintf("0.0.1%d", duplicateIdx),
|
|
"bundle_identifier": bundleID,
|
|
"source": "apps",
|
|
"installed_path": fmt.Sprintf("/some/path/DuplicateBundle_%d.app", duplicateIdx),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Unique Software (always per-host, not distributed)
|
|
uniqueSoftware := make([]map[string]string, a.softwareCount.unique)
|
|
for i := 0; i < len(uniqueSoftware); i++ {
|
|
var lastOpenedAt string
|
|
if l := a.genLastOpenedAt(&lastOpenedCount); l != nil {
|
|
lastOpenedAt = l.Format(time.UnixDate)
|
|
}
|
|
name := fmt.Sprintf("Unique_%s_%d", a.CachedString("hostname"), i)
|
|
uniqueSoftware[i] = map[string]string{
|
|
"name": name,
|
|
"version": a.selectSoftwareVersion(name, "1.1.1", "1.1.2"),
|
|
"bundle_identifier": fmt.Sprintf("com.fleetdm.osquery-perf.unique_%s_%d", a.CachedString("hostname"), i),
|
|
"source": "apps",
|
|
"last_opened_at": lastOpenedAt,
|
|
"installed_path": fmt.Sprintf("/some/path/Unique_%s_%d.app", a.CachedString("hostname"), i),
|
|
}
|
|
}
|
|
if a.softwareCount.uniqueSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareCount.uniqueSoftwareUninstallProb {
|
|
rand.Shuffle(len(uniqueSoftware), func(i, j int) {
|
|
uniqueSoftware[i], uniqueSoftware[j] = uniqueSoftware[j], uniqueSoftware[i]
|
|
})
|
|
uniqueSoftware = uniqueSoftware[:a.softwareCount.unique-a.softwareCount.uniqueSoftwareUninstallCount]
|
|
}
|
|
|
|
// Use database software 80% of the time if available; otherwise use legacy vulnerable software.
|
|
var realSoftware []map[string]string
|
|
if softwareDB != nil && len(softwareDB.Darwin) > 0 && rand.Float64() < 0.8 { // nolint:gosec,G404 // load testing, not security-sensitive
|
|
// Initialize cached indices on first call, then mutate on subsequent calls
|
|
if a.cachedSoftwareIndices == nil {
|
|
// Select a random count between min-max, then pick that many random indices
|
|
count := softwaredb.RandomSoftwareCount("darwin")
|
|
perm := rand.Perm(len(softwareDB.Darwin))
|
|
a.cachedSoftwareIndices = make([]uint32, count)
|
|
for i := 0; i < count; i++ {
|
|
a.cachedSoftwareIndices[i] = uint32(perm[i])
|
|
}
|
|
} else {
|
|
a.cachedSoftwareIndices = softwaredb.MaybeMutateSoftware(a.cachedSoftwareIndices, len(softwareDB.Darwin))
|
|
}
|
|
realSoftware = softwareDB.DarwinToMaps(a.cachedSoftwareIndices)
|
|
} else {
|
|
// Vulnerable Software
|
|
var vCount int
|
|
if a.softwareCount.vulnerable < 0 {
|
|
vCount = len(macosVulnerableSoftware)
|
|
} else {
|
|
vCount = a.softwareCount.vulnerable
|
|
}
|
|
|
|
realSoftware = make([]map[string]string, 0, vCount)
|
|
randomIndices := rand.Perm(len(macosVulnerableSoftware)) // Randomize software selection
|
|
var softwareLimit int
|
|
|
|
switch {
|
|
case a.softwareCount.vulnerable < 0: // Sequential assignment
|
|
softwareLimit = len(macosVulnerableSoftware)
|
|
case a.softwareCount.vulnerable == 0: // No vulnerable software
|
|
softwareLimit = 0
|
|
default: // Random assignment
|
|
softwareLimit = min(a.softwareCount.vulnerable, len(macosVulnerableSoftware)) // Limit to available software
|
|
}
|
|
|
|
for i := range softwareLimit {
|
|
var sw fleet.Software
|
|
|
|
if a.softwareCount.vulnerable < 0 {
|
|
sw = macosVulnerableSoftware[i]
|
|
} else {
|
|
sw = macosVulnerableSoftware[randomIndices[i]]
|
|
}
|
|
|
|
var lastOpenedAt string
|
|
if l := a.genLastOpenedAt(&lastOpenedCount); l != nil {
|
|
lastOpenedAt = l.Format(time.UnixDate)
|
|
}
|
|
|
|
realSoftware = append(realSoftware, map[string]string{
|
|
"name": sw.Name,
|
|
"version": sw.Version,
|
|
"bundle_identifier": sw.BundleIdentifier,
|
|
"source": sw.Source,
|
|
"last_opened_at": lastOpenedAt,
|
|
"installed_path": fmt.Sprintf("/some/path/%s", sw.Name),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Combine all software
|
|
software := commonSoftware
|
|
software = append(software, uniqueSoftware...)
|
|
software = append(software, realSoftware...)
|
|
software = append(software, duplicateBundleSoftware...)
|
|
a.installedSoftware.Range(func(key, value interface{}) bool {
|
|
software = append(software, value.(map[string]string))
|
|
return true
|
|
})
|
|
rand.Shuffle(len(software), func(i, j int) {
|
|
software[i], software[j] = software[j], software[i]
|
|
})
|
|
|
|
return software
|
|
}
|
|
|
|
func (a *mdmAgent) softwareIOSandIPadOS(source string) []fleet.Software {
|
|
commonSoftware := make([]map[string]string, a.softwareCount.common)
|
|
for i := 0; i < len(commonSoftware); i++ {
|
|
name := fmt.Sprintf("Common_%d", i)
|
|
commonSoftware[i] = map[string]string{
|
|
"name": name,
|
|
"version": a.selectSoftwareVersion(name, "0.0.1", "0.0.2"),
|
|
"bundle_identifier": fmt.Sprintf("com.fleetdm.osquery-perf.common_%d", i),
|
|
"source": source,
|
|
}
|
|
}
|
|
if a.softwareCount.commonSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareCount.commonSoftwareUninstallProb {
|
|
rand.Shuffle(len(commonSoftware), func(i, j int) {
|
|
commonSoftware[i], commonSoftware[j] = commonSoftware[j], commonSoftware[i]
|
|
})
|
|
commonSoftware = commonSoftware[:a.softwareCount.common-a.softwareCount.commonSoftwareUninstallCount]
|
|
}
|
|
uniqueSoftware := make([]map[string]string, a.softwareCount.unique)
|
|
for i := 0; i < len(uniqueSoftware); i++ {
|
|
name := fmt.Sprintf("Unique_%s_%d", a.CachedString("hostname"), i)
|
|
uniqueSoftware[i] = map[string]string{
|
|
"name": name,
|
|
"version": a.selectSoftwareVersion(name, "1.1.1", "1.1.2"),
|
|
"bundle_identifier": fmt.Sprintf("com.fleetdm.osquery-perf.unique_%s_%d", a.CachedString("hostname"), i),
|
|
"source": source,
|
|
}
|
|
}
|
|
if a.softwareCount.uniqueSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareCount.uniqueSoftwareUninstallProb {
|
|
rand.Shuffle(len(uniqueSoftware), func(i, j int) {
|
|
uniqueSoftware[i], uniqueSoftware[j] = uniqueSoftware[j], uniqueSoftware[i]
|
|
})
|
|
uniqueSoftware = uniqueSoftware[:a.softwareCount.unique-a.softwareCount.uniqueSoftwareUninstallCount]
|
|
}
|
|
software := commonSoftware
|
|
software = append(software, uniqueSoftware...)
|
|
rand.Shuffle(len(software), func(i, j int) {
|
|
software[i], software[j] = software[j], software[i]
|
|
})
|
|
fleetSoftware := make([]fleet.Software, len(software))
|
|
for i, s := range software {
|
|
fleetSoftware[i] = fleet.Software{
|
|
Name: s["name"],
|
|
Version: s["version"],
|
|
BundleIdentifier: s["bundle_identifier"],
|
|
Source: s["source"],
|
|
}
|
|
}
|
|
return fleetSoftware
|
|
}
|
|
|
|
func (a *agent) softwareVSCodeExtensions() []map[string]string {
|
|
commonVSCodeExtensionsSoftware := make([]map[string]string, a.softwareVSCodeExtensionsCount.common)
|
|
for i := 0; i < len(commonVSCodeExtensionsSoftware); i++ {
|
|
name := fmt.Sprintf("common.extension_%d", i)
|
|
commonVSCodeExtensionsSoftware[i] = map[string]string{
|
|
"name": name,
|
|
"version": a.selectSoftwareVersion(name, "0.0.1", "0.0.2"),
|
|
"source": "vscode_extensions",
|
|
}
|
|
}
|
|
if a.softwareVSCodeExtensionsCount.commonSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareCount.commonSoftwareUninstallProb {
|
|
rand.Shuffle(len(commonVSCodeExtensionsSoftware), func(i, j int) {
|
|
commonVSCodeExtensionsSoftware[i], commonVSCodeExtensionsSoftware[j] = commonVSCodeExtensionsSoftware[j], commonVSCodeExtensionsSoftware[i]
|
|
})
|
|
commonVSCodeExtensionsSoftware = commonVSCodeExtensionsSoftware[:a.softwareVSCodeExtensionsCount.common-a.softwareVSCodeExtensionsCount.commonSoftwareUninstallCount]
|
|
}
|
|
uniqueVSCodeExtensionsSoftware := make([]map[string]string, a.softwareVSCodeExtensionsCount.unique)
|
|
for i := 0; i < len(uniqueVSCodeExtensionsSoftware); i++ {
|
|
name := fmt.Sprintf("unique.extension_%s_%d", a.CachedString("hostname"), i)
|
|
uniqueVSCodeExtensionsSoftware[i] = map[string]string{
|
|
"name": name,
|
|
"version": a.selectSoftwareVersion(name, "1.1.1", "1.1.2"),
|
|
"source": "vscode_extensions",
|
|
}
|
|
}
|
|
if a.softwareVSCodeExtensionsCount.uniqueSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsCount.uniqueSoftwareUninstallProb {
|
|
rand.Shuffle(len(uniqueVSCodeExtensionsSoftware), func(i, j int) {
|
|
uniqueVSCodeExtensionsSoftware[i], uniqueVSCodeExtensionsSoftware[j] = uniqueVSCodeExtensionsSoftware[j], uniqueVSCodeExtensionsSoftware[i]
|
|
})
|
|
uniqueVSCodeExtensionsSoftware = uniqueVSCodeExtensionsSoftware[:a.softwareVSCodeExtensionsCount.unique-a.softwareVSCodeExtensionsCount.uniqueSoftwareUninstallCount]
|
|
}
|
|
var vulnerableVSCodeExtensionsSoftware []map[string]string
|
|
for _, vsCodeExtension := range vsCodeExtensionsVulnerableSoftware {
|
|
vulnerableVSCodeExtensionsSoftware = append(vulnerableVSCodeExtensionsSoftware, map[string]string{
|
|
"name": vsCodeExtension.Name,
|
|
"version": vsCodeExtension.Version,
|
|
"vendor": vsCodeExtension.Vendor,
|
|
"source": vsCodeExtension.Source,
|
|
})
|
|
}
|
|
software := commonVSCodeExtensionsSoftware
|
|
software = append(software, uniqueVSCodeExtensionsSoftware...)
|
|
software = append(software, vulnerableVSCodeExtensionsSoftware...)
|
|
rand.Shuffle(len(software), func(i, j int) {
|
|
software[i], software[j] = software[j], software[i]
|
|
})
|
|
return software
|
|
}
|
|
|
|
func selectKernels(kernelList []string) []map[string]string {
|
|
// Determine number of kernels based on probability distribution
|
|
r := rand.Float64()
|
|
var numKernels int
|
|
switch {
|
|
case r < 0.05:
|
|
numKernels = 0 // 5% - rare, fresh install
|
|
case r < 0.45:
|
|
numKernels = 1 // 40% - most common, single current kernel
|
|
case r < 0.75:
|
|
numKernels = 2 // 30% - common, current + previous
|
|
case r < 0.90:
|
|
numKernels = 3 // 15% - a few old kernels
|
|
case r < 0.95:
|
|
numKernels = 4 // 5% - rare
|
|
case r < 0.98:
|
|
numKernels = 5 // 3% - very rare
|
|
default:
|
|
numKernels = 6 // 2% - very rare, many old kernels not cleaned up
|
|
}
|
|
|
|
if numKernels == 0 || len(kernelList) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Randomly select unique kernels
|
|
indices := rand.Perm(len(kernelList))
|
|
kernels := make([]map[string]string, 0, numKernels)
|
|
|
|
for i := 0; i < numKernels && i < len(indices); i++ {
|
|
kernelName := kernelList[indices[i]]
|
|
// Extract version from name (remove "linux-image-" prefix)
|
|
version := strings.TrimPrefix(kernelName, "linux-image-")
|
|
|
|
kernels = append(kernels, map[string]string{
|
|
"name": kernelName,
|
|
"version": version,
|
|
"source": "deb_packages",
|
|
})
|
|
}
|
|
|
|
return kernels
|
|
}
|
|
|
|
func (a *agent) DistributedRead() (*distributedReadResponse, error) {
|
|
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/distributed/read", bytes.NewReader([]byte(`{"node_key": "`+a.nodeKey+`"}`)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request.Header.Add("Content-type", "application/json")
|
|
|
|
response, err := http.DefaultClient.Do(a.sign(request))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("distributed/read request failed to run: %w", err)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
a.stats.IncrementDistributedReads()
|
|
|
|
statusCode := response.StatusCode
|
|
if statusCode != http.StatusOK {
|
|
a.stats.IncrementDistributedReadErrors()
|
|
return nil, fmt.Errorf("distributed/read request failed: %d", statusCode)
|
|
}
|
|
|
|
var parsedResp distributedReadResponse
|
|
if err := json.NewDecoder(response.Body).Decode(&parsedResp); err != nil {
|
|
a.stats.IncrementDistributedReadErrors()
|
|
log.Printf("json parse: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
return &parsedResp, nil
|
|
}
|
|
|
|
var defaultQueryResult = []map[string]string{
|
|
{"foo": "bar"},
|
|
}
|
|
|
|
func (a *agent) genLastOpenedAt(count *int) *time.Time {
|
|
if *count >= a.softwareCount.withLastOpened {
|
|
return nil
|
|
}
|
|
*count++
|
|
if rand.Float64() <= a.softwareCount.lastOpenedProb {
|
|
now := time.Now()
|
|
return &now
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *agent) runPolicy(query string) []map[string]string {
|
|
// Used to control the pass or fail of a policy
|
|
// in the UI by setting the query to "select 1"(pass)
|
|
// or "select 0"(fail)
|
|
query = strings.TrimRight(query, ";")
|
|
query = strings.ToLower(query)
|
|
|
|
switch query {
|
|
case "select 1":
|
|
return []map[string]string{
|
|
{"1": "1"},
|
|
}
|
|
case "select 0":
|
|
return []map[string]string{}
|
|
}
|
|
|
|
if rand.Float64() <= a.policyPassProb {
|
|
return []map[string]string{
|
|
{"1": "1"},
|
|
}
|
|
}
|
|
return []map[string]string{}
|
|
}
|
|
|
|
func (a *agent) randomQueryStats() []map[string]string {
|
|
var stats []map[string]string
|
|
a.scheduledQueryMapMutex.RLock()
|
|
queryData := a.scheduledQueryData
|
|
a.scheduledQueryMapMutex.RUnlock()
|
|
|
|
queryData.Range(func(key, value any) bool {
|
|
queryName := key.(string)
|
|
|
|
stats = append(stats, map[string]string{
|
|
"name": queryName,
|
|
"delimiter": "_",
|
|
"average_memory": fmt.Sprint(rand.Intn(200) + 10),
|
|
"denylisted": "false",
|
|
"executions": fmt.Sprint(rand.Intn(100) + 1),
|
|
"interval": fmt.Sprint(rand.Intn(100) + 1),
|
|
"last_executed": fmt.Sprint(time.Now().Unix()),
|
|
"output_size": fmt.Sprint(rand.Intn(100) + 1),
|
|
"system_time": fmt.Sprint(rand.Intn(4000) + 10),
|
|
"user_time": fmt.Sprint(rand.Intn(4000) + 10),
|
|
"wall_time": fmt.Sprint(rand.Intn(4) + 1),
|
|
"wall_time_ms": fmt.Sprint(rand.Intn(4000) + 10),
|
|
})
|
|
|
|
return true
|
|
})
|
|
return stats
|
|
}
|
|
|
|
// mdmMac returns the results for the `mdm` table query.
|
|
//
|
|
// If the host is enrolled via MDM it will return installed_from_dep as false
|
|
// (which means the host will be identified as manually enrolled).
|
|
//
|
|
// NOTE: To support proper DEP simulation in a loadtest environment
|
|
// we may need to implement a mocked Apple DEP endpoint.
|
|
func (a *agent) mdmMac() []map[string]string {
|
|
if !a.mdmEnrolled() {
|
|
return []map[string]string{
|
|
{"enrolled": "false", "server_url": "", "installed_from_dep": "false"},
|
|
}
|
|
}
|
|
return []map[string]string{
|
|
{
|
|
"enrolled": "true",
|
|
"server_url": a.macMDMClient.EnrollInfo.MDMURL,
|
|
"installed_from_dep": "false",
|
|
"payload_identifier": apple_mdm.FleetPayloadIdentifier,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (a *agent) mdmConfigProfilesMac() []map[string]string {
|
|
return []map[string]string{
|
|
{
|
|
"identifier": "osquery-perf",
|
|
"display_name": "OSQuery Perf Agent",
|
|
"install_date": "2006-01-02 15:04:05 -0700",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (a *agent) entraConditionalAccess() []map[string]string {
|
|
return []map[string]string{
|
|
{
|
|
"device_id": a.entraIDDeviceID,
|
|
"user_principal_name": a.entraIDUserPrincipalName,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (a *agent) mdmEnrolled() bool {
|
|
a.isEnrolledToMDMMu.Lock()
|
|
defer a.isEnrolledToMDMMu.Unlock()
|
|
|
|
return a.isEnrolledToMDM
|
|
}
|
|
|
|
func (a *agent) setMDMEnrolled() {
|
|
a.isEnrolledToMDMMu.Lock()
|
|
defer a.isEnrolledToMDMMu.Unlock()
|
|
|
|
a.isEnrolledToMDM = true
|
|
}
|
|
|
|
func (a *agent) mdmWindows() []map[string]string {
|
|
if !a.mdmEnrolled() {
|
|
return []map[string]string{
|
|
// empty service url means not enrolled
|
|
{"aad_resource_id": "", "discovery_service_url": "", "provider_id": "", "installation_type": "Client"},
|
|
}
|
|
}
|
|
return []map[string]string{
|
|
{
|
|
"aad_resource_id": "",
|
|
"discovery_service_url": a.serverAddress,
|
|
"provider_id": fleet.WellKnownMDMFleet,
|
|
"installation_type": "Client",
|
|
},
|
|
}
|
|
}
|
|
|
|
var munkiIssues = func() []string {
|
|
// generate a list of random munki issues (messages)
|
|
issues := make([]string, 1000)
|
|
for i := range issues {
|
|
// message size: between 60 and 200, with spaces between each 10-char word so
|
|
// that it can still make a bit of sense for UI tests.
|
|
numParts := rand.Intn(15) + 6 // number between 0-14, add 6 to get between 6-20
|
|
var sb strings.Builder
|
|
for j := 0; j < numParts; j++ {
|
|
if j > 0 {
|
|
sb.WriteString(" ")
|
|
}
|
|
sb.WriteString(randomString(10))
|
|
}
|
|
issues[i] = sb.String()
|
|
}
|
|
return issues
|
|
}()
|
|
|
|
func (a *agent) munkiInfo() []map[string]string {
|
|
var errors, warnings []string
|
|
|
|
if rand.Float64() <= a.munkiIssueProb {
|
|
for i := 0; i < a.munkiIssueCount; i++ {
|
|
if rand.Intn(2) == 1 {
|
|
errors = append(errors, munkiIssues[rand.Intn(len(munkiIssues))])
|
|
} else {
|
|
warnings = append(warnings, munkiIssues[rand.Intn(len(munkiIssues))])
|
|
}
|
|
}
|
|
}
|
|
|
|
errList := strings.Join(errors, ";")
|
|
warnList := strings.Join(warnings, ";")
|
|
return []map[string]string{
|
|
{"version": "1.2.3", "errors": errList, "warnings": warnList},
|
|
}
|
|
}
|
|
|
|
func (a *agent) googleChromeProfiles() []map[string]string {
|
|
count := rand.Intn(5) // return between 0 and 4 emails
|
|
result := make([]map[string]string, count)
|
|
for i := range result {
|
|
email := fmt.Sprintf("user%d@example.com", i)
|
|
if i == len(result)-1 {
|
|
// if the maximum number of emails is returned, set a random domain name
|
|
// so that we have email addresses that match a lot of hosts, and some
|
|
// that match few hosts.
|
|
domainRand := rand.Intn(10)
|
|
email = fmt.Sprintf("user%d@example%d.com", i, domainRand)
|
|
}
|
|
result[i] = map[string]string{"email": email}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (a *agent) batteries() []map[string]string {
|
|
count := rand.Intn(3) // return between 0 and 2 batteries
|
|
result := make([]map[string]string, count)
|
|
for i := range result {
|
|
max_capacity := 700 + rand.Intn(300) // between 700 and 1000 to ensure most batteries are healthy
|
|
cycleCount := rand.Intn(1200)
|
|
|
|
result[i] = map[string]string{
|
|
"serial_number": fmt.Sprintf("%04d", i),
|
|
"cycle_count": strconv.Itoa(cycleCount),
|
|
"max_capacity": strconv.Itoa(max_capacity),
|
|
"designed_capacity": "1000",
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (a *agent) diskSpace() []map[string]string {
|
|
// between 1-100 gigs, between 0-99 percentage available
|
|
gigs := rand.Intn(100)
|
|
gigs++
|
|
pct := rand.Intn(100)
|
|
available := gigs * pct / 100
|
|
return []map[string]string{
|
|
{
|
|
"percent_disk_space_available": strconv.Itoa(pct),
|
|
"gigs_disk_space_available": strconv.Itoa(available),
|
|
"gigs_total_disk_space": strconv.Itoa(gigs),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (a *agent) diskEncryption() []map[string]string {
|
|
// 50% of results have encryption enabled
|
|
a.DiskEncryptionEnabled = rand.Intn(2) == 1
|
|
if a.DiskEncryptionEnabled {
|
|
return []map[string]string{{"1": "1"}}
|
|
}
|
|
return []map[string]string{}
|
|
}
|
|
|
|
func (a *agent) diskEncryptionLinux() []map[string]string {
|
|
// 50% of results have encryption enabled
|
|
a.DiskEncryptionEnabled = rand.Intn(2) == 1
|
|
if a.DiskEncryptionEnabled {
|
|
return []map[string]string{
|
|
{"path": "/etc", "encrypted": "0"},
|
|
{"path": "/tmp", "encrypted": "0"},
|
|
{"path": "/", "encrypted": "1"},
|
|
}
|
|
}
|
|
return []map[string]string{
|
|
{"path": "/etc", "encrypted": "0"},
|
|
{"path": "/tmp", "encrypted": "0"},
|
|
}
|
|
}
|
|
|
|
func (a *agent) certificatesDarwin() []map[string]string {
|
|
a.certificatesMutex.RLock()
|
|
cache := a.certificatesCache
|
|
a.certificatesMutex.RUnlock()
|
|
|
|
// 90% of the time certificates do not change
|
|
if rand.Intn(100) < 90 && len(cache) > 0 {
|
|
return cache
|
|
}
|
|
|
|
// between 2 and 10 certificates (probably impossible to have 0, quick check
|
|
// on dogfood gives between 4-7)
|
|
count := rand.Intn(9) + 2
|
|
|
|
sources := []string{"system", "user"}
|
|
users := a.hostUsers()
|
|
const day = 24 * time.Hour
|
|
|
|
results := make([]map[string]string, count)
|
|
for i := range count {
|
|
m := make(map[string]string, 12)
|
|
m["ca"] = fmt.Sprint(rand.Intn(2))
|
|
m["common_name"] = uuid.NewString()
|
|
m["issuer"] = fmt.Sprintf("/C=US/O=Issuer %d Inc./CN=Issuer %d Common Name", i, i)
|
|
m["subject"] = fmt.Sprintf("/C=US/O=Subject %d Inc./OU=Subject %d Org Unit/CN=Subject %d Common Name", i, i, i)
|
|
m["key_algorithm"] = "rsaEncryption"
|
|
m["key_strength"] = "2048"
|
|
m["key_usage"] = "Data Encipherment, Key Encipherment, Digital Signature"
|
|
m["serial"] = uuid.NewString()
|
|
m["signing_algorithm"] = "sha256WithRSAEncryption"
|
|
// generate so that it may be expired
|
|
m["not_valid_after"] = fmt.Sprint(time.Now().Add(-1 * day).Add(time.Duration(rand.Intn(100)) * day).Unix())
|
|
// notBefore is always in the past (1-10 days in the past)
|
|
m["not_valid_before"] = fmt.Sprint(time.Now().Add(-time.Duration(rand.Intn(10)+1) * day).Unix())
|
|
rawHash := sha1.Sum([]byte(m["serial"])) //nolint: gosec
|
|
hash := hex.EncodeToString(rawHash[:])
|
|
m["sha1"] = hash
|
|
m["source"] = sources[rand.Intn(2)]
|
|
|
|
if m["source"] == "user" {
|
|
// Set username for user keychain certificates
|
|
user := users[rand.Intn(len(users))]
|
|
m["path"] = fmt.Sprintf(`/Users/%s/Library/Keychains/login.keychain-db`, user["username"])
|
|
}
|
|
|
|
results[i] = m
|
|
}
|
|
|
|
a.certificatesMutex.Lock()
|
|
a.certificatesCache = results
|
|
a.certificatesMutex.Unlock()
|
|
return results
|
|
}
|
|
|
|
func (a *agent) certificatesWindows() []map[string]string {
|
|
a.certificatesMutex.RLock()
|
|
cache := a.certificatesCache
|
|
a.certificatesMutex.RUnlock()
|
|
|
|
// 90% of the time certificates do not change
|
|
if rand.Intn(100) < 90 && len(cache) > 0 {
|
|
return cache
|
|
}
|
|
|
|
const day = 24 * time.Hour
|
|
|
|
// custom SCEP profile ID used for certs issued via custom SCEP profiles (inserted by
|
|
// FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID)
|
|
//
|
|
// TODO: make this configurable as a loadtest agent parameter? for now, just hardcode it and try
|
|
// manipulating it in loadtest DB directly if needed.
|
|
profileIDCustomSCEP := "w2a6fd2c4-0018-4bdc-8046-c7342962b576"
|
|
|
|
// when windows hosts enroll to Fleet MDM, we issue them a unique cert during the WSTEP/SCEP process
|
|
uuidFleetSCEP := uuid.NewString()
|
|
|
|
// uuids that we'll use in serials and hashes to ensure uniqueness
|
|
serial1 := uuid.NewString()
|
|
s1 := sha1.Sum([]byte(serial1)) //nolint: gosec
|
|
|
|
serial2 := uuid.NewString()
|
|
s2 := sha1.Sum([]byte(serial2)) //nolint: gosec
|
|
|
|
// Fleet SCEP cert example based on data from a real Windows host
|
|
c1 := map[string]string{
|
|
"ca": "-1",
|
|
"common_name": uuidFleetSCEP,
|
|
"subject": "Fleet, " + uuidFleetSCEP,
|
|
"issuer": "\"\", scep-ca, SCEP CA, FleetDM",
|
|
"key_algorithm": "RSA",
|
|
"key_strength": "2160",
|
|
"key_usage": "CERT_KEY_ENCIPHERMENT_KEY_USAGE,CERT_DIGITAL_SIGNATURE_KEY_USAGE",
|
|
"signing_algorithm": "sha256RSA",
|
|
// generate so that it may be expired
|
|
"not_valid_after": fmt.Sprint(time.Now().Add(-1 * day).Add(time.Duration(rand.Intn(100)) * day).Unix()),
|
|
// notBefore is always in the past (1-10 days in the past)
|
|
"not_valid_before": fmt.Sprint(time.Now().Add(-time.Duration(rand.Intn(10)+1) * day).Unix()),
|
|
"serial": serial1,
|
|
"sha1": hex.EncodeToString(s1[:]),
|
|
"username": "Admin",
|
|
"path": "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000\\Personal",
|
|
}
|
|
// Custom SCEP cert example based on data from a real Windows host
|
|
c2 := map[string]string{
|
|
"ca": "-1",
|
|
"common_name": fmt.Sprintf("%s User\n CN", profileIDCustomSCEP),
|
|
"subject": fmt.Sprintf("fleet-%s, \"%s User\n CN\"", profileIDCustomSCEP, profileIDCustomSCEP),
|
|
"issuer": "US, scep-ca, SCEP CA, MICROMDM SCEP CA",
|
|
"key_algorithm": "RSA",
|
|
"key_strength": "1120",
|
|
"key_usage": "CERT_DIGITAL_SIGNATURE_KEY_USAGE",
|
|
"signing_algorithm": "sha256RSA",
|
|
// generate so that it may be expired
|
|
"not_valid_after": fmt.Sprint(time.Now().Add(-1 * day).Add(time.Duration(rand.Intn(100)) * day).Unix()),
|
|
// notBefore is always in the past (1-10 days in the past)
|
|
"not_valid_before": fmt.Sprint(time.Now().Add(-time.Duration(rand.Intn(10)+1) * day).Unix()),
|
|
"serial": serial2,
|
|
"sha1": hex.EncodeToString(s2[:]),
|
|
"username": "Admin",
|
|
"path": "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000\\Personal",
|
|
}
|
|
|
|
// We'll use the examples above to create rows with minor variations, similar to what
|
|
// we would get from a real Windows host.
|
|
c3 := maps.Clone(c1)
|
|
c3["username"] = "SYSTEM"
|
|
c3["path"] = "Users\\S-1-5-18\\Personal"
|
|
|
|
c4 := maps.Clone(c1)
|
|
c4["username"] = "SYSTEM"
|
|
c4["path"] = "CurrentUser\\Personal"
|
|
|
|
c5 := maps.Clone(c1)
|
|
c5["username"] = "SYSTEM"
|
|
c5["path"] = "Users\\S-1-5-18\\Personal"
|
|
|
|
c6 := maps.Clone(c1)
|
|
c6["path"] = "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000_Classes\\Personal"
|
|
|
|
c7 := maps.Clone(c2)
|
|
c7["path"] = "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000_Classes\\Personal"
|
|
|
|
rows := []map[string]string{c1, c2, c3, c4, c5, c6, c7}
|
|
|
|
a.certificatesMutex.Lock()
|
|
a.certificatesCache = rows
|
|
a.certificatesMutex.Unlock()
|
|
return rows
|
|
}
|
|
|
|
func (a *agent) orbitInfo() []map[string]string {
|
|
version := "1.22.0"
|
|
desktopVersion := version
|
|
if a.disableFleetDesktop {
|
|
desktopVersion = ""
|
|
}
|
|
deviceAuthToken := ""
|
|
if a.deviceAuthToken != nil {
|
|
deviceAuthToken = *a.deviceAuthToken
|
|
}
|
|
return []map[string]string{
|
|
{
|
|
"version": version,
|
|
"device_auth_token": deviceAuthToken,
|
|
"enrolled": "true",
|
|
"last_recorded_error": "",
|
|
"orbit_channel": "stable",
|
|
"osqueryd_channel": "stable",
|
|
"desktop_channel": "stable",
|
|
"desktop_version": desktopVersion,
|
|
"uptime": "10000",
|
|
"scripts_enabled": "1",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (a *agent) runLiveQuery(query string) (results []map[string]string, status *fleet.OsqueryStatus, message *string, stats *fleet.Stats) {
|
|
if a.liveQueryFailProb > 0.0 && rand.Float64() <= a.liveQueryFailProb {
|
|
ss := fleet.OsqueryStatus(1)
|
|
return []map[string]string{}, &ss, ptr.String("live query failed with error foobar"), nil
|
|
}
|
|
if a.liveQueryNoResultsProb > 0.0 && rand.Float64() <= a.liveQueryNoResultsProb {
|
|
ss := fleet.OsqueryStatus(0)
|
|
return []map[string]string{}, &ss, nil, nil
|
|
}
|
|
|
|
// Switch based on contents of the query.
|
|
lcQuery := strings.ToLower(query)
|
|
switch {
|
|
case strings.Contains(lcQuery, "from yara") && strings.Contains(lcQuery, "sigurl"):
|
|
return a.runLiveYaraQuery(query)
|
|
default:
|
|
return a.runLiveMockQuery(query)
|
|
}
|
|
}
|
|
|
|
func (a *agent) runLiveYaraQuery(query string) (results []map[string]string, status *fleet.OsqueryStatus, message *string, stats *fleet.Stats) {
|
|
// Get the URL of the YARA rule to request (i.e. the sigurl).
|
|
urlRegex := regexp.MustCompile(`sigurl=(["'])([^"']*)["']`)
|
|
matches := urlRegex.FindStringSubmatch(query)
|
|
var url string
|
|
if len(matches) > 2 {
|
|
url = matches[2]
|
|
} else {
|
|
ss := fleet.OsqueryStatus(1)
|
|
return []map[string]string{}, &ss, ptr.String("live yara query failed because a valid sigurl could not be found"), nil
|
|
}
|
|
|
|
// Osquery validates that the sigurl is one of a configured set, so that it's not
|
|
// sending requests to just anywhere. We'll check that it's at least the same host
|
|
// as the Fleet server.
|
|
if !strings.HasPrefix(url, a.serverAddress) {
|
|
ss := fleet.OsqueryStatus(1)
|
|
return []map[string]string{}, &ss, ptr.String("live yara query failed because sigurl host did not match server address"), nil
|
|
}
|
|
|
|
// Make the request.
|
|
body := []byte(`{"node_key": "` + a.nodeKey + `"}`)
|
|
request, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
|
if err != nil {
|
|
ss := fleet.OsqueryStatus(1)
|
|
return []map[string]string{}, &ss, ptr.String("live yara query failed due to error creating request"), nil
|
|
}
|
|
request.Header.Add("Content-type", "application/json")
|
|
|
|
// Make the request.
|
|
response, err := http.DefaultClient.Do(a.sign(request))
|
|
if err != nil {
|
|
ss := fleet.OsqueryStatus(1)
|
|
return []map[string]string{}, &ss, ptr.String(fmt.Sprintf("yara request failed to run: %v", err)), nil
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// For load testing purposes we don't actually care about the response, but check that we at least got one.
|
|
if _, err := io.Copy(io.Discard, response.Body); err != nil {
|
|
ss := fleet.OsqueryStatus(1)
|
|
return []map[string]string{}, &ss, ptr.String(fmt.Sprintf("error reading response from yara API: %v", err)), nil
|
|
}
|
|
|
|
// Return a response indicating that the file is clean.
|
|
ss := fleet.OsqueryStatus(0)
|
|
return []map[string]string{
|
|
{
|
|
"count": "0",
|
|
"matches": "",
|
|
"strings": "",
|
|
"tags": "",
|
|
"sig_group": "",
|
|
"sigfile": "",
|
|
"sigrule": "",
|
|
"sigurl": url,
|
|
// Could pull this from the query, but not necessary for load testing.
|
|
"path": "/some/path",
|
|
},
|
|
}, &ss, nil, &fleet.Stats{
|
|
WallTimeMs: uint64(rand.Intn(1000) * 1000),
|
|
UserTime: uint64(rand.Intn(1000)),
|
|
SystemTime: uint64(rand.Intn(1000)),
|
|
Memory: uint64(rand.Intn(1000)),
|
|
}
|
|
}
|
|
|
|
func (a *agent) runLiveMockQuery(query string) (results []map[string]string, status *fleet.OsqueryStatus, message *string, stats *fleet.Stats) {
|
|
ss := fleet.OsqueryStatus(0)
|
|
return []map[string]string{
|
|
{
|
|
"admindir": "/var/lib/dpkg",
|
|
"arch": "amd64",
|
|
"maintainer": "foobar",
|
|
"name": "netconf",
|
|
"priority": "optional",
|
|
"revision": "",
|
|
"section": "default",
|
|
"size": "112594",
|
|
"source": "",
|
|
"status": "install ok installed",
|
|
"version": "20230224000000",
|
|
},
|
|
}, &ss, nil, &fleet.Stats{
|
|
WallTimeMs: uint64(rand.Intn(1000) * 1000),
|
|
UserTime: uint64(rand.Intn(1000)),
|
|
SystemTime: uint64(rand.Intn(1000)),
|
|
Memory: uint64(rand.Intn(1000)),
|
|
}
|
|
}
|
|
|
|
func (a *agent) processQuery(name, query string, cachedResults *cachedResults) (
|
|
handled bool, results []map[string]string,
|
|
status *fleet.OsqueryStatus, message *string, stats *fleet.Stats,
|
|
) {
|
|
const (
|
|
hostPolicyQueryPrefix = "fleet_policy_query_"
|
|
hostDetailQueryPrefix = "fleet_detail_query_"
|
|
liveQueryPrefix = "fleet_distributed_query_"
|
|
)
|
|
statusOK := fleet.StatusOK
|
|
statusNotOK := fleet.OsqueryStatus(1)
|
|
results = []map[string]string{} // if a query fails, osquery returns empty results array
|
|
|
|
switch {
|
|
case strings.HasPrefix(name, liveQueryPrefix):
|
|
results, status, message, stats = a.runLiveQuery(query)
|
|
return true, results, status, message, stats
|
|
case strings.HasPrefix(name, hostPolicyQueryPrefix):
|
|
return true, a.runPolicy(query), &statusOK, nil, nil
|
|
case name == hostDetailQueryPrefix+"scheduled_query_stats":
|
|
return true, a.randomQueryStats(), &statusOK, nil, nil
|
|
case name == hostDetailQueryPrefix+"mdm":
|
|
ss := statusOK
|
|
if rand.Intn(10) > 0 { // 90% success
|
|
results = a.mdmMac()
|
|
} else {
|
|
ss = statusNotOK
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"mdm_config_profiles_darwin_with_user", name == hostDetailQueryPrefix+"mdm_config_profiles_darwin":
|
|
ss := statusOK
|
|
if rand.Intn(10) > 0 { // 90% success
|
|
results = a.mdmConfigProfilesMac()
|
|
} else {
|
|
ss = statusNotOK
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"conditional_access_microsoft_device_id":
|
|
ss := statusOK
|
|
if rand.Intn(10) > 0 { // 90% success
|
|
results = a.entraConditionalAccess()
|
|
} else {
|
|
ss = statusNotOK
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"mdm_windows":
|
|
ss := statusOK
|
|
if rand.Intn(10) > 0 { // 90% success
|
|
results = a.mdmWindows()
|
|
} else {
|
|
ss = statusNotOK
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"munki_info":
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.munkiInfo()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"google_chrome_profiles":
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.googleChromeProfiles()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"battery":
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.batteries()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"users":
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.hostUsers()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"software_macos":
|
|
ss := fleet.StatusOK
|
|
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
|
|
ss = fleet.OsqueryStatus(1)
|
|
}
|
|
if ss == fleet.StatusOK {
|
|
results = a.softwareMacOS()
|
|
cachedResults.software = results
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"software_macos_codesign":
|
|
// Given queries run in lexicographic order software_macos already run and
|
|
// cachedResults.software should have its results.
|
|
ss := fleet.StatusOK
|
|
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
|
|
ss = fleet.OsqueryStatus(1)
|
|
}
|
|
if ss == fleet.StatusOK {
|
|
if len(cachedResults.software) > 0 {
|
|
for _, s := range cachedResults.software {
|
|
if s["source"] != "apps" {
|
|
continue
|
|
}
|
|
installedPath := s["installed_path"]
|
|
teamIdentifier := s["name"] // use name to be fixed (more realistic than changing often).
|
|
if len(teamIdentifier) > 10 {
|
|
teamIdentifier = teamIdentifier[:10]
|
|
}
|
|
results = append(results, map[string]string{
|
|
"path": installedPath,
|
|
"team_identifier": teamIdentifier,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"software_windows":
|
|
ss := fleet.StatusOK
|
|
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
|
|
ss = fleet.OsqueryStatus(1)
|
|
}
|
|
if ss == fleet.StatusOK {
|
|
// Use database software 80% of the time if available, otherwise use embedded data
|
|
if softwareDB != nil && len(softwareDB.Windows) > 0 && rand.Float64() < 0.8 { // nolint:gosec,G404 // load testing, not security-sensitive
|
|
// Initialize cached indices on first call, then mutate on subsequent calls
|
|
if a.cachedSoftwareIndices == nil {
|
|
// Select a random count between min-max, then pick that many random indices
|
|
count := softwaredb.RandomSoftwareCount("windows")
|
|
perm := rand.Perm(len(softwareDB.Windows))
|
|
a.cachedSoftwareIndices = make([]uint32, count)
|
|
for i := 0; i < count; i++ {
|
|
a.cachedSoftwareIndices[i] = uint32(perm[i])
|
|
}
|
|
} else {
|
|
a.cachedSoftwareIndices = softwaredb.MaybeMutateSoftware(a.cachedSoftwareIndices, len(softwareDB.Windows))
|
|
}
|
|
results = softwareDB.WindowsToMaps(a.cachedSoftwareIndices)
|
|
} else {
|
|
results = make([]map[string]string, 0, len(windowsSoftware))
|
|
for _, s := range windowsSoftware {
|
|
// Use consistent version based on software name's first character
|
|
baseVersion := s["version"]
|
|
alternateVersion := baseVersion + ".1"
|
|
m := map[string]string{
|
|
"name": s["name"],
|
|
"source": s["source"],
|
|
"version": a.selectSoftwareVersion(s["name"], baseVersion, alternateVersion),
|
|
"upgrade_code": s["upgrade_code"],
|
|
}
|
|
results = append(results, m)
|
|
}
|
|
}
|
|
a.installedSoftware.Range(func(key, value interface{}) bool {
|
|
results = append(results, value.(map[string]string))
|
|
return true
|
|
})
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"software_linux":
|
|
ss := fleet.StatusOK
|
|
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
|
|
ss = fleet.OsqueryStatus(1)
|
|
}
|
|
if ss == fleet.StatusOK {
|
|
switch a.os { //nolint:gocritic // ignore singleCaseSwitch
|
|
case "ubuntu":
|
|
// Use database software 80% of the time if available, otherwise use embedded data
|
|
if softwareDB != nil && len(softwareDB.Ubuntu) > 0 && rand.Float64() < 0.8 { // nolint:gosec,G404 // load testing, not security-sensitive
|
|
// Initialize cached indices on first call, then mutate on subsequent calls
|
|
if a.cachedSoftwareIndices == nil {
|
|
// Select a random count between min-max, then pick that many random indices
|
|
count := softwaredb.RandomSoftwareCount("ubuntu")
|
|
perm := rand.Perm(len(softwareDB.Ubuntu))
|
|
a.cachedSoftwareIndices = make([]uint32, count)
|
|
for i := 0; i < count; i++ {
|
|
a.cachedSoftwareIndices[i] = uint32(perm[i])
|
|
}
|
|
} else {
|
|
a.cachedSoftwareIndices = softwaredb.MaybeMutateSoftware(a.cachedSoftwareIndices, len(softwareDB.Ubuntu))
|
|
}
|
|
results = softwareDB.UbuntuToMaps(a.cachedSoftwareIndices)
|
|
} else {
|
|
results = make([]map[string]string, 0, len(ubuntuSoftware))
|
|
for _, s := range ubuntuSoftware {
|
|
softwareName := s["name"]
|
|
if a.linuxUniqueSoftwareTitle {
|
|
softwareName = fmt.Sprintf("%s-%d-%s", softwareName, a.agentIndex, linuxRandomBuildNumber)
|
|
}
|
|
var version string
|
|
if a.linuxUniqueSoftwareVersion {
|
|
version = fmt.Sprintf("1.2.%d-%s", a.agentIndex, linuxRandomBuildNumber)
|
|
} else {
|
|
// Use consistent version based on software name's first character
|
|
baseVersion := s["version"]
|
|
alternateVersion := baseVersion + ".1"
|
|
version = a.selectSoftwareVersion(softwareName, baseVersion, alternateVersion)
|
|
}
|
|
m := map[string]string{
|
|
"name": softwareName,
|
|
"source": s["source"],
|
|
"version": version,
|
|
}
|
|
results = append(results, m)
|
|
}
|
|
}
|
|
|
|
// Add pre-selected kernels for this agent
|
|
results = append(results, a.linuxKernels...)
|
|
|
|
a.installedSoftware.Range(func(key, value interface{}) bool {
|
|
results = append(results, value.(map[string]string))
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"software_vscode_extensions":
|
|
ss := fleet.StatusOK
|
|
if a.softwareVSCodeExtensionsFailProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsFailProb {
|
|
ss = fleet.OsqueryStatus(1)
|
|
}
|
|
if ss == fleet.StatusOK {
|
|
results = a.softwareVSCodeExtensions()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"disk_space_unix" || name == hostDetailQueryPrefix+"disk_space_windows":
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.diskSpace()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
|
|
case strings.HasPrefix(name, hostDetailQueryPrefix+"disk_encryption_linux"):
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.diskEncryptionLinux()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"disk_encryption_darwin" ||
|
|
name == hostDetailQueryPrefix+"disk_encryption_windows":
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.diskEncryption()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case name == hostDetailQueryPrefix+"kubequery_info" && a.os != "kubequery":
|
|
// Real osquery running on hosts would return no results if it was not
|
|
// running kubequery (due to discovery query). Returning true here so that
|
|
// the caller knows it is handled, will not try to return lorem-ipsum-style
|
|
// results.
|
|
return true, nil, &statusNotOK, nil, nil
|
|
case name == hostDetailQueryPrefix+"orbit_info":
|
|
if a.orbitNodeKey == nil {
|
|
return true, nil, &statusNotOK, nil, nil
|
|
}
|
|
return true, a.orbitInfo(), &statusOK, nil, nil
|
|
case strings.HasPrefix(name, hostDetailQueryPrefix+"certificates_darwin"):
|
|
// NOTE: feels exaggerated to fail osquery 50% of the time but this is how
|
|
// most other osquery queries are handled.
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.certificatesDarwin()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
case strings.HasPrefix(name, hostDetailQueryPrefix+"certificates_windows"):
|
|
// NOTE: feels exaggerated to fail osquery 50% of the time but this is how
|
|
// most other osquery queries are handled.
|
|
ss := fleet.OsqueryStatus(rand.Intn(2))
|
|
if ss == fleet.StatusOK {
|
|
results = a.certificatesWindows()
|
|
}
|
|
return true, results, &ss, nil, nil
|
|
default:
|
|
// Look for results in the template file.
|
|
if t := a.templates.Lookup(name); t == nil {
|
|
return false, nil, nil, nil, nil
|
|
}
|
|
var ni bytes.Buffer
|
|
err := a.templates.ExecuteTemplate(&ni, name, a)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = json.Unmarshal(ni.Bytes(), &results)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return true, results, &statusOK, nil, nil
|
|
}
|
|
}
|
|
|
|
type cachedResults struct {
|
|
software []map[string]string
|
|
}
|
|
|
|
func (a *agent) DistributedWrite(queries map[string]string) error {
|
|
r := service.SubmitDistributedQueryResultsRequest{
|
|
Results: make(fleet.OsqueryDistributedQueryResults),
|
|
Statuses: make(map[string]fleet.OsqueryStatus),
|
|
Messages: make(map[string]string),
|
|
Stats: make(map[string]*fleet.Stats),
|
|
}
|
|
r.NodeKey = a.nodeKey
|
|
|
|
cachedResults := cachedResults{}
|
|
|
|
// Sort queries to be executed by lexicographic name order (for result processing
|
|
// to be more predictable). This aligns to how osquery executes the queries.
|
|
queryNames := make([]string, 0, len(queries))
|
|
for name := range queries {
|
|
queryNames = append(queryNames, name)
|
|
}
|
|
sort.Strings(queryNames)
|
|
|
|
for _, name := range queryNames {
|
|
query := queries[name]
|
|
|
|
handled, results, status, message, stats := a.processQuery(name, query, &cachedResults)
|
|
if !handled {
|
|
// If osquery-perf does not handle the incoming query,
|
|
// always return status OK and the default query result.
|
|
r.Results[name] = defaultQueryResult
|
|
r.Statuses[name] = fleet.StatusOK
|
|
} else {
|
|
if results != nil {
|
|
r.Results[name] = results
|
|
}
|
|
if status != nil {
|
|
r.Statuses[name] = *status
|
|
}
|
|
if message != nil {
|
|
r.Messages[name] = *message
|
|
}
|
|
if stats != nil {
|
|
r.Stats[name] = stats
|
|
}
|
|
}
|
|
}
|
|
body, err := json.Marshal(r)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/distributed/write", bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Header.Add("Content-type", "application/json")
|
|
|
|
response, err := http.DefaultClient.Do(a.sign(request))
|
|
if err != nil {
|
|
return fmt.Errorf("distributed/write request failed to run: %w", err)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
a.stats.IncrementDistributedWrites()
|
|
|
|
statusCode := response.StatusCode
|
|
if statusCode != http.StatusOK {
|
|
a.stats.IncrementDistributedWriteErrors()
|
|
return fmt.Errorf("distributed/write request failed: %d", statusCode)
|
|
}
|
|
|
|
// No need to read the distributed write body
|
|
return nil
|
|
}
|
|
|
|
func scheduledQueryResults(packName, queryName string, numResults int) []byte {
|
|
return []byte(`{
|
|
"snapshot": [` + rows(numResults) + `
|
|
],
|
|
"action": "snapshot",
|
|
"name": "pack/` + packName + `/` + queryName + `",
|
|
"hostIdentifier": "EF9595F0-CE81-493A-9B06-D8A9D2CCB952",
|
|
"calendarTime": "Fri Oct 6 18:13:04 2023 UTC",
|
|
"unixTime": 1696615984,
|
|
"epoch": 0,
|
|
"counter": 0,
|
|
"numerics": false,
|
|
"decorations": {
|
|
"host_uuid": "187c4d56-8e45-1a9d-8513-ac17efd2f0fd",
|
|
"hostname": "osquery-perf"
|
|
}
|
|
}`)
|
|
}
|
|
|
|
func (a *agent) connCheck() error {
|
|
request, err := http.NewRequest("GET", a.serverAddress+"/version", nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
response, err := http.DefaultClient.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusOK {
|
|
return errors.New(http.StatusText(response.StatusCode))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *agent) submitLogs(results []resultLog) error {
|
|
// Connection check to prevent unnecessary JSON marshaling when the server is down.
|
|
if err := a.connCheck(); err != nil {
|
|
return fmt.Errorf("/version check failed: %w", err)
|
|
}
|
|
|
|
var resultLogs []byte
|
|
for i, result := range results {
|
|
if i > 0 {
|
|
resultLogs = append(resultLogs, ',')
|
|
}
|
|
resultLogs = append(resultLogs, result.emit()...)
|
|
}
|
|
|
|
body := []byte(`{"node_key": "` + a.nodeKey + `", "log_type": "result", "data": [` + string(resultLogs) + `]}`)
|
|
request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/log", bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Header.Add("Content-type", "application/json")
|
|
|
|
response, err := http.DefaultClient.Do(a.sign(request))
|
|
if err != nil {
|
|
return fmt.Errorf("log request failed to run: %w", err)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
a.stats.IncrementResultLogRequests()
|
|
|
|
statusCode := response.StatusCode
|
|
if statusCode != http.StatusOK {
|
|
a.stats.IncrementResultLogErrors()
|
|
return fmt.Errorf("log request failed: %d", statusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *mdmAgent) runAppleIDeviceMDMLoop(mdmSCEPChallenge string) {
|
|
udid := mdmtest.RandUDID()
|
|
|
|
mdmClient := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
|
|
SCEPChallenge: mdmSCEPChallenge,
|
|
SCEPURL: a.serverAddress + apple_mdm.SCEPPath,
|
|
MDMURL: a.serverAddress + apple_mdm.MDMPath,
|
|
}, a.model)
|
|
mdmClient.UUID = udid
|
|
mdmClient.SerialNumber = mdmtest.RandSerialNumber()
|
|
deviceName := fmt.Sprintf("%s-%d", a.model, a.agentIndex)
|
|
productName := a.model
|
|
softwareSource := "ios_apps"
|
|
if strings.HasPrefix(a.model, "iPad") {
|
|
softwareSource = "ipados_apps"
|
|
}
|
|
|
|
if err := mdmClient.Enroll(); err != nil {
|
|
log.Printf("%s MDM enroll failed: %s", a.model, err)
|
|
a.stats.IncrementMDMErrors()
|
|
return
|
|
}
|
|
|
|
a.stats.IncrementMDMEnrollments()
|
|
|
|
mdmCheckInTicker := time.Tick(a.MDMCheckInInterval)
|
|
|
|
for range mdmCheckInTicker {
|
|
mdmCommandPayload, err := mdmClient.Idle()
|
|
if err != nil {
|
|
log.Printf("MDM Idle request failed: %s: %s", a.model, err)
|
|
a.stats.IncrementMDMErrors()
|
|
continue
|
|
}
|
|
a.stats.IncrementMDMSessions()
|
|
|
|
for mdmCommandPayload != nil {
|
|
a.stats.IncrementMDMCommandsReceived()
|
|
switch mdmCommandPayload.Command.RequestType {
|
|
case "DeviceInformation":
|
|
mdmCommandPayload, err = mdmClient.AcknowledgeDeviceInformation(udid, mdmCommandPayload.CommandUUID, deviceName,
|
|
productName)
|
|
case "InstalledApplicationList":
|
|
software := a.softwareIOSandIPadOS(softwareSource)
|
|
mdmCommandPayload, err = mdmClient.AcknowledgeInstalledApplicationList(udid, mdmCommandPayload.CommandUUID, software)
|
|
case "InstallProfile":
|
|
if a.mdmProfileFailureProb > 0.0 && rand.Float64() <= a.mdmProfileFailureProb {
|
|
errChain := []mdm.ErrorChain{
|
|
{
|
|
ErrorCode: 89,
|
|
ErrorDomain: "ErrorDomain",
|
|
LocalizedDescription: "The profile did not install",
|
|
},
|
|
}
|
|
mdmCommandPayload, err = mdmClient.Err(mdmCommandPayload.CommandUUID, errChain)
|
|
} else {
|
|
mdmCommandPayload, err = mdmClient.Acknowledge(mdmCommandPayload.CommandUUID)
|
|
}
|
|
|
|
default:
|
|
mdmCommandPayload, err = mdmClient.Acknowledge(mdmCommandPayload.CommandUUID)
|
|
}
|
|
if err != nil {
|
|
log.Printf("MDM Acknowledge request failed: %s: %s", a.model, err)
|
|
a.stats.IncrementMDMErrors()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// rows returns a set of rows for use in tests for query results.
|
|
func rows(num int) string {
|
|
b := strings.Builder{}
|
|
for i := 0; i < num; i++ {
|
|
b.WriteString(` {
|
|
"build_distro": "centos7",
|
|
"build_platform": "linux",
|
|
"config_hash": "eed0d8296e5f90b790a23814a9db7a127b13498d",
|
|
"config_valid": "1",
|
|
"extensions": "active",
|
|
"instance_id": "e5799132-85ab-4cfa-89f3-03e0dd3c509a",
|
|
"pid": "3574",
|
|
"platform_mask": "9",
|
|
"start_time": "1696502961",
|
|
"uuid": "EF9595F0-CE81-493A-9B06-D8A9D2CCB95",
|
|
"version": "5.9.2",
|
|
"watcher": "3570"
|
|
}`)
|
|
if i != num-1 {
|
|
b.WriteString(",")
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func main() {
|
|
// Start HTTP server for pprof. See https://pkg.go.dev/net/http/pprof.
|
|
go func() {
|
|
log.Println(http.ListenAndServe("localhost:6060", nil))
|
|
}()
|
|
|
|
// #nosec (osquery-perf is only used for testing)
|
|
tlsConfig := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
}
|
|
tr := http.DefaultTransport.(*http.Transport).Clone()
|
|
tr.TLSClientConfig = tlsConfig
|
|
http.DefaultClient.Transport = tr
|
|
|
|
validTemplateNames := map[string]bool{
|
|
"macos_13.6.2.tmpl": true,
|
|
"macos_14.1.2.tmpl": true,
|
|
"windows_11.tmpl": true,
|
|
"windows_11_22H2_2861.tmpl": true,
|
|
"windows_11_22H2_3007.tmpl": true,
|
|
"ubuntu_22.04.tmpl": true,
|
|
"iphone_14.6.tmpl": true,
|
|
"ipad_13.18.tmpl": true,
|
|
}
|
|
allowedTemplateNames := make([]string, 0, len(validTemplateNames))
|
|
for k := range validTemplateNames {
|
|
allowedTemplateNames = append(allowedTemplateNames, k)
|
|
}
|
|
|
|
var (
|
|
serverURL = flag.String("server_url", "https://localhost:8080", "URL (with protocol and port of osquery server)")
|
|
enrollSecret = flag.String("enroll_secret", "", "Enroll secret to authenticate enrollment")
|
|
hostCount = flag.Int("host_count", 10, "Number of hosts to start (default 10)")
|
|
totalHostCount = flag.Int("total_host_count", 0, "Total number of hosts across all containers (if 0, uses host_count)")
|
|
hostIndexOffset = flag.Int("host_index_offset", 0, "Starting index offset for this container's hosts (default 0)")
|
|
randSeed = flag.Int64("seed", time.Now().UnixNano(), "Seed for random generator (default current time)")
|
|
startPeriod = flag.Duration("start_period", 10*time.Second, "Duration to spread start of hosts over")
|
|
configInterval = flag.Duration("config_interval", 1*time.Minute, "Interval for config requests")
|
|
// Flag logger_tls_period defines how often to check for sending scheduled query results.
|
|
// osquery-perf will send log requests with results only if there are scheduled queries configured AND it's their time to run.
|
|
logInterval = flag.Duration("logger_tls_period", 10*time.Second, "Interval for scheduled queries log requests")
|
|
queryInterval = flag.Duration("query_interval", 10*time.Second, "Interval for distributed query requests")
|
|
mdmCheckInInterval = flag.Duration("mdm_check_in_interval", 1*time.Minute, "Interval for performing MDM check-ins (applies to both macOS and Windows)")
|
|
onlyAlreadyEnrolled = flag.Bool("only_already_enrolled", false, "Only start agents that are already enrolled")
|
|
nodeKeyFile = flag.String("node_key_file", "", "File with node keys to use")
|
|
|
|
httpMessageSignatureProb = flag.Float64("http_message_signature_prob", 0.1, "Probability of hosts using HTTP message signatures")
|
|
httpMessageSignatureP384Prob = flag.Float64("http_message_signature_p384_prob", 0.5,
|
|
"Probability of hosts using P384 elliptic curve (as opposed to P256) for HTTP message signatures")
|
|
|
|
// 50% failure probability is not realistic but this is our current baseline for the osquery-perf setup.
|
|
// We tried setting this to a more realistic value like 5% but it overloaded the MySQL Writer instance
|
|
// during hosts enroll.
|
|
softwareQueryFailureProb = flag.Float64("software_query_fail_prob", 0.5, "Probability of the software query failing")
|
|
softwareVSCodeExtensionsQueryFailureProb = flag.Float64("software_vscode_extensions_query_fail_prob", 0.0, "Probability of the software vscode_extensions query failing")
|
|
|
|
softwareInstallerPreInstallFailureProb = flag.Float64("software_installer_pre_install_fail_prob", 0.05,
|
|
"Probability of the pre-install query failing")
|
|
softwareInstallerInstallFailureProb = flag.Float64("software_installer_install_fail_prob", 0.05,
|
|
"Probability of the install script failing")
|
|
softwareInstallerPostInstallFailureProb = flag.Float64("software_installer_post_install_fail_prob", 0.05,
|
|
"Probability of the post-install script failing")
|
|
|
|
commonSoftwareCount = flag.Int("common_software_count", 10, "Number of common installed applications reported to fleet")
|
|
commonVSCodeExtensionsSoftwareCount = flag.Int("common_vscode_extensions_software_count", 5, "Number of common vscode_extensions installed applications reported to fleet")
|
|
commonSoftwareUninstallCount = flag.Int("common_software_uninstall_count", 1, "Number of common software to uninstall")
|
|
commonVSCodeExtensionsSoftwareUninstallCount = flag.Int("common_vscode_extensions_software_uninstall_count", 1, "Number of common vscode_extensions software to uninstall")
|
|
commonSoftwareUninstallProb = flag.Float64("common_software_uninstall_prob", 0.1, "Probability of uninstalling common_software_uninstall_count unique software/s")
|
|
commonVSCodeExtensionsSoftwareUninstallProb = flag.Float64("common_vscode_extensions_software_uninstall_prob", 0.1, "Probability of uninstalling vscode_extensions common_software_uninstall_count unique software/s")
|
|
|
|
uniqueSoftwareCount = flag.Int("unique_software_count", 1, "Number of unique software installed on each host")
|
|
uniqueVSCodeExtensionsSoftwareCount = flag.Int("unique_vscode_extensions_software_count", 1, "Number of unique vscode_extensions software installed on each host")
|
|
uniqueSoftwareUninstallCount = flag.Int("unique_software_uninstall_count", 1, "Number of unique software to uninstall")
|
|
uniqueVSCodeExtensionsSoftwareUninstallCount = flag.Int("unique_vscode_extensions_software_uninstall_count", 1, "Number of unique vscode_extensions software to uninstall")
|
|
uniqueSoftwareUninstallProb = flag.Float64("unique_software_uninstall_prob", 0.1, "Probability of uninstalling unique_software_uninstall_count common software/s")
|
|
uniqueVSCodeExtensionsSoftwareUninstallProb = flag.Float64("unique_vscode_extensions_software_uninstall_prob", 0.1, "Probability of uninstalling unique_vscode_extensions_software_uninstall_count common software/s")
|
|
|
|
duplicateBundleIdentifiersPercent = flag.Int("duplicate_bundle_identifiers_percent", 0, "Percentage of software with duplicate bundle identifiers (0-100)")
|
|
softwareRenaming = flag.Bool("software_renaming", false, "Enable software renaming for duplicate bundle identifiers")
|
|
// WARNING: This will generate massive amounts of entries in the software table,
|
|
// because linux devices report many individual software items, ~1600, compared to Windows around ~100s or macOS around ~500s.
|
|
//
|
|
// This flag can be used to load test software ingestion for Linux during enrollment (during enrollment all devices
|
|
// report software to Fleet, so the initial reads/inserts can be expensive).
|
|
linuxUniqueSoftwareVersion = flag.Bool("linux_unique_software_version", false, "Make version of software items on linux hosts unique. WARNING: This will generate massive amounts of entries in the software table, because linux devices report many individual software items (compared to Windows/macOS).")
|
|
// WARNING: This will generate massive amounts of entries in the software and software_titles tables,
|
|
//
|
|
// This flag can be used to load test software ingestion for Linux during enrollment (during enrollment all devices
|
|
// report software to Fleet, so the initial reads/inserts can be expensive).
|
|
linuxUniqueSoftwareTitle = flag.Bool("linux_unique_software_title", false, "Make name of software items on linux hosts unique. WARNING: This will generate massive amounts of titles which is not realistic but serves to test performance of software ingestion when processing large number of titles.")
|
|
|
|
// This flag can be used to set the number of vulnerable software items reported by each host picked randomly from the
|
|
// list of vulnerable software. Use -1 to load all vulnerable software.
|
|
vulnerableSoftwareCount = flag.Int("vulnerable_software_count", 10, "Number of vulnerable installed applications reported to fleet. Use -1 to load all vulnerable software.")
|
|
withLastOpenedSoftwareCount = flag.Int("with_last_opened_software_count", 10, "Number of applications that may report a last opened timestamp to fleet")
|
|
lastOpenedChangeProb = flag.Float64("last_opened_change_prob", 0.1, "Probability of last opened timestamp to be reported as changed [0, 1]")
|
|
commonUserCount = flag.Int("common_user_count", 10, "Number of common host users reported to fleet")
|
|
uniqueUserCount = flag.Int("unique_user_count", 10, "Number of unique host users reported to fleet")
|
|
policyPassProb = flag.Float64("policy_pass_prob", 1.0, "Probability of policies to pass [0, 1]")
|
|
orbitProb = flag.Float64("orbit_prob", 0.5, "Probability of a host being identified as orbit install [0, 1]")
|
|
munkiIssueProb = flag.Float64("munki_issue_prob", 0.5, "Probability of a host having munki issues (note that ~50% of hosts have munki installed) [0, 1]")
|
|
munkiIssueCount = flag.Int("munki_issue_count", 10, "Number of munki issues reported by hosts identified to have munki issues")
|
|
// E.g. when running with `-host_count=10`, you can set host count for each template the following way:
|
|
// `-os_templates=windows_11.tmpl:3,macos_14.1.2.tmpl:4,ubuntu_22.04.tmpl:3`
|
|
osTemplates = flag.String("os_templates", "macos_14.1.2", fmt.Sprintf("Comma-separated list of host OS templates to use and optionally their host count separated by ':' (any of %v, with or without the .tmpl extension)", allowedTemplateNames))
|
|
emptySerialProb = flag.Float64("empty_serial_prob", 0.1, "Probability of a host having no serial number [0, 1]")
|
|
defaultSerialProb = flag.Float64("default_serial_prob", 0.05,
|
|
"Probability of osquery returning a default (-1) serial number. See: #19789")
|
|
|
|
mdmProb = flag.Float64("mdm_prob", 0.0, "Probability of a host enrolling via Fleet MDM (applies for macOS and Windows hosts, implies orbit enrollment on Windows) [0, 1]")
|
|
mdmSCEPChallenge = flag.String("mdm_scep_challenge", "", "SCEP challenge to use when running macOS MDM enroll")
|
|
mdmProfileFailureProb = flag.Float64("mdm_profile_failure_prob", 0.0, "Probability of an MDM profile to fail install [0, 1]")
|
|
|
|
liveQueryFailProb = flag.Float64("live_query_fail_prob", 0.0, "Probability of a live query failing execution in the host")
|
|
liveQueryNoResultsProb = flag.Float64("live_query_no_results_prob", 0.2, "Probability of a live query returning no results")
|
|
|
|
disableScriptExec = flag.Bool("disable_script_exec", false, "Disable script execution support")
|
|
|
|
disableFleetDesktop = flag.Bool("disable_fleet_desktop", false, "Disable Fleet Desktop")
|
|
// logger_tls_max_lines is simulating the osquery setting with the same name.
|
|
loggerTLSMaxLines = flag.Int("logger_tls_max_lines", 1024,
|
|
"Maximum number of buffered result log lines to send on every log request")
|
|
commonSoftwareNameSuffix = flag.String("common_software_name_suffix", "", "Suffix to add to generated common software names")
|
|
softwareDatabasePath = flag.String("software_db_path", "software-library/software.db",
|
|
"Path to software.db (SQLite database with realistic software data). Auto-generates from software.sql if missing.")
|
|
)
|
|
|
|
flag.Parse()
|
|
rand.Seed(*randSeed)
|
|
|
|
// Load software from database if path provided
|
|
if *softwareDatabasePath != "" {
|
|
db, err := softwaredb.LoadFromDatabase(*softwareDatabasePath)
|
|
if err != nil {
|
|
log.Fatalf("Failed to load software database: %v", err)
|
|
}
|
|
softwareDB = db
|
|
} else {
|
|
log.Println("No software database specified (--software_db_path). Using embedded software data.")
|
|
}
|
|
|
|
// There are two modes for osquery-perf:
|
|
// 1. Non distributed mode (old behavior). All agents get all software specified. This is done when specifying --host_count and --common_software_count
|
|
// Example --host_count 500 --common_software_count 1000 -> means 500 hosts each with 1000 pieces of software
|
|
// 2. Distributed mode. All agents get a subset of the total software specified. This is done when specifying --total_host_count and --host_index_offset along with other params.
|
|
// Example --host_count 500 --common_software_count 1000 --total_host_count 5000 --host_index_offset [0...N...1000]
|
|
// This example means that each container will run 500 hosts, but each host will only get a subset of the total 5000 software requested.
|
|
if *totalHostCount > 0 && *totalHostCount > *hostCount {
|
|
log.Printf("WARNING: total_host_count (%d) > host_count (%d). You are trying to use distributed mode, ensure you have --host_index_offset specified for each container", *totalHostCount, *hostCount)
|
|
log.Printf(" Container 0 should use: --host_index_offset 0")
|
|
log.Printf(" Container 1 should use: --host_index_offset %d", *hostCount)
|
|
log.Printf(" Container 2 should use: --host_index_offset %d", *hostCount*2)
|
|
log.Printf(" Container N should use: --host_index_offset Y")
|
|
}
|
|
|
|
if *onlyAlreadyEnrolled {
|
|
// Orbit enrollment does not support the "already enrolled" mode at the
|
|
// moment (see TODO in this file).
|
|
*orbitProb = 0
|
|
}
|
|
|
|
if *commonSoftwareUninstallCount > *commonSoftwareCount {
|
|
log.Fatalf("Argument common_software_uninstall_count cannot be bigger than common_software_count")
|
|
}
|
|
if *uniqueSoftwareUninstallCount > *uniqueSoftwareCount {
|
|
log.Fatalf("Argument unique_software_uninstall_count cannot be bigger than unique_software_count")
|
|
}
|
|
|
|
tmplsm := make(map[*template.Template]int)
|
|
requestedTemplates := strings.Split(*osTemplates, ",")
|
|
tmplsTotalHostCount := 0
|
|
for _, nm := range requestedTemplates {
|
|
numberOfHosts := 0
|
|
if strings.Contains(nm, ":") {
|
|
parts := strings.Split(nm, ":")
|
|
nm = parts[0]
|
|
hc, err := strconv.ParseInt(parts[1], 10, 64)
|
|
if err != nil {
|
|
log.Fatalf("Invalid template host count: %s", parts[1])
|
|
}
|
|
numberOfHosts = int(hc)
|
|
}
|
|
if !strings.HasSuffix(nm, ".tmpl") {
|
|
nm += ".tmpl"
|
|
}
|
|
if !validTemplateNames[nm] {
|
|
log.Fatalf("Invalid template name: %s (accepted values: %v)", nm, allowedTemplateNames)
|
|
}
|
|
|
|
tmpl, err := template.ParseFS(templatesFS, nm)
|
|
if err != nil {
|
|
log.Fatal("parse templates: ", err)
|
|
}
|
|
tmplsm[tmpl] = numberOfHosts
|
|
tmplsTotalHostCount += numberOfHosts
|
|
}
|
|
if tmplsTotalHostCount != 0 && tmplsTotalHostCount != *hostCount {
|
|
log.Fatalf("Invalid host count in templates: total=%d vs host_count=%d", tmplsTotalHostCount, *hostCount)
|
|
}
|
|
|
|
// Spread starts over the interval to prevent thundering herd
|
|
sleepTime := *startPeriod / time.Duration(*hostCount)
|
|
|
|
stats := &osquery_perf.Stats{
|
|
StartTime: time.Now(),
|
|
}
|
|
go stats.RunLoop()
|
|
installerMetadataCache.Stats = stats
|
|
|
|
nodeKeyManager := &nodeKeyManager{}
|
|
if nodeKeyFile != nil {
|
|
nodeKeyManager.filepath = *nodeKeyFile
|
|
nodeKeyManager.LoadKeys()
|
|
}
|
|
|
|
var tmplss []*template.Template
|
|
for tmpl := range tmplsm {
|
|
tmplss = append(tmplss, tmpl)
|
|
}
|
|
|
|
for i := 0; i < *hostCount; i++ {
|
|
var tmpl *template.Template
|
|
if tmplsTotalHostCount > 0 {
|
|
for tmpl_, hostCount := range tmplsm {
|
|
if hostCount > 0 {
|
|
tmpl = tmpl_
|
|
tmplsm[tmpl_]--
|
|
break
|
|
}
|
|
}
|
|
if tmpl == nil {
|
|
log.Fatalf("Failed to determine template for host: %d", i)
|
|
}
|
|
} else {
|
|
tmpl = tmplss[i%len(tmplss)]
|
|
}
|
|
|
|
if tmpl.Name() == "iphone_14.6.tmpl" || tmpl.Name() == "ipad_13.18.tmpl" {
|
|
model := "iPhone 14,6"
|
|
if tmpl.Name() == "ipad_13.18.tmpl" {
|
|
model = "iPad 13,18"
|
|
}
|
|
mobileDevice := mdmAgent{
|
|
agentIndex: i + 1,
|
|
MDMCheckInInterval: *mdmCheckInInterval,
|
|
model: model,
|
|
serverAddress: *serverURL,
|
|
softwareCount: softwareEntityCount{
|
|
entityCount: entityCount{
|
|
common: *commonSoftwareCount,
|
|
unique: *uniqueSoftwareCount,
|
|
},
|
|
vulnerable: *vulnerableSoftwareCount,
|
|
commonSoftwareUninstallCount: *commonSoftwareUninstallCount,
|
|
commonSoftwareUninstallProb: *commonSoftwareUninstallProb,
|
|
uniqueSoftwareUninstallCount: *uniqueSoftwareUninstallCount,
|
|
uniqueSoftwareUninstallProb: *uniqueSoftwareUninstallProb,
|
|
},
|
|
stats: stats,
|
|
strings: make(map[string]string),
|
|
softwareVersionMap: make(map[rune]int),
|
|
mdmProfileFailureProb: *mdmProfileFailureProb,
|
|
}
|
|
go mobileDevice.runAppleIDeviceMDMLoop(*mdmSCEPChallenge)
|
|
time.Sleep(sleepTime)
|
|
continue
|
|
}
|
|
|
|
a := newAgent(i+1,
|
|
*hostCount,
|
|
*totalHostCount,
|
|
*hostIndexOffset,
|
|
*serverURL,
|
|
*enrollSecret,
|
|
tmpl,
|
|
*configInterval,
|
|
*logInterval,
|
|
*queryInterval,
|
|
*mdmCheckInInterval,
|
|
*softwareQueryFailureProb,
|
|
*softwareVSCodeExtensionsQueryFailureProb,
|
|
softwareInstaller{
|
|
preInstallFailureProb: *softwareInstallerPreInstallFailureProb,
|
|
installFailureProb: *softwareInstallerInstallFailureProb,
|
|
postInstallFailureProb: *softwareInstallerPostInstallFailureProb,
|
|
mu: new(sync.Mutex),
|
|
},
|
|
softwareEntityCount{
|
|
entityCount: entityCount{
|
|
common: *commonSoftwareCount,
|
|
unique: *uniqueSoftwareCount,
|
|
},
|
|
vulnerable: *vulnerableSoftwareCount,
|
|
withLastOpened: *withLastOpenedSoftwareCount,
|
|
lastOpenedProb: *lastOpenedChangeProb,
|
|
commonSoftwareUninstallCount: *commonSoftwareUninstallCount,
|
|
commonSoftwareUninstallProb: *commonSoftwareUninstallProb,
|
|
uniqueSoftwareUninstallCount: *uniqueSoftwareUninstallCount,
|
|
uniqueSoftwareUninstallProb: *uniqueSoftwareUninstallProb,
|
|
duplicateBundleIdentifiersPercent: *duplicateBundleIdentifiersPercent,
|
|
softwareRenaming: *softwareRenaming,
|
|
},
|
|
softwareExtraEntityCount{
|
|
entityCount: entityCount{
|
|
common: *commonVSCodeExtensionsSoftwareCount,
|
|
unique: *uniqueVSCodeExtensionsSoftwareCount,
|
|
},
|
|
commonSoftwareUninstallCount: *commonVSCodeExtensionsSoftwareUninstallCount,
|
|
commonSoftwareUninstallProb: *commonVSCodeExtensionsSoftwareUninstallProb,
|
|
uniqueSoftwareUninstallCount: *uniqueVSCodeExtensionsSoftwareUninstallCount,
|
|
uniqueSoftwareUninstallProb: *uniqueVSCodeExtensionsSoftwareUninstallProb,
|
|
},
|
|
entityCount{
|
|
common: *commonUserCount,
|
|
unique: *uniqueUserCount,
|
|
},
|
|
*policyPassProb,
|
|
*orbitProb,
|
|
*munkiIssueProb,
|
|
*munkiIssueCount,
|
|
*emptySerialProb,
|
|
*defaultSerialProb,
|
|
*mdmProb,
|
|
*mdmSCEPChallenge,
|
|
*liveQueryFailProb,
|
|
*liveQueryNoResultsProb,
|
|
*disableScriptExec,
|
|
*disableFleetDesktop,
|
|
*loggerTLSMaxLines,
|
|
*linuxUniqueSoftwareVersion,
|
|
*linuxUniqueSoftwareTitle,
|
|
*commonSoftwareNameSuffix,
|
|
*mdmProfileFailureProb,
|
|
*httpMessageSignatureProb,
|
|
*httpMessageSignatureP384Prob,
|
|
)
|
|
a.stats = stats
|
|
a.nodeKeyManager = nodeKeyManager
|
|
go a.runLoop(i, *onlyAlreadyEnrolled)
|
|
time.Sleep(sleepTime)
|
|
}
|
|
|
|
log.Println("Agents running. Kill with C-c.")
|
|
<-make(chan struct{})
|
|
}
|