2021-09-22 20:18:55 +00:00
package main
import (
"bytes"
2022-06-23 20:44:45 +00:00
"compress/bzip2"
2023-08-23 22:31:47 +00:00
cryptorand "crypto/rand"
2025-03-11 15:20:49 +00:00
"crypto/sha1" // nolint:gosec
2026-01-17 01:52:02 +00:00
"crypto/sha256"
2021-09-22 20:18:55 +00:00
"crypto/tls"
2021-10-14 13:09:58 +00:00
"embed"
2023-08-23 22:31:47 +00:00
"encoding/base64"
2025-03-11 15:20:49 +00:00
"encoding/hex"
2021-09-22 20:18:55 +00:00
"encoding/json"
2024-03-13 13:29:25 +00:00
"encoding/xml"
2021-11-24 20:56:54 +00:00
"errors"
2021-09-22 20:18:55 +00:00
"flag"
"fmt"
2023-08-23 22:31:47 +00:00
"io"
2021-09-22 20:18:55 +00:00
"log"
2025-12-18 15:48:21 +00:00
"maps"
2021-09-22 20:18:55 +00:00
"math/rand"
"net/http"
2024-02-12 22:06:58 +00:00
_ "net/http/pprof"
2021-10-14 13:09:58 +00:00
"os"
2025-01-09 16:00:22 +00:00
"regexp"
2024-11-15 19:30:39 +00:00
"sort"
2022-06-28 18:11:49 +00:00
"strconv"
2021-09-22 20:18:55 +00:00
"strings"
2021-10-14 13:09:58 +00:00
"sync"
2023-08-23 22:31:47 +00:00
"sync/atomic"
2021-09-22 20:18:55 +00:00
"text/template"
"time"
2025-07-18 13:19:05 +00:00
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/hostidentity"
2024-08-21 14:08:16 +00:00
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/installer_cache"
2025-01-29 16:24:44 +00:00
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/osquery_perf"
2025-11-21 16:42:19 +00:00
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/softwaredb"
2024-08-21 14:08:16 +00:00
"github.com/fleetdm/fleet/v4/pkg/file"
2023-05-12 16:50:20 +00:00
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
2021-11-01 18:23:31 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2023-05-12 16:50:20 +00:00
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
2024-03-13 13:29:25 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
2024-04-15 20:56:25 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
2022-05-31 13:15:58 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
2021-11-01 18:23:31 +00:00
"github.com/fleetdm/fleet/v4/server/service"
2025-07-16 18:08:27 +00:00
"github.com/fleetdm/fleet/v4/server/service/contract"
2021-09-22 20:18:55 +00:00
"github.com/google/uuid"
2025-07-24 12:26:03 +00:00
"github.com/micromdm/plist"
2025-07-18 13:19:05 +00:00
"github.com/remitly-oss/httpsig-go"
2021-09-22 20:18:55 +00:00
)
2023-07-14 16:06:34 +00:00
var (
//go:embed *.tmpl
templatesFS embed . FS
2021-10-14 13:09:58 +00:00
2025-03-03 20:20:28 +00:00
//go:embed macos_vulnerable-software.json.bz2
2023-07-14 16:06:34 +00:00
macOSVulnerableSoftwareFS embed . FS
2022-01-28 13:05:11 +00:00
2024-03-14 19:33:12 +00:00
//go:embed vscode_extensions_vulnerable.software
vsCodeExtensionsVulnerableSoftwareFS embed . FS
2023-07-14 16:06:34 +00:00
//go:embed ubuntu_2204-software.json.bz2
ubuntuSoftwareFS embed . FS
2025-11-03 19:07:25 +00:00
//go:embed ubuntu_2204-kernels.json
ubuntuKernelsFS embed . FS
2023-07-14 16:06:34 +00:00
//go:embed windows_11-software.json.bz2
windowsSoftwareFS embed . FS
2022-01-28 13:05:11 +00:00
2024-03-14 19:33:12 +00:00
macosVulnerableSoftware [ ] fleet . Software
vsCodeExtensionsVulnerableSoftware [ ] fleet . Software
windowsSoftware [ ] map [ string ] string
ubuntuSoftware [ ] map [ string ] string
2025-11-03 19:07:25 +00:00
ubuntuKernels [ ] string
2024-08-21 14:08:16 +00:00
2025-11-21 16:42:19 +00:00
// Software library database (loaded from SQLite if --software_db_path is specified)
softwareDB * softwaredb . DB
2024-08-21 14:08:16 +00:00
installerMetadataCache installer_cache . Metadata
2025-01-23 18:48:21 +00:00
linuxRandomBuildNumber = randomString ( 8 )
2023-07-14 16:06:34 +00:00
)
func loadMacOSVulnerableSoftware ( ) {
2025-03-03 20:20:28 +00:00
bz2 , err := macOSVulnerableSoftwareFS . Open ( "macos_vulnerable-software.json.bz2" )
2022-01-28 13:05:11 +00:00
if err != nil {
2025-03-03 20:20:28 +00:00
log . Fatal ( "open vulnerable macOS software file: " , err )
2022-01-28 13:05:11 +00:00
}
2025-03-03 20:20:28 +00:00
type vulnerableSoftware struct {
Software [ ] fleet . Software ` json:"software" `
2022-01-28 13:05:11 +00:00
}
2025-03-03 20:20:28 +00:00
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
2024-02-08 15:40:13 +00:00
log . Printf ( "Loaded %d vulnerable macOS software" , len ( macosVulnerableSoftware ) )
2023-07-14 16:06:34 +00:00
}
2024-03-14 19:33:12 +00:00
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 ) )
}
2025-01-23 18:48:21 +00:00
func loadSoftwareItems ( fs embed . FS , path string , source string ) [ ] map [ string ] string {
2023-07-14 16:06:34 +00:00
bz2 , err := fs . Open ( path )
if err != nil {
panic ( err )
}
type softwareJSON struct {
2025-11-07 23:33:31 +00:00
Name string ` json:"name" `
Version string ` json:"version" `
UpgradeCode string ` json:"upgrade_code" `
Release string ` json:"release,omitempty" `
Arch string ` json:"arch,omitempty" `
2023-07-14 16:06:34 +00:00
}
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 {
2025-11-07 23:33:31 +00:00
"name" : s . Name ,
"version" : s . Version ,
"source" : source ,
"upgrade_code" : s . UpgradeCode ,
2023-07-14 16:06:34 +00:00
} )
}
return softwareRows
}
2025-11-03 19:07:25 +00:00
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
}
2023-07-14 16:06:34 +00:00
func init ( ) {
loadMacOSVulnerableSoftware ( )
2024-03-14 19:33:12 +00:00
loadExtraVulnerableSoftware ( )
2025-01-23 18:48:21 +00:00
windowsSoftware = loadSoftwareItems ( windowsSoftwareFS , "windows_11-software.json.bz2" , "programs" )
ubuntuSoftware = loadSoftwareItems ( ubuntuSoftwareFS , "ubuntu_2204-software.json.bz2" , "deb_packages" )
2025-11-03 19:07:25 +00:00
ubuntuKernels = loadKernelList ( ubuntuKernelsFS , "ubuntu_2204-kernels.json" )
2022-01-28 13:05:11 +00:00
}
2021-11-01 18:23:31 +00:00
type nodeKeyManager struct {
2021-10-14 13:09:58 +00:00
filepath string
l sync . Mutex
nodekeys [ ] string
}
2021-11-01 18:23:31 +00:00
func ( n * nodeKeyManager ) LoadKeys ( ) {
2021-10-14 13:09:58 +00:00
if n . filepath == "" {
return
}
n . l . Lock ( )
defer n . l . Unlock ( )
data , err := os . ReadFile ( n . filepath )
if err != nil {
2024-02-08 15:40:13 +00:00
log . Println ( "WARNING (ignore if creating a new node key file): error loading nodekey file:" , err )
2021-10-14 13:09:58 +00:00
return
}
n . nodekeys = strings . Split ( string ( data ) , "\n" )
2021-12-09 21:05:32 +00:00
n . nodekeys = n . nodekeys [ : len ( n . nodekeys ) - 1 ] // remove last empty node key due to new line.
2024-02-08 15:40:13 +00:00
log . Printf ( "loaded %d node keys" , len ( n . nodekeys ) )
2021-10-14 13:09:58 +00:00
}
2021-11-01 18:23:31 +00:00
func ( n * nodeKeyManager ) Get ( i int ) string {
2021-10-14 13:09:58 +00:00
n . l . Lock ( )
defer n . l . Unlock ( )
if len ( n . nodekeys ) > i {
return n . nodekeys [ i ]
}
return ""
}
2021-11-01 18:23:31 +00:00
func ( n * nodeKeyManager ) Add ( nodekey string ) {
2021-10-14 13:09:58 +00:00
if n . filepath == "" {
return
}
// we lock just to make sure we write one at a time
n . l . Lock ( )
defer n . l . Unlock ( )
2022-05-31 13:15:58 +00:00
f , err := os . OpenFile ( n . filepath , os . O_APPEND | os . O_CREATE | os . O_WRONLY , 0 o600 )
2021-10-14 13:09:58 +00:00
if err != nil {
2024-02-08 15:40:13 +00:00
log . Printf ( "error opening nodekey file: %s" , err . Error ( ) )
2021-10-14 13:09:58 +00:00
return
}
defer f . Close ( )
if _ , err := f . WriteString ( nodekey + "\n" ) ; err != nil {
2024-02-08 15:40:13 +00:00
log . Printf ( "error writing nodekey file: %s" , err )
2021-10-14 13:09:58 +00:00
}
}
2024-07-28 14:17:27 +00:00
type mdmAgent struct {
2025-06-11 13:33:01 +00:00
agentIndex int
MDMCheckInInterval time . Duration
model string
serverAddress string
softwareCount softwareEntityCount
stats * osquery_perf . Stats
strings map [ string ] string
2025-11-03 19:07:25 +00:00
softwareVersionMap map [ rune ] int // Maps first char to version option: 0=base, 1=alternate, 2-31=patch versions 0-29
2025-06-11 13:33:01 +00:00
mdmProfileFailureProb float64
2024-07-28 14:17:27 +00:00
}
// 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
}
2025-11-03 19:07:25 +00:00
// 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 )
}
}
2025-07-24 12:26:03 +00:00
// 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 ,
} ,
}
2021-11-01 18:23:31 +00:00
type agent struct {
2024-03-14 19:33:12 +00:00
agentIndex int
2025-10-08 16:22:06 +00:00
hostCount int
totalHostCount int
hostIndexOffset int
2024-03-14 19:33:12 +00:00
softwareCount softwareEntityCount
softwareVSCodeExtensionsCount softwareExtraEntityCount
userCount entityCount
policyPassProb float64
munkiIssueProb float64
munkiIssueCount int
liveQueryFailProb float64
liveQueryNoResultsProb float64
strings map [ string ] string
serverAddress string
2025-01-29 16:24:44 +00:00
stats * osquery_perf . Stats
2024-03-14 19:33:12 +00:00
nodeKeyManager * nodeKeyManager
nodeKey string
templates * template . Template
os string
2022-05-31 13:15:58 +00:00
// 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
2022-10-28 17:27:21 +00:00
orbitNodeKey * string
2021-12-09 21:05:32 +00:00
2024-03-13 13:29:25 +00:00
// macMDMClient and winMDMClient simulate a device running the MDM protocol
// (client side) against Fleet MDM.
macMDMClient * mdmtest . TestAppleMDMClient
winMDMClient * mdmtest . TestWindowsMDMClient
2023-05-12 16:50:20 +00:00
// isEnrolledToMDM is true when the mdmDevice has enrolled.
isEnrolledToMDM bool
// isEnrolledToMDMMu protects isEnrolledToMDM.
isEnrolledToMDMMu sync . Mutex
2024-02-08 15:40:13 +00:00
disableScriptExec bool
disableFleetDesktop bool
loggerTLSMaxLines int
2023-08-23 22:31:47 +00:00
// atomic boolean is set to true when executing scripts, so that only a
// single goroutine at a time can execute scripts.
scriptExecRunning atomic . Bool
2024-03-29 15:18:31 +00:00
softwareQueryFailureProb float64
2024-03-14 19:33:12 +00:00
softwareVSCodeExtensionsFailProb float64
2024-08-21 14:08:16 +00:00
softwareInstaller softwareInstaller
2025-01-23 18:48:21 +00:00
linuxUniqueSoftwareVersion bool
linuxUniqueSoftwareTitle bool
2024-08-21 14:08:16 +00:00
// Software installed on the host via Fleet. Key is the software name + version + bundle identifier.
installedSoftware sync . Map
2025-11-21 16:42:19 +00:00
// Cached software indices (pointers into global softwareDB array for this agent's platform)
cachedSoftwareIndices [ ] uint32
2025-07-18 13:19:05 +00:00
// Host identity client for HTTP message signatures
hostIdentityClient * hostidentity . Client
2023-05-12 16:50:20 +00:00
//
2021-12-09 21:05:32 +00:00
// The following are exported to be used by the templates.
2023-05-12 16:50:20 +00:00
//
2021-12-09 21:05:32 +00:00
2023-02-08 14:49:42 +00:00
EnrollSecret string
UUID string
2023-02-17 20:10:49 +00:00
SerialNumber string
2024-07-10 14:01:25 +00:00
defaultSerialProb float64
2023-02-08 14:49:42 +00:00
ConfigInterval time . Duration
2023-10-17 14:30:59 +00:00
LogInterval time . Duration
2023-02-08 14:49:42 +00:00
QueryInterval time . Duration
2023-05-12 16:50:20 +00:00
MDMCheckInInterval time . Duration
2023-02-08 14:49:42 +00:00
DiskEncryptionEnabled bool
2025-06-11 13:33:01 +00:00
mdmProfileFailureProb float64
2025-11-03 19:07:25 +00:00
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
2023-10-20 13:29:59 +00:00
2025-03-11 15:20:49 +00:00
// 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
2024-03-04 18:10:10 +00:00
// 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
2025-03-11 15:20:49 +00:00
// 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).
2025-04-11 23:19:07 +00:00
certificatesMutex sync . RWMutex
certificatesCache [ ] map [ string ] string
commonSoftwareNameSuffix string
2025-06-20 16:41:46 +00:00
entraIDDeviceID string
entraIDUserPrincipalName string
2025-07-24 12:26:03 +00:00
installedAdamIDs [ ] int
2021-12-09 21:05:32 +00:00
}
2021-12-09 20:20:32 +00:00
2024-07-10 14:01:25 +00:00
func ( a * agent ) GetSerialNumber ( ) string {
if rand . Float64 ( ) <= a . defaultSerialProb {
return "-1"
}
return a . SerialNumber
}
2021-12-09 21:24:48 +00:00
type entityCount struct {
2021-12-09 21:05:32 +00:00
common int
unique int
2021-09-22 20:18:55 +00:00
}
2022-01-28 13:05:11 +00:00
type softwareEntityCount struct {
entityCount
2025-10-06 16:30:10 +00:00
vulnerable int
withLastOpened int
lastOpenedProb float64
commonSoftwareUninstallCount int
commonSoftwareUninstallProb float64
uniqueSoftwareUninstallCount int
uniqueSoftwareUninstallProb float64
duplicateBundleIdentifiersPercent int
softwareRenaming bool
2022-01-28 13:05:11 +00:00
}
2024-03-14 19:33:12 +00:00
type softwareExtraEntityCount struct {
entityCount
commonSoftwareUninstallCount int
commonSoftwareUninstallProb float64
uniqueSoftwareUninstallCount int
uniqueSoftwareUninstallProb float64
}
2024-08-21 14:08:16 +00:00
type softwareInstaller struct {
preInstallFailureProb float64
installFailureProb float64
postInstallFailureProb float64
mu * sync . Mutex
}
2022-01-28 13:05:11 +00:00
2021-11-19 11:50:25 +00:00
func newAgent (
2021-12-09 21:05:32 +00:00
agentIndex int ,
2025-10-08 16:22:06 +00:00
hostCount int ,
totalHostCount int ,
hostIndexOffset int ,
2023-05-12 16:50:20 +00:00
serverAddress , enrollSecret string ,
templates * template . Template ,
2023-10-17 14:30:59 +00:00
configInterval , logInterval , queryInterval , mdmCheckInInterval time . Duration ,
2024-03-14 19:33:12 +00:00
softwareQueryFailureProb float64 ,
softwareVSCodeExtensionsQueryFailureProb float64 ,
2024-08-21 14:08:16 +00:00
softwareInstaller softwareInstaller ,
2023-05-12 16:50:20 +00:00
softwareCount softwareEntityCount ,
2024-03-14 19:33:12 +00:00
softwareVSCodeExtensionsCount softwareExtraEntityCount ,
2023-05-12 16:50:20 +00:00
userCount entityCount ,
2021-11-19 11:50:25 +00:00
policyPassProb float64 ,
2022-05-31 13:15:58 +00:00
orbitProb float64 ,
2022-08-29 18:40:16 +00:00
munkiIssueProb float64 , munkiIssueCount int ,
2023-02-28 17:55:04 +00:00
emptySerialProb float64 ,
2024-07-10 14:01:25 +00:00
defaultSerialProb float64 ,
2023-05-12 16:50:20 +00:00
mdmProb float64 ,
mdmSCEPChallenge string ,
2023-05-25 11:12:10 +00:00
liveQueryFailProb float64 ,
liveQueryNoResultsProb float64 ,
2023-08-23 22:31:47 +00:00
disableScriptExec bool ,
2024-02-08 15:40:13 +00:00
disableFleetDesktop bool ,
loggerTLSMaxLines int ,
2025-01-23 18:48:21 +00:00
linuxUniqueSoftwareVersion bool ,
linuxUniqueSoftwareTitle bool ,
2025-04-11 23:19:07 +00:00
commonSoftwareNameSuffix string ,
2025-06-11 13:33:01 +00:00
mdmProfileFailureProb float64 ,
2025-07-18 13:19:05 +00:00
httpMessageSignatureProb float64 ,
2025-08-13 14:27:00 +00:00
httpMessageSignatureP384Prob float64 ,
2021-11-19 11:50:25 +00:00
) * agent {
2022-05-31 13:15:58 +00:00
var deviceAuthToken * string
if rand . Float64 ( ) <= orbitProb {
deviceAuthToken = ptr . String ( uuid . NewString ( ) )
}
2023-05-12 16:50:20 +00:00
serialNumber := mdmtest . RandSerialNumber ( )
2023-02-28 17:55:04 +00:00
if rand . Float64 ( ) <= emptySerialProb {
2023-05-12 16:50:20 +00:00
serialNumber = ""
}
2024-03-13 13:29:25 +00:00
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 ,
2024-06-10 20:02:35 +00:00
} , "MacBookPro16,1" )
2024-03-13 13:29:25 +00:00
// 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 )
}
2023-02-28 17:55:04 +00:00
}
2024-03-13 13:29:25 +00:00
2025-07-18 13:19:05 +00:00
// Determine if this agent should use HTTP message signatures
useHTTPSig := rand . Float64 ( ) < httpMessageSignatureProb // nolint:gosec // ignore weak randomizer
agent := & agent {
2024-03-14 19:33:12 +00:00
agentIndex : agentIndex ,
2025-10-08 16:22:06 +00:00
hostCount : hostCount ,
totalHostCount : totalHostCount ,
hostIndexOffset : hostIndexOffset ,
2024-03-14 19:33:12 +00:00
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 ,
2024-07-10 14:01:25 +00:00
defaultSerialProb : defaultSerialProb ,
2025-11-03 19:07:25 +00:00
OSPatchLevel : rand . Intn ( 25 ) , // Random patch level 0-24
2024-03-14 19:33:12 +00:00
2024-03-29 15:18:31 +00:00
softwareQueryFailureProb : softwareQueryFailureProb ,
2024-03-14 19:33:12 +00:00
softwareVSCodeExtensionsFailProb : softwareVSCodeExtensionsQueryFailureProb ,
2024-08-21 14:08:16 +00:00
softwareInstaller : softwareInstaller ,
2024-03-14 19:33:12 +00:00
2025-01-23 18:48:21 +00:00
linuxUniqueSoftwareVersion : linuxUniqueSoftwareVersion ,
linuxUniqueSoftwareTitle : linuxUniqueSoftwareTitle ,
2024-03-14 19:33:12 +00:00
macMDMClient : macMDMClient ,
winMDMClient : winMDMClient ,
2025-04-11 23:19:07 +00:00
disableScriptExec : disableScriptExec ,
disableFleetDesktop : disableFleetDesktop ,
loggerTLSMaxLines : loggerTLSMaxLines ,
bufferedResults : make ( map [ resultLog ] int ) ,
scheduledQueryData : new ( sync . Map ) ,
2025-11-03 19:07:25 +00:00
softwareVersionMap : make ( map [ rune ] int ) ,
2025-04-11 23:19:07 +00:00
commonSoftwareNameSuffix : commonSoftwareNameSuffix ,
2025-06-11 13:33:01 +00:00
mdmProfileFailureProb : mdmProfileFailureProb ,
2025-06-20 16:41:46 +00:00
entraIDDeviceID : uuid . NewString ( ) ,
entraIDUserPrincipalName : fmt . Sprintf ( "fake-%s@example.com" , randomString ( 5 ) ) ,
2021-09-22 20:18:55 +00:00
}
2025-07-18 13:19:05 +00:00
// Initialize host identity client
agent . hostIdentityClient = hostidentity . NewClient ( hostidentity . Config {
ServerAddress : serverAddress ,
EnrollSecret : enrollSecret ,
HostUUID : hostUUID ,
AgentIndex : agentIndex ,
2025-08-13 14:27:00 +00:00
} , useHTTPSig , httpMessageSignatureP384Prob )
2025-07-18 13:19:05 +00:00
2025-11-03 19:07:25 +00:00
// Pre-select kernels for Ubuntu agents to ensure consistency across queries
if agentOS == "ubuntu" {
agent . linuxKernels = selectKernels ( ubuntuKernels )
}
2025-07-18 13:19:05 +00:00
return agent
2021-09-22 20:18:55 +00:00
}
type enrollResponse struct {
NodeKey string ` json:"node_key" `
}
type distributedReadResponse struct {
Queries map [ string ] string ` json:"queries" `
}
2023-10-13 02:41:04 +00:00
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" `
2024-03-14 15:59:57 +00:00
lastRun int64
numRows uint
packName string
2023-10-13 02:41:04 +00:00
}
2022-10-28 17:27:21 +00:00
func ( a * agent ) isOrbit ( ) bool {
return a . deviceAuthToken != nil
}
2021-11-01 18:23:31 +00:00
func ( a * agent ) runLoop ( i int , onlyAlreadyEnrolled bool ) {
2025-07-18 13:19:05 +00:00
// 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
}
}
2022-10-28 17:27:21 +00:00
if a . isOrbit ( ) {
if err := a . orbitEnroll ( ) ; err != nil {
2024-03-13 13:29:25 +00:00
// 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
2022-10-28 17:27:21 +00:00
return
}
2024-03-13 13:29:25 +00:00
if a . winMDMClient != nil {
a . winMDMClient = mdmtest . NewTestMDMClientWindowsProgramatic ( a . serverAddress , * a . orbitNodeKey )
}
2022-10-28 17:27:21 +00:00
}
2021-11-01 18:23:31 +00:00
if err := a . enroll ( i , onlyAlreadyEnrolled ) ; err != nil {
2021-10-14 13:09:58 +00:00
return
}
2021-09-22 20:18:55 +00:00
2024-02-08 15:40:13 +00:00
_ = a . config ( )
2021-09-22 20:18:55 +00:00
resp , err := a . DistributedRead ( )
2024-02-08 15:40:13 +00:00
if err == nil {
2021-09-22 20:18:55 +00:00
if len ( resp . Queries ) > 0 {
2024-02-08 15:40:13 +00:00
_ = a . DistributedWrite ( resp . Queries )
2021-09-22 20:18:55 +00:00
}
}
2022-10-28 17:27:21 +00:00
if a . isOrbit ( ) {
go a . runOrbitLoop ( )
}
2024-03-13 13:29:25 +00:00
// 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 )
2023-05-12 16:50:20 +00:00
a . stats . IncrementMDMErrors ( )
return
}
a . setMDMEnrolled ( )
a . stats . IncrementMDMEnrollments ( )
2024-03-13 13:29:25 +00:00
go a . runMacosMDMLoop ( )
2023-05-12 16:50:20 +00:00
}
2023-10-17 14:30:59 +00:00
//
// 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.
2023-10-20 13:29:59 +00:00
// (1) distributed thread:
2023-10-17 14:30:59 +00:00
go func ( ) {
2023-10-20 13:29:59 +00:00
liveQueryTicker := time . NewTicker ( a . QueryInterval )
defer liveQueryTicker . Stop ( )
for range liveQueryTicker . C {
2024-02-08 15:40:13 +00:00
if resp , err := a . DistributedRead ( ) ; err == nil && len ( resp . Queries ) > 0 {
_ = a . DistributedWrite ( resp . Queries )
2023-10-17 14:30:59 +00:00
}
}
} ( )
2023-10-20 13:29:59 +00:00
// (2) config thread:
2023-10-17 14:30:59 +00:00
go func ( ) {
2023-10-20 13:29:59 +00:00
configTicker := time . NewTicker ( a . ConfigInterval )
defer configTicker . Stop ( )
for range configTicker . C {
2024-02-08 15:40:13 +00:00
_ = a . config ( )
2023-10-17 14:30:59 +00:00
}
} ( )
2023-10-20 13:29:59 +00:00
// (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
2024-02-12 22:06:58 +00:00
var results [ ] resultLog
2023-10-20 13:29:59 +00:00
now := time . Now ( ) . Unix ( )
2024-03-04 18:10:10 +00:00
prevCount := a . countBuffered ( )
2024-12-20 16:28:45 +00:00
// 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.
2025-03-11 15:20:49 +00:00
a . scheduledQueryMapMutex . RLock ( )
2024-12-20 16:28:45 +00:00
queryData := a . scheduledQueryData
2025-03-11 15:20:49 +00:00
a . scheduledQueryMapMutex . RUnlock ( )
2024-12-20 16:28:45 +00:00
queryData . Range ( func ( key , value any ) bool {
queryName := key . ( string )
query := value . ( scheduledQuery )
2024-03-14 15:59:57 +00:00
if query . lastRun == 0 || now >= ( query . lastRun + int64 ( query . ScheduleInterval ) ) {
2024-02-12 22:06:58 +00:00
results = append ( results , resultLog {
packName : query . packName ,
queryName : query . Name ,
numRows : int ( query . numRows ) ,
} )
2024-03-14 15:59:57 +00:00
// Update lastRun
2024-12-20 16:28:45 +00:00
query . lastRun = now
queryData . Store ( queryName , query )
2023-10-17 14:30:59 +00:00
}
2024-12-20 16:28:45 +00:00
return true
} )
2024-03-04 18:10:10 +00:00
if prevCount + len ( results ) < 1_000_000 { // osquery buffered_log_max is 1M
a . addToBuffer ( results )
2023-10-20 13:29:59 +00:00
}
2024-02-08 15:40:13 +00:00
a . sendLogsBatch ( )
2024-03-04 18:10:10 +00:00
newBufferedCount := a . countBuffered ( ) - prevCount
2024-02-08 15:40:13 +00:00
a . stats . UpdateBufferedLogs ( newBufferedCount )
}
}
2024-03-04 18:10:10 +00:00
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
}
2024-02-12 22:06:58 +00:00
type resultLog struct {
packName string
queryName string
numRows int
}
2024-03-04 18:10:10 +00:00
func ( r resultLog ) emit ( ) [ ] byte {
2024-02-12 22:06:58 +00:00
return scheduledQueryResults ( r . packName , r . queryName , r . numRows )
}
2024-02-08 15:40:13 +00:00
// sendLogsBatch sends up to loggerTLSMaxLines logs and updates the buffer.
func ( a * agent ) sendLogsBatch ( ) {
if len ( a . bufferedResults ) == 0 {
return
2021-09-22 20:18:55 +00:00
}
2024-02-08 15:40:13 +00:00
batchSize := a . loggerTLSMaxLines
2024-03-04 18:10:10 +00:00
if count := a . countBuffered ( ) ; count < batchSize {
batchSize = count
2024-02-12 22:06:58 +00:00
}
2024-03-04 18:10:10 +00:00
batch := a . getBatch ( batchSize )
if err := a . submitLogs ( batch ) ; err != nil {
2024-02-08 15:40:13 +00:00
return
}
2024-03-04 18:10:10 +00:00
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
}
}
2021-09-22 20:18:55 +00:00
}
2022-10-28 17:27:21 +00:00
func ( a * agent ) runOrbitLoop ( ) {
2025-07-18 13:19:05 +00:00
// 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 )
}
}
2022-10-28 17:27:21 +00:00
orbitClient , err := service . NewOrbitClient (
"" ,
a . serverAddress ,
"" ,
true ,
a . EnrollSecret ,
2023-04-27 11:44:39 +00:00
nil ,
2023-03-13 21:54:18 +00:00
fleet . OrbitHostInfo {
HardwareUUID : a . UUID ,
HardwareSerial : a . SerialNumber ,
Hostname : a . CachedString ( "hostname" ) ,
} ,
2024-02-21 18:36:15 +00:00
nil ,
2025-07-18 13:19:05 +00:00
signerWrapper ,
2025-07-18 14:31:52 +00:00
"" ,
2022-10-28 17:27:21 +00:00
)
if err != nil {
log . Println ( "creating orbit client: " , err )
}
orbitClient . TestNodeKey = * a . orbitNodeKey
2023-04-27 11:44:39 +00:00
deviceClient , err := service . NewDeviceClient ( a . serverAddress , true , "" , nil , "" )
2022-10-28 17:27:21 +00:00
if err != nil {
2024-02-08 15:40:13 +00:00
log . Fatal ( "creating device client: " , err )
2022-10-28 17:27:21 +00:00
}
// orbit does a config check when it starts
if _ , err := orbitClient . GetConfig ( ) ; err != nil {
a . stats . IncrementOrbitErrors ( )
}
2024-02-08 15:40:13 +00:00
tokenRotationEnabled := false
if ! a . disableFleetDesktop {
tokenRotationEnabled = orbitClient . GetServerCapabilities ( ) . Has ( fleet . CapabilityOrbitEndpoints ) &&
orbitClient . GetServerCapabilities ( ) . Has ( fleet . CapabilityTokenRotation )
2022-10-28 17:27:21 +00:00
2024-02-08 15:40:13 +00:00
// 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 )
}
2022-10-28 17:27:21 +00:00
2024-02-08 15:40:13 +00:00
if err := deviceClient . CheckToken ( * a . deviceAuthToken ) ; err != nil {
a . stats . IncrementOrbitErrors ( )
log . Println ( "deviceClient.CheckToken: " , err )
}
2022-10-28 17:27:21 +00:00
}
}
// 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 ( ) {
2025-03-31 16:14:09 +00:00
minVal := 1
maxVal := 5
numberOfRequests := rand . Intn ( maxVal - minVal + 1 ) + minVal
2022-10-28 17:27:21 +00:00
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
}
}
}
2024-02-08 15:40:13 +00:00
// Fleet Desktop performs a burst of check token requests when it's initialized
if ! a . disableFleetDesktop {
checkToken ( )
}
2022-10-28 17:27:21 +00:00
2023-08-23 22:31:47 +00:00
// orbit makes a call to check the config and update the CLI flags every 30
2022-10-28 17:27:21 +00:00
// 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 )
2025-08-08 17:31:25 +00:00
// fleet desktop pings every 10s for connectivity check.
fleetDesktopConnectivityCheck := time . Tick ( 10 * time . Second )
2022-10-28 17:27:21 +00:00
2024-03-13 13:29:25 +00:00
const windowsMDMEnrollmentAttemptFrequency = time . Hour
var lastEnrollAttempt time . Time
2022-10-28 17:27:21 +00:00
for {
select {
case <- orbitConfigTicker :
2023-08-23 22:31:47 +00:00
cfg , err := orbitClient . GetConfig ( )
if err != nil {
2022-10-28 17:27:21 +00:00
a . stats . IncrementOrbitErrors ( )
2023-10-04 12:10:16 +00:00
continue
2022-10-28 17:27:21 +00:00
}
2023-08-23 22:31:47 +00:00
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 )
}
2024-08-21 14:08:16 +00:00
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 )
}
2024-03-13 13:29:25 +00:00
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 ( )
}
}
2022-10-28 17:27:21 +00:00
case <- orbitTokenRemoteCheckTicker :
2024-02-08 15:40:13 +00:00
if ! a . disableFleetDesktop && tokenRotationEnabled {
2022-10-28 17:27:21 +00:00
if err := deviceClient . CheckToken ( * a . deviceAuthToken ) ; err != nil {
a . stats . IncrementOrbitErrors ( )
log . Println ( "deviceClient.CheckToken: " , err )
2023-10-04 12:10:16 +00:00
continue
2022-10-28 17:27:21 +00:00
}
}
case <- orbitTokenRotationTicker :
2024-02-08 15:40:13 +00:00
if ! a . disableFleetDesktop && tokenRotationEnabled {
2022-10-28 17:27:21 +00:00
newToken := ptr . String ( uuid . NewString ( ) )
if err := orbitClient . SetOrUpdateDeviceToken ( * newToken ) ; err != nil {
a . stats . IncrementOrbitErrors ( )
log . Println ( "orbitClient.SetOrUpdateDeviceToken: " , err )
2023-10-04 12:10:16 +00:00
continue
2022-10-28 17:27:21 +00:00
}
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 ( )
2023-10-04 12:10:16 +00:00
continue
2022-10-28 17:27:21 +00:00
}
case <- fleetDesktopPolicyTicker :
2024-02-08 15:40:13 +00:00
if ! a . disableFleetDesktop {
if _ , err := deviceClient . DesktopSummary ( * a . deviceAuthToken ) ; err != nil {
a . stats . IncrementDesktopErrors ( )
log . Println ( "deviceClient.NumberOfFailingPolicies: " , err )
continue
}
2022-10-28 17:27:21 +00:00
}
2025-08-08 17:31:25 +00:00
case <- fleetDesktopConnectivityCheck :
if ! a . disableFleetDesktop {
if err := deviceClient . Ping ( ) ; err != nil {
a . stats . IncrementDesktopErrors ( )
log . Println ( "deviceClient.Ping: " , err )
continue
}
}
2022-10-28 17:27:21 +00:00
}
}
}
2024-03-13 13:29:25 +00:00
func ( a * agent ) runMacosMDMLoop ( ) {
2023-05-12 16:50:20 +00:00
mdmCheckInTicker := time . Tick ( a . MDMCheckInInterval )
for range mdmCheckInTicker {
2024-03-13 13:29:25 +00:00
mdmCommandPayload , err := a . macMDMClient . Idle ( )
2023-05-12 16:50:20 +00:00
if err != nil {
2024-02-08 15:40:13 +00:00
log . Printf ( "MDM Idle request failed: %s" , err )
2023-05-12 16:50:20 +00:00
a . stats . IncrementMDMErrors ( )
continue
}
2024-03-13 13:29:25 +00:00
a . stats . IncrementMDMSessions ( )
2023-05-12 16:50:20 +00:00
INNER_FOR_LOOP :
for mdmCommandPayload != nil {
a . stats . IncrementMDMCommandsReceived ( )
2025-06-11 13:33:01 +00:00
2025-06-13 18:55:44 +00:00
switch mdmCommandPayload . Command . RequestType {
case "InstallProfile" :
2025-06-11 13:33:01 +00:00
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 )
2025-06-13 18:55:44 +00:00
if err != nil {
log . Printf ( "MDM Error request failed: %s" , err )
a . stats . IncrementMDMErrors ( )
break INNER_FOR_LOOP
}
2025-06-11 13:33:01 +00:00
} else {
mdmCommandPayload , err = a . macMDMClient . Acknowledge ( mdmCommandPayload . CommandUUID )
2025-06-13 18:55:44 +00:00
if err != nil {
log . Printf ( "MDM Acknowledge request failed: %s" , err )
a . stats . IncrementMDMErrors ( )
break INNER_FOR_LOOP
}
2025-06-11 13:33:01 +00:00
}
2025-06-13 18:55:44 +00:00
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.
2024-04-15 20:56:25 +00:00
a . doDeclarativeManagement ( mdmCommandPayload )
2025-06-13 18:55:44 +00:00
mdmCommandPayload = nextMdmCommandPayload
2025-07-24 12:26:03 +00:00
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
}
2025-06-13 18:55:44 +00:00
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
}
2024-04-15 20:56:25 +00:00
}
}
}
}
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
2023-05-12 16:50:20 +00:00
}
}
2024-04-15 20:56:25 +00:00
a . stats . IncrementDDMStatusSuccess ( )
2023-05-12 16:50:20 +00:00
}
2024-03-13 13:29:25 +00:00
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
2025-06-11 13:33:01 +00:00
if a . mdmProfileFailureProb > 0.0 && rand . Float64 ( ) <= a . mdmProfileFailureProb {
status = syncml . CmdStatusBadRequest
}
2024-03-13 13:29:25 +00:00
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
}
}
}
2023-08-23 22:31:47 +00:00
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 )
2024-02-08 15:40:13 +00:00
log . Printf ( "running scripts: %v" , execIDs )
2023-08-23 22:31:47 +00:00
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 ,
2023-08-31 14:08:50 +00:00
Output : "Scripts are disabled" ,
2023-08-23 22:31:47 +00:00
Runtime : 0 ,
2023-08-30 18:02:44 +00:00
ExitCode : - 2 ,
2023-08-23 22:31:47 +00:00
} ) ; err != nil {
log . Println ( "save disabled host script result:" , err )
return
}
2024-02-08 15:40:13 +00:00
log . Printf ( "did save disabled host script result: id=%s" , execID )
2023-08-23 22:31:47 +00:00
continue
}
2025-02-11 17:46:53 +00:00
a . stats . IncrementScriptExecs ( )
2023-08-23 22:31:47 +00:00
script , err := orbitClient . GetHostScript ( execID )
if err != nil {
log . Println ( "get host script:" , err )
2025-02-11 17:46:53 +00:00
a . stats . IncrementScriptExecErrs ( )
2023-08-23 22:31:47 +00:00
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 )
2025-02-11 17:46:53 +00:00
a . stats . IncrementScriptExecErrs ( )
2023-08-23 22:31:47 +00:00
return
}
2024-02-08 15:40:13 +00:00
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 )
2023-08-23 22:31:47 +00:00
}
}
2024-08-21 14:08:16 +00:00
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 ) {
2025-02-11 17:46:53 +00:00
a . stats . IncrementSoftwareInstalls ( )
2024-08-21 14:08:16 +00:00
payload := & fleet . HostSoftwareInstallResultPayload { }
payload . InstallUUID = installerID
installer , err := orbitClient . GetInstallerDetails ( installerID )
if err != nil {
log . Println ( "get installer details:" , err )
2025-02-11 17:46:53 +00:00
a . stats . IncrementSoftwareInstallErrs ( )
2024-08-21 14:08:16 +00:00
return
}
failed := false
if installer . PreInstallCondition != "" {
time . Sleep ( time . Duration ( rand . Intn ( 1000 ) ) * time . Millisecond )
2024-10-18 17:38:26 +00:00
if installer . PreInstallCondition == "select 1" { //nolint:gocritic // ignore ifElseChain
2024-08-21 14:08:16 +00:00
// 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
2025-01-29 16:24:44 +00:00
meta , cacheMiss , err = installerMetadataCache . Get ( installer , orbitClient )
2024-08-21 14:08:16 +00:00
if err != nil {
2025-02-11 17:46:53 +00:00
a . stats . IncrementSoftwareInstallErrs ( )
2024-08-21 14:08:16 +00:00
return
}
2025-01-29 16:24:44 +00:00
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.
2024-08-21 14:08:16 +00:00
err = orbitClient . DownloadAndDiscardSoftwareInstaller ( installer . InstallerID )
if err != nil {
log . Println ( "download and discard software installer:" , err )
2025-02-11 17:46:53 +00:00
a . stats . IncrementSoftwareInstallErrs ( )
2024-08-21 14:08:16 +00:00
return
}
}
time . Sleep ( time . Duration ( rand . Intn ( 30 ) ) * time . Second )
2024-10-18 17:38:26 +00:00
if installer . InstallScript == "exit 0" { //nolint:gocritic // ignore ifElseChain
2024-08-21 14:08:16 +00:00
// 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 )
2024-10-18 17:38:26 +00:00
if installer . PostInstallScript == "exit 0" { //nolint:gocritic // ignore ifElseChain
2024-08-21 14:08:16 +00:00
// 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 )
2025-02-11 17:46:53 +00:00
a . stats . IncrementSoftwareInstallErrs ( )
2024-08-21 14:08:16 +00:00
return
}
}
2025-07-18 13:19:05 +00:00
// 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
}
2024-03-04 18:10:10 +00:00
func ( a * agent ) waitingDo ( fn func ( ) * http . Request ) * http . Response {
2025-07-18 13:19:05 +00:00
response , err := http . DefaultClient . Do ( a . sign ( fn ( ) ) )
2024-02-19 14:33:19 +00:00
for err != nil || response . StatusCode != http . StatusOK {
2024-02-08 15:40:13 +00:00
if err != nil {
log . Printf ( "failed to run request: %s" , err )
} else { // res.StatusCode() != http.StatusOK
2024-03-04 18:10:10 +00:00
response . Body . Close ( )
2024-02-19 14:33:19 +00:00
log . Printf ( "request failed: %d" , response . StatusCode )
2024-02-08 15:40:13 +00:00
}
2022-10-28 17:27:21 +00:00
a . stats . IncrementErrors ( 1 )
2021-10-14 13:09:58 +00:00
<- time . Tick ( time . Duration ( rand . Intn ( 120 ) + 1 ) * time . Second )
2025-07-18 13:19:05 +00:00
response , err = http . DefaultClient . Do ( a . sign ( fn ( ) ) )
2021-09-22 20:18:55 +00:00
}
2024-02-19 14:33:19 +00:00
return response
2021-09-22 20:18:55 +00:00
}
2022-10-28 17:27:21 +00:00
// 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 {
2025-07-16 18:08:27 +00:00
params := contract . EnrollOrbitRequest {
2023-02-28 17:55:04 +00:00
EnrollSecret : a . EnrollSecret ,
HardwareUUID : a . UUID ,
HardwareSerial : a . SerialNumber ,
2024-11-18 21:51:36 +00:00
Hostname : a . CachedString ( "hostname" ) ,
2023-02-28 17:55:04 +00:00
}
2022-10-28 17:27:21 +00:00
jsonBytes , err := json . Marshal ( params )
if err != nil {
log . Println ( "orbit json marshall:" , err )
return err
}
2024-03-04 18:10:10 +00:00
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
} )
2024-02-19 14:33:19 +00:00
defer response . Body . Close ( )
2022-10-28 17:27:21 +00:00
var parsedResp service . EnrollOrbitResponse
2024-02-19 14:33:19 +00:00
if err := json . NewDecoder ( response . Body ) . Decode ( & parsedResp ) ; err != nil {
2022-10-28 17:27:21 +00:00
log . Println ( "orbit json parse:" , err )
return err
}
a . orbitNodeKey = & parsedResp . OrbitNodeKey
a . stats . IncrementOrbitEnrollments ( )
return nil
}
2024-03-15 22:04:46 +00:00
// This is an osquery enroll as opposed to an orbit enroll
2021-11-01 18:23:31 +00:00
func ( a * agent ) enroll ( i int , onlyAlreadyEnrolled bool ) error {
2021-12-09 21:05:32 +00:00
a . nodeKey = a . nodeKeyManager . Get ( i )
if a . nodeKey != "" {
2022-10-28 17:27:21 +00:00
a . stats . IncrementEnrollments ( )
2021-10-14 13:09:58 +00:00
return nil
}
if onlyAlreadyEnrolled {
2021-11-24 20:56:54 +00:00
return errors . New ( "not enrolled" )
2021-09-22 20:18:55 +00:00
}
2024-03-04 18:10:10 +00:00
response := a . waitingDo ( func ( ) * http . Request {
2024-04-16 11:39:34 +00:00
var body bytes . Buffer
if err := a . templates . ExecuteTemplate ( & body , "enroll" , a ) ; err != nil {
panic ( err )
}
2024-03-04 18:10:10 +00:00
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
} )
2024-02-19 14:33:19 +00:00
defer response . Body . Close ( )
2021-10-14 13:09:58 +00:00
2024-02-19 14:33:19 +00:00
if response . StatusCode != http . StatusOK {
log . Println ( "enroll status:" , response . StatusCode )
return fmt . Errorf ( "status code: %d" , response . StatusCode )
2021-09-22 20:18:55 +00:00
}
var parsedResp enrollResponse
2024-02-19 14:33:19 +00:00
if err := json . NewDecoder ( response . Body ) . Decode ( & parsedResp ) ; err != nil {
2021-09-22 20:18:55 +00:00
log . Println ( "json parse:" , err )
2021-10-14 13:09:58 +00:00
return err
2021-09-22 20:18:55 +00:00
}
2021-12-09 21:05:32 +00:00
a . nodeKey = parsedResp . NodeKey
2022-10-28 17:27:21 +00:00
a . stats . IncrementEnrollments ( )
2021-10-14 13:09:58 +00:00
2021-12-09 21:05:32 +00:00
a . nodeKeyManager . Add ( a . nodeKey )
2021-10-14 13:09:58 +00:00
return nil
2021-09-22 20:18:55 +00:00
}
2024-02-08 15:40:13 +00:00
func ( a * agent ) config ( ) error {
2024-02-19 14:33:19 +00:00
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" )
2021-10-14 13:09:58 +00:00
2025-07-18 13:19:05 +00:00
response , err := http . DefaultClient . Do ( a . sign ( request ) )
2024-02-12 22:06:58 +00:00
if err != nil {
return fmt . Errorf ( "config request failed to run: %w" , err )
}
2024-02-19 14:33:19 +00:00
defer response . Body . Close ( )
2024-02-12 22:06:58 +00:00
2023-10-20 13:29:59 +00:00
a . stats . IncrementConfigRequests ( )
2024-02-19 14:33:19 +00:00
statusCode := response . StatusCode
2024-02-08 15:40:13 +00:00
if statusCode != http . StatusOK {
a . stats . IncrementConfigErrors ( )
return fmt . Errorf ( "config request failed: %d" , statusCode )
2021-09-22 20:18:55 +00:00
}
2021-12-09 20:20:32 +00:00
parsedResp := struct {
Packs map [ string ] struct {
Queries map [ string ] interface { } ` json:"queries" `
} ` json:"packs" `
} { }
2024-02-19 14:33:19 +00:00
if err := json . NewDecoder ( response . Body ) . Decode ( & parsedResp ) ; err != nil {
2024-03-04 18:10:10 +00:00
a . stats . IncrementConfigErrors ( )
2024-02-08 15:40:13 +00:00
return fmt . Errorf ( "json parse at config: %w" , err )
2021-12-09 20:20:32 +00:00
}
2024-12-20 16:28:45 +00:00
existingLastRunData := make ( map [ string ] int64 )
2025-03-11 15:20:49 +00:00
a . scheduledQueryMapMutex . RLock ( )
queryData := a . scheduledQueryData
a . scheduledQueryMapMutex . RUnlock ( )
queryData . Range ( func ( key , value any ) bool {
2024-12-20 16:28:45 +00:00
existingLastRunData [ key . ( string ) ] = value . ( scheduledQuery ) . lastRun
return true
} )
newScheduledQueryData := new ( sync . Map )
2021-12-09 20:20:32 +00:00
for packName , pack := range parsedResp . Packs {
2023-10-13 02:41:04 +00:00
for queryName , query := range pack . Queries {
m , ok := query . ( map [ string ] interface { } )
if ! ok {
2024-02-08 15:40:13 +00:00
return fmt . Errorf ( "processing scheduled query failed: %v" , query )
2023-10-13 02:41:04 +00:00
}
q := scheduledQuery { }
2024-02-08 15:40:13 +00:00
q . packName = packName
2023-10-13 02:41:04 +00:00
q . Name = queryName
2024-03-15 22:04:46 +00:00
// 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"
2024-02-08 15:40:13 +00:00
q . numRows = 1
2023-10-13 02:41:04 +00:00
parts := strings . Split ( q . Name , "_" )
2023-10-13 17:26:46 +00:00
if len ( parts ) == 2 {
2023-10-13 02:41:04 +00:00
num , err := strconv . ParseInt ( parts [ 1 ] , 10 , 32 )
if err != nil {
num = 1
}
2024-02-08 15:40:13 +00:00
q . numRows = uint ( num )
2023-10-13 02:41:04 +00:00
}
2024-03-15 22:04:46 +00:00
2023-10-13 02:41:04 +00:00
q . ScheduleInterval = m [ "interval" ] . ( float64 )
q . Query = m [ "query" ] . ( string )
2024-03-14 15:59:57 +00:00
scheduledQueryName := packName + "_" + queryName
2024-12-20 16:28:45 +00:00
if lastRun , ok := existingLastRunData [ scheduledQueryName ] ; ok {
q . lastRun = lastRun
2024-03-14 15:59:57 +00:00
}
2024-12-20 16:28:45 +00:00
newScheduledQueryData . Store ( scheduledQueryName , q )
2021-12-09 20:20:32 +00:00
}
}
2023-10-20 13:29:59 +00:00
2025-03-11 15:20:49 +00:00
a . scheduledQueryMapMutex . Lock ( )
2024-12-20 16:28:45 +00:00
a . scheduledQueryData = newScheduledQueryData
2025-03-11 15:20:49 +00:00
a . scheduledQueryMapMutex . Unlock ( )
2024-02-08 15:40:13 +00:00
return nil
2021-09-22 20:18:55 +00:00
}
2021-10-14 13:09:58 +00:00
const stringVals = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
2021-09-22 20:18:55 +00:00
2022-08-29 18:40:16 +00:00
func randomString ( n int ) string {
2021-10-14 13:09:58 +00:00
sb := strings . Builder { }
sb . Grow ( n )
for i := 0 ; i < n ; i ++ {
sb . WriteByte ( stringVals [ rand . Int63 ( ) % int64 ( len ( stringVals ) ) ] )
2021-09-22 20:18:55 +00:00
}
2021-10-14 13:09:58 +00:00
return sb . String ( )
}
2021-09-22 20:18:55 +00:00
2025-11-03 19:07:25 +00:00
// 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 {
2025-10-28 18:46:59 +00:00
return baseVersion
2025-11-03 19:07:25 +00:00
}
// 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 :
2025-10-28 18:46:59 +00:00
return alternateVersion
2025-11-03 19:07:25 +00:00
default : // 2-31
patchNum := versionOption - 2 // 0-29
2025-10-28 18:46:59 +00:00
return fmt . Sprintf ( "%s.%d" , baseVersion , patchNum )
}
}
2021-11-01 18:23:31 +00:00
func ( a * agent ) CachedString ( key string ) string {
2021-10-14 13:09:58 +00:00
if val , ok := a . strings [ key ] ; ok {
return val
2021-09-22 20:18:55 +00:00
}
2022-08-29 18:40:16 +00:00
val := randomString ( 12 )
2021-10-14 13:09:58 +00:00
a . strings [ key ] = val
return val
}
2021-09-22 20:18:55 +00:00
2022-11-15 13:24:40 +00:00
func ( a * agent ) hostUsers ( ) [ ] map [ string ] string {
2021-12-09 21:24:48 +00:00
groupNames := [ ] string { "staff" , "nobody" , "wheel" , "tty" , "daemon" }
shells := [ ] string { "/bin/zsh" , "/bin/sh" , "/usr/bin/false" , "/bin/bash" }
2022-08-29 16:34:40 +00:00
commonUsers := make ( [ ] map [ string ] string , a . userCount . common )
2021-12-09 21:24:48 +00:00
for i := 0 ; i < len ( commonUsers ) ; i ++ {
2022-08-29 16:34:40 +00:00
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 ) ] ,
2021-12-09 21:24:48 +00:00
}
}
2022-08-29 16:34:40 +00:00
uniqueUsers := make ( [ ] map [ string ] string , a . userCount . unique )
2021-12-09 21:24:48 +00:00
for i := 0 ; i < len ( uniqueUsers ) ; i ++ {
2022-08-29 16:34:40 +00:00
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 ) ] ,
2021-12-09 21:24:48 +00:00
}
}
2024-10-18 17:38:26 +00:00
users := commonUsers
users = append ( users , uniqueUsers ... )
2021-12-09 21:24:48 +00:00
rand . Shuffle ( len ( users ) , func ( i , j int ) {
users [ i ] , users [ j ] = users [ j ] , users [ i ]
} )
return users
}
2022-08-29 16:34:40 +00:00
func ( a * agent ) softwareMacOS ( ) [ ] map [ string ] string {
2022-04-26 18:16:59 +00:00
var lastOpenedCount int
2025-10-08 16:22:06 +00:00
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 ++ {
2022-08-29 16:34:40 +00:00
var lastOpenedAt string
if l := a . genLastOpenedAt ( & lastOpenedCount ) ; l != nil {
2025-04-11 23:19:07 +00:00
lastOpenedAt = fmt . Sprint ( l . Unix ( ) )
2022-08-29 16:34:40 +00:00
}
2025-10-08 16:22:06 +00:00
if i < totalCommon {
2025-11-03 19:07:25 +00:00
name := fmt . Sprintf ( "Common_%d%s" , i , a . commonSoftwareNameSuffix )
2025-10-08 16:22:06 +00:00
commonSoftware = append ( commonSoftware , map [ string ] string {
2025-11-03 19:07:25 +00:00
"name" : name ,
"version" : a . selectSoftwareVersion ( name , "0.0.1" , "0.0.2" ) ,
2025-10-08 16:22:06 +00:00
"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 ) ,
} )
2021-11-01 18:23:31 +00:00
}
}
2025-03-03 20:20:28 +00:00
2025-10-08 16:22:06 +00:00
// Unique Software (always per-host, not distributed)
2022-08-29 16:34:40 +00:00
uniqueSoftware := make ( [ ] map [ string ] string , a . softwareCount . unique )
2021-12-09 21:05:32 +00:00
for i := 0 ; i < len ( uniqueSoftware ) ; i ++ {
2022-08-29 16:34:40 +00:00
var lastOpenedAt string
if l := a . genLastOpenedAt ( & lastOpenedCount ) ; l != nil {
lastOpenedAt = l . Format ( time . UnixDate )
}
2025-11-03 19:07:25 +00:00
name := fmt . Sprintf ( "Unique_%s_%d" , a . CachedString ( "hostname" ) , i )
2022-08-29 16:34:40 +00:00
uniqueSoftware [ i ] = map [ string ] string {
2025-11-03 19:07:25 +00:00
"name" : name ,
"version" : a . selectSoftwareVersion ( name , "1.1.1" , "1.1.2" ) ,
2024-03-14 19:33:12 +00:00
"bundle_identifier" : fmt . Sprintf ( "com.fleetdm.osquery-perf.unique_%s_%d" , a . CachedString ( "hostname" ) , i ) ,
"source" : "apps" ,
2022-08-29 16:34:40 +00:00
"last_opened_at" : lastOpenedAt ,
2024-03-14 19:33:12 +00:00
"installed_path" : fmt . Sprintf ( "/some/path/Unique_%s_%d.app" , a . CachedString ( "hostname" ) , i ) ,
2021-12-09 21:05:32 +00:00
}
}
2023-04-05 16:53:43 +00:00
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 ]
}
2025-03-03 20:20:28 +00:00
2025-11-21 16:42:19 +00:00
// 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 )
2025-03-03 20:20:28 +00:00
} else {
2025-11-21 16:42:19 +00:00
// Vulnerable Software
var vCount int
2025-03-03 20:20:28 +00:00
if a . softwareCount . vulnerable < 0 {
2025-11-21 16:42:19 +00:00
vCount = len ( macosVulnerableSoftware )
2025-03-03 20:20:28 +00:00
} else {
2025-11-21 16:42:19 +00:00
vCount = a . softwareCount . vulnerable
2025-03-03 20:20:28 +00:00
}
2025-11-21 16:42:19 +00:00
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
2022-08-29 16:34:40 +00:00
}
2025-03-03 20:20:28 +00:00
2025-11-21 16:42:19 +00:00
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 ) ,
} )
}
2022-01-28 13:05:11 +00:00
}
2025-03-03 20:20:28 +00:00
// Combine all software
2024-10-18 17:38:26 +00:00
software := commonSoftware
software = append ( software , uniqueSoftware ... )
2025-11-21 16:42:19 +00:00
software = append ( software , realSoftware ... )
2025-10-06 16:30:10 +00:00
software = append ( software , duplicateBundleSoftware ... )
2024-08-21 14:08:16 +00:00
a . installedSoftware . Range ( func ( key , value interface { } ) bool {
software = append ( software , value . ( map [ string ] string ) )
return true
} )
2021-12-09 21:05:32 +00:00
rand . Shuffle ( len ( software ) , func ( i , j int ) {
software [ i ] , software [ j ] = software [ j ] , software [ i ]
} )
2025-03-03 20:20:28 +00:00
2021-11-01 18:23:31 +00:00
return software
}
2024-07-28 14:17:27 +00:00
func ( a * mdmAgent ) softwareIOSandIPadOS ( source string ) [ ] fleet . Software {
commonSoftware := make ( [ ] map [ string ] string , a . softwareCount . common )
for i := 0 ; i < len ( commonSoftware ) ; i ++ {
2025-11-03 19:07:25 +00:00
name := fmt . Sprintf ( "Common_%d" , i )
2024-07-28 14:17:27 +00:00
commonSoftware [ i ] = map [ string ] string {
2025-11-03 19:07:25 +00:00
"name" : name ,
"version" : a . selectSoftwareVersion ( name , "0.0.1" , "0.0.2" ) ,
2024-07-28 14:17:27 +00:00
"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 ++ {
2025-11-03 19:07:25 +00:00
name := fmt . Sprintf ( "Unique_%s_%d" , a . CachedString ( "hostname" ) , i )
2024-07-28 14:17:27 +00:00
uniqueSoftware [ i ] = map [ string ] string {
2025-11-03 19:07:25 +00:00
"name" : name ,
"version" : a . selectSoftwareVersion ( name , "1.1.1" , "1.1.2" ) ,
2024-07-28 14:17:27 +00:00
"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 ]
}
2024-10-18 17:38:26 +00:00
software := commonSoftware
software = append ( software , uniqueSoftware ... )
2024-07-28 14:17:27 +00:00
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
}
2024-03-14 19:33:12 +00:00
func ( a * agent ) softwareVSCodeExtensions ( ) [ ] map [ string ] string {
commonVSCodeExtensionsSoftware := make ( [ ] map [ string ] string , a . softwareVSCodeExtensionsCount . common )
for i := 0 ; i < len ( commonVSCodeExtensionsSoftware ) ; i ++ {
2025-11-03 19:07:25 +00:00
name := fmt . Sprintf ( "common.extension_%d" , i )
2024-03-14 19:33:12 +00:00
commonVSCodeExtensionsSoftware [ i ] = map [ string ] string {
2025-11-03 19:07:25 +00:00
"name" : name ,
"version" : a . selectSoftwareVersion ( name , "0.0.1" , "0.0.2" ) ,
2024-03-14 19:33:12 +00:00
"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 ++ {
2025-11-03 19:07:25 +00:00
name := fmt . Sprintf ( "unique.extension_%s_%d" , a . CachedString ( "hostname" ) , i )
2024-03-14 19:33:12 +00:00
uniqueVSCodeExtensionsSoftware [ i ] = map [ string ] string {
2025-11-03 19:07:25 +00:00
"name" : name ,
"version" : a . selectSoftwareVersion ( name , "1.1.1" , "1.1.2" ) ,
2024-03-14 19:33:12 +00:00
"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 ,
} )
}
2024-10-18 17:38:26 +00:00
software := commonVSCodeExtensionsSoftware
software = append ( software , uniqueVSCodeExtensionsSoftware ... )
2024-03-14 19:33:12 +00:00
software = append ( software , vulnerableVSCodeExtensionsSoftware ... )
rand . Shuffle ( len ( software ) , func ( i , j int ) {
software [ i ] , software [ j ] = software [ j ] , software [ i ]
} )
return software
}
2025-11-03 19:07:25 +00:00
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
}
2021-11-01 18:23:31 +00:00
func ( a * agent ) DistributedRead ( ) ( * distributedReadResponse , error ) {
2024-02-19 14:33:19 +00:00
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" )
2021-09-22 20:18:55 +00:00
2025-07-18 13:19:05 +00:00
response , err := http . DefaultClient . Do ( a . sign ( request ) )
2024-02-12 22:06:58 +00:00
if err != nil {
return nil , fmt . Errorf ( "distributed/read request failed to run: %w" , err )
}
2024-02-19 14:33:19 +00:00
defer response . Body . Close ( )
2024-02-12 22:06:58 +00:00
2023-10-20 13:29:59 +00:00
a . stats . IncrementDistributedReads ( )
2024-02-19 14:33:19 +00:00
statusCode := response . StatusCode
2024-02-08 15:40:13 +00:00
if statusCode != http . StatusOK {
a . stats . IncrementDistributedReadErrors ( )
return nil , fmt . Errorf ( "distributed/read request failed: %d" , statusCode )
}
2021-09-22 20:18:55 +00:00
var parsedResp distributedReadResponse
2024-02-19 14:33:19 +00:00
if err := json . NewDecoder ( response . Body ) . Decode ( & parsedResp ) ; err != nil {
2024-03-04 18:10:10 +00:00
a . stats . IncrementDistributedReadErrors ( )
2024-02-08 15:40:13 +00:00
log . Printf ( "json parse: %s" , err )
2021-10-14 13:09:58 +00:00
return nil , err
2021-09-22 20:18:55 +00:00
}
return & parsedResp , nil
}
2021-11-01 18:23:31 +00:00
var defaultQueryResult = [ ] map [ string ] string {
{ "foo" : "bar" } ,
2021-09-22 20:18:55 +00:00
}
2022-04-26 18:16:59 +00:00
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
}
2021-11-19 11:50:25 +00:00
func ( a * agent ) runPolicy ( query string ) [ ] map [ string ] string {
2024-03-15 22:04:46 +00:00
// 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 { }
}
2021-11-19 11:50:25 +00:00
if rand . Float64 ( ) <= a . policyPassProb {
return [ ] map [ string ] string {
{ "1" : "1" } ,
}
}
2022-09-12 19:37:38 +00:00
return [ ] map [ string ] string { }
2021-11-19 11:50:25 +00:00
}
2021-12-09 20:20:32 +00:00
func ( a * agent ) randomQueryStats ( ) [ ] map [ string ] string {
var stats [ ] map [ string ] string
2025-03-11 15:20:49 +00:00
a . scheduledQueryMapMutex . RLock ( )
queryData := a . scheduledQueryData
a . scheduledQueryMapMutex . RUnlock ( )
queryData . Range ( func ( key , value any ) bool {
2024-12-20 16:28:45 +00:00
queryName := key . ( string )
2021-12-09 20:20:32 +00:00
stats = append ( stats , map [ string ] string {
2024-12-20 16:28:45 +00:00
"name" : queryName ,
2021-12-09 20:20:32 +00:00
"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 ) ,
2024-02-12 13:58:40 +00:00
"wall_time" : fmt . Sprint ( rand . Intn ( 4 ) + 1 ) ,
"wall_time_ms" : fmt . Sprint ( rand . Intn ( 4000 ) + 10 ) ,
2021-12-09 20:20:32 +00:00
} )
2024-12-20 16:28:45 +00:00
return true
} )
2021-12-09 20:20:32 +00:00
return stats
}
2023-05-12 16:50:20 +00:00
// 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.
2022-11-15 13:24:40 +00:00
func ( a * agent ) mdmMac ( ) [ ] map [ string ] string {
2023-05-12 16:50:20 +00:00
if ! a . mdmEnrolled ( ) {
return [ ] map [ string ] string {
{ "enrolled" : "false" , "server_url" : "" , "installed_from_dep" : "false" } ,
}
2021-12-21 12:37:58 +00:00
}
return [ ] map [ string ] string {
2024-03-13 13:29:25 +00:00
{
"enrolled" : "true" ,
"server_url" : a . macMDMClient . EnrollInfo . MDMURL ,
"installed_from_dep" : "false" ,
"payload_identifier" : apple_mdm . FleetPayloadIdentifier ,
} ,
2022-11-15 13:24:40 +00:00
}
}
2025-08-22 07:02:29 +00:00
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" ,
} ,
}
}
2025-06-20 16:41:46 +00:00
func ( a * agent ) entraConditionalAccess ( ) [ ] map [ string ] string {
return [ ] map [ string ] string {
{
"device_id" : a . entraIDDeviceID ,
"user_principal_name" : a . entraIDUserPrincipalName ,
} ,
}
}
2023-05-12 16:50:20 +00:00
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
}
2022-11-15 13:24:40 +00:00
func ( a * agent ) mdmWindows ( ) [ ] map [ string ] string {
2024-03-13 13:29:25 +00:00
if ! a . mdmEnrolled ( ) {
return [ ] map [ string ] string {
// empty service url means not enrolled
2024-03-21 19:09:05 +00:00
{ "aad_resource_id" : "" , "discovery_service_url" : "" , "provider_id" : "" , "installation_type" : "Client" } ,
2024-03-13 13:29:25 +00:00
}
2021-12-21 12:37:58 +00:00
}
2024-03-13 13:29:25 +00:00
return [ ] map [ string ] string {
{
2024-03-21 19:09:05 +00:00
"aad_resource_id" : "" ,
2024-03-13 13:29:25 +00:00
"discovery_service_url" : a . serverAddress ,
"provider_id" : fleet . WellKnownMDMFleet ,
"installation_type" : "Client" ,
} ,
2022-11-15 13:24:40 +00:00
}
2021-12-21 12:37:58 +00:00
}
2022-08-29 18:40:16 +00:00
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
} ( )
2021-12-21 12:37:58 +00:00
func ( a * agent ) munkiInfo ( ) [ ] map [ string ] string {
2022-08-29 18:40:16 +00:00
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 , ";" )
2021-12-21 12:37:58 +00:00
return [ ] map [ string ] string {
2022-08-29 18:40:16 +00:00
{ "version" : "1.2.3" , "errors" : errList , "warnings" : warnList } ,
2021-12-21 12:37:58 +00:00
}
}
2021-12-21 20:36:19 +00:00
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
}
2022-06-28 18:11:49 +00:00
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 {
2024-10-10 16:03:21 +00:00
max_capacity := 700 + rand . Intn ( 300 ) // between 700 and 1000 to ensure most batteries are healthy
cycleCount := rand . Intn ( 1200 )
2022-06-28 18:11:49 +00:00
result [ i ] = map [ string ] string {
2024-10-10 16:03:21 +00:00
"serial_number" : fmt . Sprintf ( "%04d" , i ) ,
"cycle_count" : strconv . Itoa ( cycleCount ) ,
"max_capacity" : strconv . Itoa ( max_capacity ) ,
"designed_capacity" : "1000" ,
2022-06-28 18:11:49 +00:00
}
}
return result
}
2022-09-21 19:16:31 +00:00
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 )
2023-12-22 18:46:33 +00:00
available := gigs * pct / 100
2022-09-21 19:16:31 +00:00
return [ ] map [ string ] string {
2023-12-22 18:46:33 +00:00
{
"percent_disk_space_available" : strconv . Itoa ( pct ) ,
"gigs_disk_space_available" : strconv . Itoa ( available ) ,
"gigs_total_disk_space" : strconv . Itoa ( gigs ) ,
} ,
2022-09-21 19:16:31 +00:00
}
}
2022-11-02 19:44:02 +00:00
func ( a * agent ) diskEncryption ( ) [ ] map [ string ] string {
// 50% of results have encryption enabled
2023-02-08 14:49:42 +00:00
a . DiskEncryptionEnabled = rand . Intn ( 2 ) == 1
if a . DiskEncryptionEnabled {
2022-11-02 19:44:02 +00:00
return [ ] map [ string ] string { { "1" : "1" } }
}
return [ ] map [ string ] string { }
}
2022-12-19 13:01:59 +00:00
func ( a * agent ) diskEncryptionLinux ( ) [ ] map [ string ] string {
// 50% of results have encryption enabled
2023-02-08 14:49:42 +00:00
a . DiskEncryptionEnabled = rand . Intn ( 2 ) == 1
if a . DiskEncryptionEnabled {
2022-12-19 13:01:59 +00:00
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" } ,
}
}
2025-12-18 15:48:21 +00:00
func ( a * agent ) certificatesDarwin ( ) [ ] map [ string ] string {
2025-03-11 15:20:49 +00:00
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
2025-08-06 12:38:59 +00:00
sources := [ ] string { "system" , "user" }
users := a . hostUsers ( )
2025-03-11 15:20:49 +00:00
const day = 24 * time . Hour
2025-08-06 12:38:59 +00:00
2025-03-11 15:20:49 +00:00
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
2025-08-06 12:38:59 +00:00
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" ] )
}
2025-03-11 15:20:49 +00:00
results [ i ] = m
}
a . certificatesMutex . Lock ( )
a . certificatesCache = results
a . certificatesMutex . Unlock ( )
return results
}
2025-12-18 15:48:21 +00:00
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
}
2024-04-09 21:33:44 +00:00
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" ,
} ,
}
}
2023-12-13 20:46:59 +00:00
func ( a * agent ) runLiveQuery ( query string ) ( results [ ] map [ string ] string , status * fleet . OsqueryStatus , message * string , stats * fleet . Stats ) {
2023-05-25 11:12:10 +00:00
if a . liveQueryFailProb > 0.0 && rand . Float64 ( ) <= a . liveQueryFailProb {
ss := fleet . OsqueryStatus ( 1 )
2023-12-13 20:46:59 +00:00
return [ ] map [ string ] string { } , & ss , ptr . String ( "live query failed with error foobar" ) , nil
2023-05-25 11:12:10 +00:00
}
if a . liveQueryNoResultsProb > 0.0 && rand . Float64 ( ) <= a . liveQueryNoResultsProb {
2025-01-09 16:00:22 +00:00
ss := fleet . OsqueryStatus ( 0 )
2023-12-13 20:46:59 +00:00
return [ ] map [ string ] string { } , & ss , nil , nil
2023-05-25 11:12:10 +00:00
}
2025-01-09 16:00:22 +00:00
// 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.
2025-07-18 13:19:05 +00:00
response , err := http . DefaultClient . Do ( a . sign ( request ) )
2025-01-09 16:00:22 +00:00
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 )
2024-01-29 18:11:49 +00:00
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" ,
} ,
2023-12-13 20:46:59 +00:00
} , & 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 ) ) ,
}
2023-05-25 11:12:10 +00:00
}
2024-11-15 19:30:39 +00:00
func ( a * agent ) processQuery ( name , query string , cachedResults * cachedResults ) (
handled bool , results [ ] map [ string ] string ,
status * fleet . OsqueryStatus , message * string , stats * fleet . Stats ,
2023-12-13 20:46:59 +00:00
) {
2022-05-31 13:15:58 +00:00
const (
hostPolicyQueryPrefix = "fleet_policy_query_"
hostDetailQueryPrefix = "fleet_detail_query_"
2023-05-25 11:12:10 +00:00
liveQueryPrefix = "fleet_distributed_query_"
2022-05-31 13:15:58 +00:00
)
2022-06-01 16:57:44 +00:00
statusOK := fleet . StatusOK
2022-09-26 19:39:39 +00:00
statusNotOK := fleet . OsqueryStatus ( 1 )
2024-03-14 19:33:12 +00:00
results = [ ] map [ string ] string { } // if a query fails, osquery returns empty results array
2022-05-31 13:15:58 +00:00
switch {
2023-05-25 11:12:10 +00:00
case strings . HasPrefix ( name , liveQueryPrefix ) :
2023-12-13 20:46:59 +00:00
results , status , message , stats = a . runLiveQuery ( query )
return true , results , status , message , stats
2022-05-31 13:15:58 +00:00
case strings . HasPrefix ( name , hostPolicyQueryPrefix ) :
2023-12-13 20:46:59 +00:00
return true , a . runPolicy ( query ) , & statusOK , nil , nil
2022-05-31 13:15:58 +00:00
case name == hostDetailQueryPrefix + "scheduled_query_stats" :
2023-12-13 20:46:59 +00:00
return true , a . randomQueryStats ( ) , & statusOK , nil , nil
2022-05-31 13:15:58 +00:00
case name == hostDetailQueryPrefix + "mdm" :
2024-03-13 13:29:25 +00:00
ss := statusOK
if rand . Intn ( 10 ) > 0 { // 90% success
2022-11-15 13:24:40 +00:00
results = a . mdmMac ( )
2024-03-13 13:29:25 +00:00
} else {
ss = statusNotOK
2025-08-22 07:02:29 +00:00
}
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
2022-11-15 13:24:40 +00:00
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2025-06-20 16:41:46 +00:00
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
2022-11-15 13:24:40 +00:00
case name == hostDetailQueryPrefix + "mdm_windows" :
2024-03-13 13:29:25 +00:00
ss := statusOK
if rand . Intn ( 10 ) > 0 { // 90% success
2022-11-15 13:24:40 +00:00
results = a . mdmWindows ( )
2024-03-13 13:29:25 +00:00
} else {
ss = statusNotOK
2021-12-21 12:37:58 +00:00
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-05-31 13:15:58 +00:00
case name == hostDetailQueryPrefix + "munki_info" :
ss := fleet . OsqueryStatus ( rand . Intn ( 2 ) )
if ss == fleet . StatusOK {
results = a . munkiInfo ( )
2021-12-21 12:37:58 +00:00
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-05-31 13:15:58 +00:00
case name == hostDetailQueryPrefix + "google_chrome_profiles" :
ss := fleet . OsqueryStatus ( rand . Intn ( 2 ) )
if ss == fleet . StatusOK {
results = a . googleChromeProfiles ( )
2021-12-21 20:36:19 +00:00
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-06-28 18:11:49 +00:00
case name == hostDetailQueryPrefix + "battery" :
ss := fleet . OsqueryStatus ( rand . Intn ( 2 ) )
if ss == fleet . StatusOK {
results = a . batteries ( )
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-08-29 16:34:40 +00:00
case name == hostDetailQueryPrefix + "users" :
ss := fleet . OsqueryStatus ( rand . Intn ( 2 ) )
if ss == fleet . StatusOK {
2022-11-15 13:24:40 +00:00
results = a . hostUsers ( )
2022-08-29 16:34:40 +00:00
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-08-29 16:34:40 +00:00
case name == hostDetailQueryPrefix + "software_macos" :
2024-03-14 19:33:12 +00:00
ss := fleet . StatusOK
2024-03-29 15:18:31 +00:00
if a . softwareQueryFailureProb > 0.0 && rand . Float64 ( ) <= a . softwareQueryFailureProb {
2024-03-14 19:33:12 +00:00
ss = fleet . OsqueryStatus ( 1 )
}
2022-08-29 16:34:40 +00:00
if ss == fleet . StatusOK {
results = a . softwareMacOS ( )
2024-11-15 19:30:39 +00:00
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 ]
}
2026-01-17 01:52:02 +00:00
cdhashSHA256 := fmt . Sprintf ( "%x" , sha1 . Sum ( [ ] byte ( installedPath ) ) ) // cdhash returns 40 characters, matching the length of the sha1 here
2024-11-15 19:30:39 +00:00
results = append ( results , map [ string ] string {
"path" : installedPath ,
"team_identifier" : teamIdentifier ,
2026-01-17 01:52:02 +00:00
"cdhash_sha256" : cdhashSHA256 ,
} )
}
}
}
return true , results , & ss , nil , nil
case name == hostDetailQueryPrefix + "software_macos_executable_sha256" :
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" ]
// Generate mock executable path
executablePath := installedPath + "/Contents/MacOS/" + strings . TrimSuffix ( s [ "name" ] , ".app" )
// Generate a mock sha256 hash based on the executable path for consistency
executableSHA256 := fmt . Sprintf ( "%x" , sha256 . Sum256 ( [ ] byte ( executablePath ) ) )
results = append ( results , map [ string ] string {
"path" : installedPath ,
"executable_path" : executablePath ,
"executable_sha256" : executableSHA256 ,
2024-11-15 19:30:39 +00:00
} )
}
}
2022-08-29 16:34:40 +00:00
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-08-29 16:34:40 +00:00
case name == hostDetailQueryPrefix + "software_windows" :
2024-03-14 19:33:12 +00:00
ss := fleet . StatusOK
2024-03-29 15:18:31 +00:00
if a . softwareQueryFailureProb > 0.0 && rand . Float64 ( ) <= a . softwareQueryFailureProb {
2024-03-14 19:33:12 +00:00
ss = fleet . OsqueryStatus ( 1 )
}
2022-08-29 16:34:40 +00:00
if ss == fleet . StatusOK {
2025-11-21 16:42:19 +00:00
// 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 )
2025-10-28 18:46:59 +00:00
}
}
2024-08-21 14:08:16 +00:00
a . installedSoftware . Range ( func ( key , value interface { } ) bool {
results = append ( results , value . ( map [ string ] string ) )
return true
} )
2022-08-29 16:34:40 +00:00
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-08-29 16:34:40 +00:00
case name == hostDetailQueryPrefix + "software_linux" :
2024-03-14 19:33:12 +00:00
ss := fleet . StatusOK
2024-03-29 15:18:31 +00:00
if a . softwareQueryFailureProb > 0.0 && rand . Float64 ( ) <= a . softwareQueryFailureProb {
2024-03-14 19:33:12 +00:00
ss = fleet . OsqueryStatus ( 1 )
}
2022-08-29 16:34:40 +00:00
if ss == fleet . StatusOK {
2024-10-18 17:38:26 +00:00
switch a . os { //nolint:gocritic // ignore singleCaseSwitch
2024-03-13 13:29:25 +00:00
case "ubuntu" :
2025-11-21 16:42:19 +00:00
// 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 ] )
}
2025-10-28 18:46:59 +00:00
} else {
2025-11-21 16:42:19 +00:00
a . cachedSoftwareIndices = softwaredb . MaybeMutateSoftware ( a . cachedSoftwareIndices , len ( softwareDB . Ubuntu ) )
2025-01-23 18:48:21 +00:00
}
2025-11-21 16:42:19 +00:00
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 )
2025-01-23 18:48:21 +00:00
}
}
2025-11-03 19:07:25 +00:00
// Add pre-selected kernels for this agent
results = append ( results , a . linuxKernels ... )
2024-08-21 14:08:16 +00:00
a . installedSoftware . Range ( func ( key , value interface { } ) bool {
results = append ( results , value . ( map [ string ] string ) )
return true
} )
2022-08-29 16:34:40 +00:00
}
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2024-03-14 19:33:12 +00:00
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
2022-09-21 19:16:31 +00:00
case name == hostDetailQueryPrefix + "disk_space_unix" || name == hostDetailQueryPrefix + "disk_space_windows" :
ss := fleet . OsqueryStatus ( rand . Intn ( 2 ) )
if ss == fleet . StatusOK {
results = a . diskSpace ( )
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-12-19 13:01:59 +00:00
case strings . HasPrefix ( name , hostDetailQueryPrefix + "disk_encryption_linux" ) :
ss := fleet . OsqueryStatus ( rand . Intn ( 2 ) )
if ss == fleet . StatusOK {
results = a . diskEncryptionLinux ( )
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2023-02-08 14:49:42 +00:00
case name == hostDetailQueryPrefix + "disk_encryption_darwin" ||
name == hostDetailQueryPrefix + "disk_encryption_windows" :
2022-11-02 19:44:02 +00:00
ss := fleet . OsqueryStatus ( rand . Intn ( 2 ) )
if ss == fleet . StatusOK {
results = a . diskEncryption ( )
}
2023-12-13 20:46:59 +00:00
return true , results , & ss , nil , nil
2022-09-26 19:39:39 +00:00
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.
2023-12-13 20:46:59 +00:00
return true , nil , & statusNotOK , nil , nil
2024-04-09 21:33:44 +00:00
case name == hostDetailQueryPrefix + "orbit_info" :
if a . orbitNodeKey == nil {
return true , nil , & statusNotOK , nil , nil
}
return true , a . orbitInfo ( ) , & statusOK , nil , nil
2025-03-11 15:20:49 +00:00
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 {
2025-12-18 15:48:21 +00:00
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 ( )
2025-03-11 15:20:49 +00:00
}
return true , results , & ss , nil , nil
2022-05-31 13:15:58 +00:00
default :
// Look for results in the template file.
2021-12-09 21:05:32 +00:00
if t := a . templates . Lookup ( name ) ; t == nil {
2023-12-13 20:46:59 +00:00
return false , nil , nil , nil , nil
2021-09-22 20:18:55 +00:00
}
2021-11-01 18:23:31 +00:00
var ni bytes . Buffer
2021-12-09 21:05:32 +00:00
err := a . templates . ExecuteTemplate ( & ni , name , a )
2021-11-01 18:23:31 +00:00
if err != nil {
panic ( err )
}
2022-05-31 13:15:58 +00:00
err = json . Unmarshal ( ni . Bytes ( ) , & results )
2021-11-01 18:23:31 +00:00
if err != nil {
panic ( err )
2021-09-22 20:18:55 +00:00
}
2022-08-26 18:55:03 +00:00
2023-12-13 20:46:59 +00:00
return true , results , & statusOK , nil , nil
2022-05-31 13:15:58 +00:00
}
}
2024-11-15 19:30:39 +00:00
type cachedResults struct {
software [ ] map [ string ] string
}
2024-02-08 15:40:13 +00:00
func ( a * agent ) DistributedWrite ( queries map [ string ] string ) error {
2022-05-31 13:15:58 +00:00
r := service . SubmitDistributedQueryResultsRequest {
Results : make ( fleet . OsqueryDistributedQueryResults ) ,
Statuses : make ( map [ string ] fleet . OsqueryStatus ) ,
2023-05-25 11:12:10 +00:00
Messages : make ( map [ string ] string ) ,
2023-12-13 20:46:59 +00:00
Stats : make ( map [ string ] * fleet . Stats ) ,
2022-05-31 13:15:58 +00:00
}
r . NodeKey = a . nodeKey
2024-11-15 19:30:39 +00:00
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 )
2022-06-01 16:57:44 +00:00
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
}
2023-05-25 11:12:10 +00:00
if message != nil {
r . Messages [ name ] = * message
}
2023-12-13 20:46:59 +00:00
if stats != nil {
r . Stats [ name ] = stats
}
2022-05-31 13:15:58 +00:00
}
2021-11-01 18:23:31 +00:00
}
body , err := json . Marshal ( r )
if err != nil {
panic ( err )
2021-09-22 20:18:55 +00:00
}
2024-02-19 14:33:19 +00:00
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" )
2021-10-14 13:09:58 +00:00
2025-07-18 13:19:05 +00:00
response , err := http . DefaultClient . Do ( a . sign ( request ) )
2024-02-12 22:06:58 +00:00
if err != nil {
return fmt . Errorf ( "distributed/write request failed to run: %w" , err )
}
2024-02-19 14:33:19 +00:00
defer response . Body . Close ( )
2024-02-12 22:06:58 +00:00
2022-10-28 17:27:21 +00:00
a . stats . IncrementDistributedWrites ( )
2024-02-08 15:40:13 +00:00
2024-02-19 14:33:19 +00:00
statusCode := response . StatusCode
2024-02-08 15:40:13 +00:00
if statusCode != http . StatusOK {
a . stats . IncrementDistributedWriteErrors ( )
return fmt . Errorf ( "distributed/write request failed: %d" , statusCode )
}
2021-09-22 20:18:55 +00:00
// No need to read the distributed write body
2024-02-08 15:40:13 +00:00
return nil
2021-09-22 20:18:55 +00:00
}
2024-03-04 18:10:10 +00:00
func scheduledQueryResults ( packName , queryName string , numResults int ) [ ] byte {
return [ ] byte ( ` {
2024-02-12 22:06:58 +00:00
"snapshot" : [ ` + rows(numResults) + `
2023-10-13 02:41:04 +00:00
] ,
"action" : "snapshot" ,
"name" : "pack/` + packName + `/` + queryName + `" ,
2024-02-12 22:06:58 +00:00
"hostIdentifier" : "EF9595F0-CE81-493A-9B06-D8A9D2CCB952" ,
2023-10-13 02:41:04 +00:00
"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" ,
2024-02-12 22:06:58 +00:00
"hostname" : "osquery-perf"
2023-10-13 02:41:04 +00:00
}
2023-10-17 14:30:59 +00:00
} ` )
}
2024-03-04 18:10:10 +00:00
func ( a * agent ) connCheck ( ) error {
request , err := http . NewRequest ( "GET" , a . serverAddress + "/version" , nil )
2024-02-19 14:33:19 +00:00
if err != nil {
2024-03-04 18:10:10 +00:00
panic ( err )
2024-02-19 14:33:19 +00:00
}
2024-03-04 18:10:10 +00:00
response , err := http . DefaultClient . Do ( request )
2023-10-17 14:30:59 +00:00
if err != nil {
2024-03-04 18:10:10 +00:00
return err
2023-10-17 14:30:59 +00:00
}
2024-03-04 18:10:10 +00:00
defer response . Body . Close ( )
if response . StatusCode != http . StatusOK {
return errors . New ( http . StatusText ( response . StatusCode ) )
2023-10-17 14:30:59 +00:00
}
2024-03-04 18:10:10 +00:00
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 )
2023-10-13 02:41:04 +00:00
}
2024-03-04 18:10:10 +00:00
var resultLogs [ ] byte
for i , result := range results {
if i > 0 {
resultLogs = append ( resultLogs , ',' )
}
resultLogs = append ( resultLogs , result . emit ( ) ... )
2023-10-13 02:41:04 +00:00
}
2024-03-04 18:10:10 +00:00
body := [ ] byte ( ` { "node_key": " ` + a . nodeKey + ` ", "log_type": "result", "data": [ ` + string ( resultLogs ) + ` ]} ` )
2024-02-19 14:33:19 +00:00
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" )
2023-10-20 13:29:59 +00:00
2025-07-18 13:19:05 +00:00
response , err := http . DefaultClient . Do ( a . sign ( request ) )
2024-02-12 22:06:58 +00:00
if err != nil {
return fmt . Errorf ( "log request failed to run: %w" , err )
}
2024-02-19 14:33:19 +00:00
defer response . Body . Close ( )
2024-02-12 22:06:58 +00:00
2023-10-20 13:29:59 +00:00
a . stats . IncrementResultLogRequests ( )
2024-02-08 15:40:13 +00:00
2024-02-19 14:33:19 +00:00
statusCode := response . StatusCode
2024-02-08 15:40:13 +00:00
if statusCode != http . StatusOK {
a . stats . IncrementResultLogErrors ( )
return fmt . Errorf ( "log request failed: %d" , statusCode )
}
return nil
2023-10-13 02:41:04 +00:00
}
2024-07-28 14:17:27 +00:00
func ( a * mdmAgent ) runAppleIDeviceMDMLoop ( mdmSCEPChallenge string ) {
2024-06-10 20:02:35 +00:00
udid := mdmtest . RandUDID ( )
mdmClient := mdmtest . NewTestMDMClientAppleDirect ( mdmtest . AppleEnrollInfo {
SCEPChallenge : mdmSCEPChallenge ,
2024-07-28 14:17:27 +00:00
SCEPURL : a . serverAddress + apple_mdm . SCEPPath ,
MDMURL : a . serverAddress + apple_mdm . MDMPath ,
} , a . model )
2024-06-10 20:02:35 +00:00
mdmClient . UUID = udid
mdmClient . SerialNumber = mdmtest . RandSerialNumber ( )
2024-07-28 14:17:27 +00:00
deviceName := fmt . Sprintf ( "%s-%d" , a . model , a . agentIndex )
productName := a . model
softwareSource := "ios_apps"
if strings . HasPrefix ( a . model , "iPad" ) {
softwareSource = "ipados_apps"
}
2024-06-10 20:02:35 +00:00
if err := mdmClient . Enroll ( ) ; err != nil {
2024-07-28 14:17:27 +00:00
log . Printf ( "%s MDM enroll failed: %s" , a . model , err )
a . stats . IncrementMDMErrors ( )
2024-06-10 20:02:35 +00:00
return
}
2024-07-28 14:17:27 +00:00
a . stats . IncrementMDMEnrollments ( )
2024-06-10 20:02:35 +00:00
2024-07-28 14:17:27 +00:00
mdmCheckInTicker := time . Tick ( a . MDMCheckInInterval )
2024-06-10 20:02:35 +00:00
for range mdmCheckInTicker {
mdmCommandPayload , err := mdmClient . Idle ( )
if err != nil {
2024-07-28 14:17:27 +00:00
log . Printf ( "MDM Idle request failed: %s: %s" , a . model , err )
a . stats . IncrementMDMErrors ( )
2024-06-10 20:02:35 +00:00
continue
}
2024-07-28 14:17:27 +00:00
a . stats . IncrementMDMSessions ( )
2024-06-10 20:02:35 +00:00
for mdmCommandPayload != nil {
2024-07-28 14:17:27 +00:00
a . stats . IncrementMDMCommandsReceived ( )
switch mdmCommandPayload . Command . RequestType {
case "DeviceInformation" :
mdmCommandPayload , err = mdmClient . AcknowledgeDeviceInformation ( udid , mdmCommandPayload . CommandUUID , deviceName ,
2026-01-15 20:03:58 +00:00
productName , "America/Los_Angeles" )
2024-07-28 14:17:27 +00:00
case "InstalledApplicationList" :
software := a . softwareIOSandIPadOS ( softwareSource )
mdmCommandPayload , err = mdmClient . AcknowledgeInstalledApplicationList ( udid , mdmCommandPayload . CommandUUID , software )
2025-06-11 13:33:01 +00:00
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 )
}
2024-07-28 14:17:27 +00:00
default :
2024-06-10 20:02:35 +00:00
mdmCommandPayload , err = mdmClient . Acknowledge ( mdmCommandPayload . CommandUUID )
}
if err != nil {
2024-07-28 14:17:27 +00:00
log . Printf ( "MDM Acknowledge request failed: %s: %s" , a . model , err )
a . stats . IncrementMDMErrors ( )
2024-06-10 20:02:35 +00:00
break
}
}
}
}
2024-02-08 15:40:13 +00:00
// rows returns a set of rows for use in tests for query results.
2024-02-12 22:06:58 +00:00
func rows ( num int ) string {
2023-10-13 02:41:04 +00:00
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" ,
2024-02-12 22:06:58 +00:00
"uuid" : "EF9595F0-CE81-493A-9B06-D8A9D2CCB95" ,
2023-10-13 02:41:04 +00:00
"version" : "5.9.2" ,
"watcher" : "3570"
} ` )
if i != num - 1 {
b . WriteString ( "," )
}
}
return b . String ( )
}
2021-09-22 20:18:55 +00:00
func main ( ) {
2024-02-12 22:06:58 +00:00
// Start HTTP server for pprof. See https://pkg.go.dev/net/http/pprof.
go func ( ) {
log . Println ( http . ListenAndServe ( "localhost:6060" , nil ) )
} ( )
2024-02-19 14:33:19 +00:00
// #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
2022-11-15 13:24:40 +00:00
validTemplateNames := map [ string ] bool {
2024-01-29 18:11:49 +00:00
"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 ,
2024-06-10 20:02:35 +00:00
"iphone_14.6.tmpl" : true ,
"ipad_13.18.tmpl" : true ,
2022-11-15 13:24:40 +00:00
}
allowedTemplateNames := make ( [ ] string , 0 , len ( validTemplateNames ) )
for k := range validTemplateNames {
allowedTemplateNames = append ( allowedTemplateNames , k )
}
var (
2025-10-08 16:22:06 +00:00
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" )
2023-10-17 14:30:59 +00:00
// 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" )
2024-03-15 22:04:46 +00:00
queryInterval = flag . Duration ( "query_interval" , 10 * time . Second , "Interval for distributed query requests" )
2024-08-26 18:20:57 +00:00
mdmCheckInInterval = flag . Duration ( "mdm_check_in_interval" , 1 * time . Minute , "Interval for performing MDM check-ins (applies to both macOS and Windows)" )
2023-04-05 16:53:43 +00:00
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" )
2025-08-13 14:27:00 +00:00
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" )
2025-07-18 13:19:05 +00:00
2024-04-17 19:11:04 +00:00
// 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" )
2024-03-14 19:33:12 +00:00
2024-08-21 14:08:16 +00:00
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" )
2024-03-14 19:33:12 +00:00
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" )
2023-04-05 16:53:43 +00:00
2024-03-14 19:33:12 +00:00
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" )
2023-04-05 16:53:43 +00:00
2025-10-06 16:30:10 +00:00
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" )
2025-01-23 18:48:21 +00:00
// 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." )
2025-03-03 20:20:28 +00:00
// 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." )
2022-11-15 13:24:40 +00:00
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" )
2023-08-31 13:58:50 +00:00
// E.g. when running with `-host_count=10`, you can set host count for each template the following way:
2024-01-29 18:11:49 +00:00
// `-os_templates=windows_11.tmpl:3,macos_14.1.2.tmpl:4,ubuntu_22.04.tmpl:3`
2024-07-10 14:01:25 +00:00
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" )
2023-05-12 16:50:20 +00:00
2025-06-11 13:33:01 +00:00
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]" )
2023-05-25 11:12:10 +00:00
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" )
2023-08-23 22:31:47 +00:00
disableScriptExec = flag . Bool ( "disable_script_exec" , false , "Disable script execution support" )
2024-02-08 15:40:13 +00:00
disableFleetDesktop = flag . Bool ( "disable_fleet_desktop" , false , "Disable Fleet Desktop" )
// logger_tls_max_lines is simulating the osquery setting with the same name.
2024-10-18 17:38:26 +00:00
loggerTLSMaxLines = flag . Int ( "logger_tls_max_lines" , 1024 ,
"Maximum number of buffered result log lines to send on every log request" )
2025-04-11 23:19:07 +00:00
commonSoftwareNameSuffix = flag . String ( "common_software_name_suffix" , "" , "Suffix to add to generated common software names" )
2025-11-21 16:42:19 +00:00
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." )
2022-11-15 13:24:40 +00:00
)
2021-09-22 20:18:55 +00:00
flag . Parse ( )
rand . Seed ( * randSeed )
2025-11-21 16:42:19 +00:00
// 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." )
}
2025-10-08 16:22:06 +00:00
// 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" )
}
2022-11-15 13:24:40 +00:00
if * onlyAlreadyEnrolled {
// Orbit enrollment does not support the "already enrolled" mode at the
// moment (see TODO in this file).
* orbitProb = 0
2022-06-08 01:09:47 +00:00
}
2024-02-08 15:40:13 +00:00
if * commonSoftwareUninstallCount > * commonSoftwareCount {
2023-04-05 16:53:43 +00:00
log . Fatalf ( "Argument common_software_uninstall_count cannot be bigger than common_software_count" )
}
2024-02-08 15:40:13 +00:00
if * uniqueSoftwareUninstallCount > * uniqueSoftwareCount {
2023-04-05 16:53:43 +00:00
log . Fatalf ( "Argument unique_software_uninstall_count cannot be bigger than unique_software_count" )
}
2023-08-31 13:58:50 +00:00
tmplsm := make ( map [ * template . Template ] int )
2022-11-15 13:24:40 +00:00
requestedTemplates := strings . Split ( * osTemplates , "," )
2023-08-31 13:58:50 +00:00
tmplsTotalHostCount := 0
2022-11-15 13:24:40 +00:00
for _ , nm := range requestedTemplates {
2023-08-31 13:58:50 +00:00
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 )
}
2022-11-15 13:24:40 +00:00
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 )
2022-06-08 01:09:47 +00:00
if err != nil {
log . Fatal ( "parse templates: " , err )
}
2023-08-31 13:58:50 +00:00
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 )
2021-09-22 20:18:55 +00:00
}
2021-11-01 18:23:31 +00:00
// Spread starts over the interval to prevent thundering herd
2021-09-22 20:18:55 +00:00
sleepTime := * startPeriod / time . Duration ( * hostCount )
2021-10-14 13:09:58 +00:00
2025-01-29 16:24:44 +00:00
stats := & osquery_perf . Stats {
StartTime : time . Now ( ) ,
2023-10-20 13:29:59 +00:00
}
2025-01-29 16:24:44 +00:00
go stats . RunLoop ( )
installerMetadataCache . Stats = stats
2021-10-14 13:09:58 +00:00
2021-11-01 18:23:31 +00:00
nodeKeyManager := & nodeKeyManager { }
2021-10-14 13:09:58 +00:00
if nodeKeyFile != nil {
nodeKeyManager . filepath = * nodeKeyFile
nodeKeyManager . LoadKeys ( )
}
2023-08-31 13:58:50 +00:00
var tmplss [ ] * template . Template
for tmpl := range tmplsm {
tmplss = append ( tmplss , tmpl )
}
2021-09-22 20:18:55 +00:00
for i := 0 ; i < * hostCount ; i ++ {
2023-08-31 13:58:50 +00:00
var tmpl * template . Template
if tmplsTotalHostCount > 0 {
for tmpl_ , hostCount := range tmplsm {
if hostCount > 0 {
tmpl = tmpl_
2024-10-18 17:38:26 +00:00
tmplsm [ tmpl_ ] --
2023-08-31 13:58:50 +00:00
break
}
}
if tmpl == nil {
log . Fatalf ( "Failed to determine template for host: %d" , i )
}
} else {
tmpl = tmplss [ i % len ( tmplss ) ]
}
2024-06-10 20:02:35 +00:00
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"
}
2024-07-28 14:17:27 +00:00
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 ,
} ,
2025-06-11 13:33:01 +00:00
stats : stats ,
strings : make ( map [ string ] string ) ,
2025-11-03 19:07:25 +00:00
softwareVersionMap : make ( map [ rune ] int ) ,
2025-06-11 13:33:01 +00:00
mdmProfileFailureProb : * mdmProfileFailureProb ,
2024-07-28 14:17:27 +00:00
}
go mobileDevice . runAppleIDeviceMDMLoop ( * mdmSCEPChallenge )
2024-06-10 20:02:35 +00:00
time . Sleep ( sleepTime )
continue
}
2023-05-12 16:50:20 +00:00
a := newAgent ( i + 1 ,
2025-10-08 16:22:06 +00:00
* hostCount ,
* totalHostCount ,
* hostIndexOffset ,
2023-05-12 16:50:20 +00:00
* serverURL ,
* enrollSecret ,
tmpl ,
* configInterval ,
2023-10-17 14:30:59 +00:00
* logInterval ,
2023-05-12 16:50:20 +00:00
* queryInterval ,
* mdmCheckInInterval ,
2024-03-14 19:33:12 +00:00
* softwareQueryFailureProb ,
* softwareVSCodeExtensionsQueryFailureProb ,
2024-08-21 14:08:16 +00:00
softwareInstaller {
preInstallFailureProb : * softwareInstallerPreInstallFailureProb ,
installFailureProb : * softwareInstallerInstallFailureProb ,
postInstallFailureProb : * softwareInstallerPostInstallFailureProb ,
mu : new ( sync . Mutex ) ,
} ,
2022-05-31 13:15:58 +00:00
softwareEntityCount {
entityCount : entityCount {
common : * commonSoftwareCount ,
unique : * uniqueSoftwareCount ,
} ,
2025-10-06 16:30:10 +00:00
vulnerable : * vulnerableSoftwareCount ,
withLastOpened : * withLastOpenedSoftwareCount ,
lastOpenedProb : * lastOpenedChangeProb ,
commonSoftwareUninstallCount : * commonSoftwareUninstallCount ,
commonSoftwareUninstallProb : * commonSoftwareUninstallProb ,
uniqueSoftwareUninstallCount : * uniqueSoftwareUninstallCount ,
uniqueSoftwareUninstallProb : * uniqueSoftwareUninstallProb ,
duplicateBundleIdentifiersPercent : * duplicateBundleIdentifiersPercent ,
softwareRenaming : * softwareRenaming ,
2024-03-14 19:33:12 +00:00
} ,
softwareExtraEntityCount {
entityCount : entityCount {
common : * commonVSCodeExtensionsSoftwareCount ,
unique : * uniqueVSCodeExtensionsSoftwareCount ,
} ,
commonSoftwareUninstallCount : * commonVSCodeExtensionsSoftwareUninstallCount ,
commonSoftwareUninstallProb : * commonVSCodeExtensionsSoftwareUninstallProb ,
uniqueSoftwareUninstallCount : * uniqueVSCodeExtensionsSoftwareUninstallCount ,
uniqueSoftwareUninstallProb : * uniqueVSCodeExtensionsSoftwareUninstallProb ,
} ,
entityCount {
2022-05-31 13:15:58 +00:00
common : * commonUserCount ,
unique : * uniqueUserCount ,
2022-01-28 13:05:11 +00:00
} ,
2022-05-31 13:15:58 +00:00
* policyPassProb ,
* orbitProb ,
2022-08-29 18:40:16 +00:00
* munkiIssueProb ,
* munkiIssueCount ,
2023-02-28 17:55:04 +00:00
* emptySerialProb ,
2024-07-10 14:01:25 +00:00
* defaultSerialProb ,
2023-05-12 16:50:20 +00:00
* mdmProb ,
* mdmSCEPChallenge ,
2023-05-25 11:12:10 +00:00
* liveQueryFailProb ,
* liveQueryNoResultsProb ,
2023-08-23 22:31:47 +00:00
* disableScriptExec ,
2024-02-08 15:40:13 +00:00
* disableFleetDesktop ,
* loggerTLSMaxLines ,
2025-01-23 18:48:21 +00:00
* linuxUniqueSoftwareVersion ,
* linuxUniqueSoftwareTitle ,
2025-04-11 23:19:07 +00:00
* commonSoftwareNameSuffix ,
2025-06-11 13:33:01 +00:00
* mdmProfileFailureProb ,
2025-07-18 13:19:05 +00:00
* httpMessageSignatureProb ,
2025-08-13 14:27:00 +00:00
* httpMessageSignatureP384Prob ,
2022-05-31 13:15:58 +00:00
)
2021-12-09 21:05:32 +00:00
a . stats = stats
a . nodeKeyManager = nodeKeyManager
2022-11-15 13:24:40 +00:00
go a . runLoop ( i , * onlyAlreadyEnrolled )
2021-09-22 20:18:55 +00:00
time . Sleep ( sleepTime )
}
2024-02-08 15:40:13 +00:00
log . Println ( "Agents running. Kill with C-c." )
2021-09-22 20:18:55 +00:00
<- make ( chan struct { } )
}