<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #45550 # Checklist for submitter - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced Windows MDM SCEP certificate simulation to track issued certificate specs per host during check-ins. * **Bug Fixes** * Certificate data generation is now stably ordered and consistent across refreshes. * SCEP certificate processing now ignores failed installs (no certificate) and records only valid issued cert details. * **Testing** * A small subset of simulated agents may withhold one SCEP certificate to exercise verification edge cases. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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-<profileUUID> 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 {
|
||||
|
||||
@@ -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-<profileUUID> 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
|
||||
|
||||
Reference in New Issue
Block a user