diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 710d225ee1..6fb1aa2e68 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -547,8 +547,15 @@ type agent struct { // cache of this host's per-host certificates (the certs unique to this host, excluding the shared certs every host // reports). Note that this requires a mutex even though only used in a.processQuery, that's because both the runLoop // and the live query goroutines may call DistributedWrite (which calls processQuery). - certificatesMutex sync.RWMutex - hostCertSpecs []simulatedCert + certificatesMutex sync.RWMutex + hostCertSpecs []simulatedCert + // scepCertSpecs holds the certs issued to this host during Windows MDM SCEP exchanges, keyed by the SCEP CSP + // unique ID so a re-issued cert replaces its predecessor. + scepCertSpecs map[string]simulatedCert + // withholdSCEPCert marks ~5% of hosts that never report their issued SCEP certs, exercising the server's SCEP + // verification backstop (test-only failure). + withholdSCEPCert bool + commonSoftwareNameSuffix string entraIDDeviceID string @@ -727,6 +734,8 @@ func newAgent( cachedLastOpenedAt: make(map[string]*time.Time), commonSoftwareNameSuffix: commonSoftwareNameSuffix, mdmProfileFailureProb: mdmProfileFailureProb, + // Every 20th host (5%) withholds its issued SCEP certs to exercise the server's verification backstop (test-only failure). + withholdSCEPCert: agentIndex%20 == 0, entraIDDeviceID: uuid.NewString(), entraIDUserPrincipalName: fmt.Sprintf("fake-%s@example.com", randomString(5)), @@ -1624,6 +1633,16 @@ func (a *agent) doWindowsMDMCheckIn(onDemand bool) (newPollInterval time.Duratio continue } a.stats.IncrementMDMSCEPSuccess() + if res.Cert == nil { + continue + } + // Report the issued cert via the certificates detail query so the server observes the + // fleet- renewal-ID marker and marks the SCEP profile verified. The withheld ~5% never + // report theirs, so the server's verification backstop fails those profiles. + if a.withholdSCEPCert { + continue + } + a.storeSCEPCertSpec(res.UniqueID, res.Cert) } }() } else { diff --git a/cmd/osquery-perf/certificates.go b/cmd/osquery-perf/certificates.go index a41bf716fd..b38ac88c4b 100644 --- a/cmd/osquery-perf/certificates.go +++ b/cmd/osquery-perf/certificates.go @@ -2,11 +2,13 @@ package main import ( "crypto/sha1" //nolint:gosec + "crypto/x509" "encoding/hex" "fmt" "maps" // osquery-perf shares one global math/rand RNG seeded from the --seed flag so load-test runs are reproducible "math/rand" //nolint:depguard + "slices" "strings" "time" @@ -151,12 +153,59 @@ func (a *agent) generateCertSpecs() []simulatedCert { a.churnPerHostCertSpecs() } - specs := make([]simulatedCert, 0, len(sharedCerts)+len(a.hostCertSpecs)) + specs := make([]simulatedCert, 0, len(sharedCerts)+len(a.hostCertSpecs)+len(a.scepCertSpecs)) specs = append(specs, sharedCerts...) specs = append(specs, a.hostCertSpecs...) + // Include the certs issued via Windows MDM SCEP exchanges (never churned; replaced only on re-issuance). Sorted + // by CSP unique ID so the report order is stable across refreshes. + for _, id := range slices.Sorted(maps.Keys(a.scepCertSpecs)) { + specs = append(specs, a.scepCertSpecs[id]) + } return specs } +// storeSCEPCertSpec records a certificate issued during a Windows MDM SCEP exchange +func (a *agent) storeSCEPCertSpec(uniqueID string, cert *x509.Certificate) { + a.certificatesMutex.Lock() + defer a.certificatesMutex.Unlock() + if a.scepCertSpecs == nil { + a.scepCertSpecs = make(map[string]simulatedCert) + } + a.scepCertSpecs[uniqueID] = scepIssuedCertSpec(cert) +} + +// scepIssuedCertSpec converts a certificate issued during a Windows MDM SCEP exchange into a simulatedCert. The +// cert's subject carries the fleet- renewal-ID marker (expanded from the profile's +// $FLEET_VAR_SCEP_RENEWAL_ID), which the server matches on ingestion to flip the SCEP profile to verified. Reported +// machine-scoped: osquery-perf drives SCEP CSPs on the device channel, so the cert lands in the LocalMachine store. +func scepIssuedCertSpec(cert *x509.Certificate) simulatedCert { + return simulatedCert{ + commonName: cert.Subject.CommonName, + subjectCommonName: cert.Subject.CommonName, + subjectOrg: firstOrEmpty(cert.Subject.Organization), + // Join multiple OUs with "+OU=", mirroring how osquery reports multi-OU certs + subjectOrgUnit: strings.Join(cert.Subject.OrganizationalUnit, "+OU="), + subjectCountry: firstOrEmpty(cert.Subject.Country), + issuerCommonName: cert.Issuer.CommonName, + issuerOrg: firstOrEmpty(cert.Issuer.Organization), + issuerCountry: firstOrEmpty(cert.Issuer.Country), + keyAlgorithm: "rsaEncryption", + keyStrength: "2048", + keyUsage: "Key Encipherment, Digital Signature", + signingAlgorithm: cert.SignatureAlgorithm.String(), + serial: cert.SerialNumber.String(), + notValidAfterUnix: fmt.Sprint(cert.NotAfter.Unix()), + notValidBeforeUnix: fmt.Sprint(cert.NotBefore.Unix()), + } +} + +func firstOrEmpty(vals []string) string { + if len(vals) == 0 { + return "" + } + return vals[0] +} + // newPerHostCertSpecs generates 0-10 certificates unique to this host func (a *agent) newPerHostCertSpecs() []simulatedCert { count := rand.Intn(11) // 0..10