diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 52e45c3bf2e..710d225ee18 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -10,7 +10,6 @@ import ( "crypto/tls" "embed" "encoding/base64" - "encoding/hex" "encoding/json" "encoding/xml" "errors" @@ -545,12 +544,11 @@ type agent struct { // a-ok for osquery-perf and load testing). bufferedResults map[resultLog]int - // cache of certificates returned by this agent. Note that this requires - // a mutex even though only used in a.processQuery, that's because both - // the runLoop and the live query goroutines may call DistributedWrite - // (which calls processQuery). + // 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 - certificatesCache []map[string]string + hostCertSpecs []simulatedCert commonSoftwareNameSuffix string entraIDDeviceID string @@ -2815,156 +2813,6 @@ func (a *agent) diskEncryptionLinux() []map[string]string { } } -func (a *agent) certificatesDarwin() []map[string]string { - a.certificatesMutex.RLock() - cache := a.certificatesCache - a.certificatesMutex.RUnlock() - - // 90% of the time certificates do not change - if rand.Intn(100) < 90 && len(cache) > 0 { - return cache - } - - // between 2 and 10 certificates (probably impossible to have 0, quick check - // on dogfood gives between 4-7) - count := rand.Intn(9) + 2 - - sources := []string{"system", "user"} - users := a.hostUsers() - const day = 24 * time.Hour - - results := make([]map[string]string, count) - for i := range count { - m := make(map[string]string, 12) - m["ca"] = fmt.Sprint(rand.Intn(2)) - m["common_name"] = uuid.NewString() - m["issuer"] = fmt.Sprintf("/C=US/O=Issuer %d Inc./CN=Issuer %d Common Name", i, i) - m["subject"] = fmt.Sprintf("/C=US/O=Subject %d Inc./OU=Subject %d Org Unit/CN=Subject %d Common Name", i, i, i) - m["key_algorithm"] = "rsaEncryption" - m["key_strength"] = "2048" - m["key_usage"] = "Data Encipherment, Key Encipherment, Digital Signature" - m["serial"] = uuid.NewString() - m["signing_algorithm"] = "sha256WithRSAEncryption" - // generate so that it may be expired - m["not_valid_after"] = fmt.Sprint(time.Now().Add(-1 * day).Add(time.Duration(rand.Intn(100)) * day).Unix()) - // notBefore is always in the past (1-10 days in the past) - m["not_valid_before"] = fmt.Sprint(time.Now().Add(-time.Duration(rand.Intn(10)+1) * day).Unix()) - rawHash := sha1.Sum([]byte(m["serial"])) //nolint: gosec - hash := hex.EncodeToString(rawHash[:]) - m["sha1"] = hash - m["source"] = sources[rand.Intn(2)] - - if m["source"] == "user" { - // Set username for user keychain certificates - user := users[rand.Intn(len(users))] - m["path"] = fmt.Sprintf(`/Users/%s/Library/Keychains/login.keychain-db`, user["username"]) - } - - results[i] = m - } - - a.certificatesMutex.Lock() - a.certificatesCache = results - a.certificatesMutex.Unlock() - return results -} - -func (a *agent) certificatesWindows() []map[string]string { - a.certificatesMutex.RLock() - cache := a.certificatesCache - a.certificatesMutex.RUnlock() - - // 90% of the time certificates do not change - if rand.Intn(100) < 90 && len(cache) > 0 { - return cache - } - - const day = 24 * time.Hour - - // custom SCEP profile ID used for certs issued via custom SCEP profiles (inserted by - // FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID) - // - // TODO: make this configurable as a loadtest agent parameter? for now, just hardcode it and try - // manipulating it in loadtest DB directly if needed. - profileIDCustomSCEP := "w2a6fd2c4-0018-4bdc-8046-c7342962b576" - - // when windows hosts enroll to Fleet MDM, we issue them a unique cert during the WSTEP/SCEP process - uuidFleetSCEP := uuid.NewString() - - // uuids that we'll use in serials and hashes to ensure uniqueness - serial1 := uuid.NewString() - s1 := sha1.Sum([]byte(serial1)) //nolint: gosec - - serial2 := uuid.NewString() - s2 := sha1.Sum([]byte(serial2)) //nolint: gosec - - // Fleet SCEP cert example based on data from a real Windows host - c1 := map[string]string{ - "ca": "-1", - "common_name": uuidFleetSCEP, - "subject": "Fleet, " + uuidFleetSCEP, - "issuer": "\"\", scep-ca, SCEP CA, FleetDM", - "key_algorithm": "RSA", - "key_strength": "2160", - "key_usage": "CERT_KEY_ENCIPHERMENT_KEY_USAGE,CERT_DIGITAL_SIGNATURE_KEY_USAGE", - "signing_algorithm": "sha256RSA", - // generate so that it may be expired - "not_valid_after": fmt.Sprint(time.Now().Add(-1 * day).Add(time.Duration(rand.Intn(100)) * day).Unix()), - // notBefore is always in the past (1-10 days in the past) - "not_valid_before": fmt.Sprint(time.Now().Add(-time.Duration(rand.Intn(10)+1) * day).Unix()), - "serial": serial1, - "sha1": hex.EncodeToString(s1[:]), - "username": "Admin", - "path": "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000\\Personal", - } - // Custom SCEP cert example based on data from a real Windows host - c2 := map[string]string{ - "ca": "-1", - "common_name": fmt.Sprintf("%s User\n CN", profileIDCustomSCEP), - "subject": fmt.Sprintf("fleet-%s, \"%s User\n CN\"", profileIDCustomSCEP, profileIDCustomSCEP), - "issuer": "US, scep-ca, SCEP CA, MICROMDM SCEP CA", - "key_algorithm": "RSA", - "key_strength": "1120", - "key_usage": "CERT_DIGITAL_SIGNATURE_KEY_USAGE", - "signing_algorithm": "sha256RSA", - // generate so that it may be expired - "not_valid_after": fmt.Sprint(time.Now().Add(-1 * day).Add(time.Duration(rand.Intn(100)) * day).Unix()), - // notBefore is always in the past (1-10 days in the past) - "not_valid_before": fmt.Sprint(time.Now().Add(-time.Duration(rand.Intn(10)+1) * day).Unix()), - "serial": serial2, - "sha1": hex.EncodeToString(s2[:]), - "username": "Admin", - "path": "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000\\Personal", - } - - // We'll use the examples above to create rows with minor variations, similar to what - // we would get from a real Windows host. - c3 := maps.Clone(c1) - c3["username"] = "SYSTEM" - c3["path"] = "Users\\S-1-5-18\\Personal" - - c4 := maps.Clone(c1) - c4["username"] = "SYSTEM" - c4["path"] = "CurrentUser\\Personal" - - c5 := maps.Clone(c1) - c5["username"] = "SYSTEM" - c5["path"] = "Users\\S-1-5-18\\Personal" - - c6 := maps.Clone(c1) - c6["path"] = "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000_Classes\\Personal" - - c7 := maps.Clone(c2) - c7["path"] = "Users\\S-1-5-21-1043593016-4249271388-1765263865-1000_Classes\\Personal" - - rows := []map[string]string{c1, c2, c3, c4, c5, c6, c7} - - a.certificatesMutex.Lock() - a.certificatesCache = rows - a.certificatesMutex.Unlock() - return rows -} - func (a *agent) orbitInfo() []map[string]string { version := "1.22.0" desktopVersion := version diff --git a/cmd/osquery-perf/certificates.go b/cmd/osquery-perf/certificates.go new file mode 100644 index 00000000000..8509f29c20a --- /dev/null +++ b/cmd/osquery-perf/certificates.go @@ -0,0 +1,383 @@ +package main + +import ( + "crypto/sha1" //nolint:gosec + "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 + "strings" + "time" + + "github.com/google/uuid" +) + +// simulatedCert is a platform-neutral description of a certificate that osquery-perf reports for the `certificates` +// detail query. +type simulatedCert struct { + ca bool + commonName string + subjectCommonName string + subjectOrg string + subjectOrgUnit string + subjectCountry string + issuerCommonName string + issuerOrg string + issuerCountry string + keyAlgorithm string + keyStrength string + keyUsage string + signingAlgorithm string + serial string + notValidAfterUnix string + notValidBeforeUnix string + // user reports whether the certificate lives in a user's store (true) or in the machine/system store (false). + user bool + username string +} + +// sha1Hex returns the hex-encoded SHA1 osquery would report for this cert. It is derived from the serial so that shared +// certs (fixed serial) dedupe to a single host_certificates row across all hosts, while per-host certs (uuid serial) +// stay unique per host. +func (c simulatedCert) sha1Hex() string { + sum := sha1.Sum([]byte(c.serial)) //nolint: gosec + return hex.EncodeToString(sum[:]) +} + +// sharedCerts are reported by every simulated host (common root and intermediate CAs). +var sharedCerts = []simulatedCert{ + { + ca: true, commonName: "Fleet Root CA", + subjectCommonName: "Fleet Root CA", subjectOrg: "Fleet Device Management Inc.", subjectCountry: "US", + issuerCommonName: "Fleet Root CA", issuerOrg: "Fleet Device Management Inc.", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "4096", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-fleet-root-ca", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", // 2020-01-01 .. 2030-01-01 + }, + { + ca: true, commonName: "Fleet Intermediate CA", + subjectCommonName: "Fleet Intermediate CA", subjectOrg: "Fleet Device Management Inc.", subjectOrgUnit: "Issuing", subjectCountry: "US", + issuerCommonName: "Fleet Root CA", issuerOrg: "Fleet Device Management Inc.", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "2048", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-fleet-intermediate-ca", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "DigiCert Global Root CA", + subjectCommonName: "DigiCert Global Root CA", subjectOrg: "DigiCert Inc", subjectCountry: "US", + issuerCommonName: "DigiCert Global Root CA", issuerOrg: "DigiCert Inc", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "2048", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-digicert-global-root-ca", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "Microsoft Root Certificate Authority 2011", + subjectCommonName: "Microsoft Root Certificate Authority 2011", subjectOrg: "Microsoft Corporation", subjectCountry: "US", + issuerCommonName: "Microsoft Root Certificate Authority 2011", issuerOrg: "Microsoft Corporation", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "4096", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-microsoft-root-ca-2011", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "GlobalSign Root CA", + subjectCommonName: "GlobalSign Root CA", subjectOrg: "GlobalSign nv-sa", subjectOrgUnit: "Root CA", subjectCountry: "BE", + issuerCommonName: "GlobalSign Root CA", issuerOrg: "GlobalSign nv-sa", issuerCountry: "BE", + keyAlgorithm: "rsaEncryption", keyStrength: "2048", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-globalsign-root-ca", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "USERTrust RSA Certification Authority", + subjectCommonName: "USERTrust RSA Certification Authority", subjectOrg: "The USERTRUST Network", subjectCountry: "US", + issuerCommonName: "USERTrust RSA Certification Authority", issuerOrg: "The USERTRUST Network", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "4096", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha384WithRSAEncryption", serial: "osquery-perf-shared-usertrust-rsa-ca", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "ISRG Root X1", + subjectCommonName: "ISRG Root X1", subjectOrg: "Internet Security Research Group", subjectCountry: "US", + issuerCommonName: "ISRG Root X1", issuerOrg: "Internet Security Research Group", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "4096", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-isrg-root-x1", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "Amazon Root CA 1", + subjectCommonName: "Amazon Root CA 1", subjectOrg: "Amazon", subjectCountry: "US", + issuerCommonName: "Amazon Root CA 1", issuerOrg: "Amazon", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "2048", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-amazon-root-ca-1", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "Baltimore CyberTrust Root", + subjectCommonName: "Baltimore CyberTrust Root", subjectOrg: "Baltimore", subjectOrgUnit: "CyberTrust", subjectCountry: "IE", + issuerCommonName: "Baltimore CyberTrust Root", issuerOrg: "Baltimore", issuerCountry: "IE", + keyAlgorithm: "rsaEncryption", keyStrength: "2048", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-baltimore-cybertrust-root", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, + { + ca: true, commonName: "Entrust Root Certification Authority - G2", + subjectCommonName: "Entrust Root Certification Authority - G2", subjectOrg: "Entrust, Inc.", subjectOrgUnit: "See www.entrust.net/legal-terms", subjectCountry: "US", + issuerCommonName: "Entrust Root Certification Authority - G2", issuerOrg: "Entrust, Inc.", issuerCountry: "US", + keyAlgorithm: "rsaEncryption", keyStrength: "2048", keyUsage: "Certificate Signing, CRL Signing", + signingAlgorithm: "sha256WithRSAEncryption", serial: "osquery-perf-shared-entrust-root-ca-g2", + notValidBeforeUnix: "1577836800", notValidAfterUnix: "1893456000", + }, +} + +const certDay = 24 * time.Hour + +// certChurnPercent is the percent chance that some of a host's per-host certificates rotate (new serial and SHA1), +// simulating certificate renewal/reinstall. It is rolled each time the host answers the certificates detail query, +// i.e. on the periodic detail refresh (osquery.detail_update_interval, 1h by default) or a forced refetch. Shared +// certs never churn. +const certChurnPercent = 5 + +// generateCertSpecs returns the certs this host reports: the constant shared certs plus this host's per-host certs. +// Per-host certs are generated once and cached so they're stable across detail-query refreshes, then occasionally +// churned to simulate certificate rotation/installs. Shared certs are never churned. +func (a *agent) generateCertSpecs() []simulatedCert { + a.certificatesMutex.Lock() + defer a.certificatesMutex.Unlock() + + switch { + case a.hostCertSpecs == nil: + a.hostCertSpecs = a.newPerHostCertSpecs() + case rand.Intn(100) < certChurnPercent: + a.churnPerHostCertSpecs() + } + + specs := make([]simulatedCert, 0, len(sharedCerts)+len(a.hostCertSpecs)) + specs = append(specs, sharedCerts...) + specs = append(specs, a.hostCertSpecs...) + return specs +} + +// newPerHostCertSpecs generates 0-10 certificates unique to this host +func (a *agent) newPerHostCertSpecs() []simulatedCert { + count := rand.Intn(11) // 0..10 + users := a.hostUsers() + specs := make([]simulatedCert, 0, count+1) + for i := range count { + specs = append(specs, a.newPerHostCertSpec(i, users)) + } + // Model a device certificate present in both the machine store and a user's store (same SHA1, two scopes), + // exercising the server's cross-scope handling (one host_certificates row, two host_certificate_sources rows). + if count > 0 && len(users) > 0 { + dup := specs[0] + dup.user = !specs[0].user + if dup.user { + dup.username = users[rand.Intn(len(users))]["username"] + } else { + dup.username = "" + } + specs = append(specs, dup) + } + return specs +} + +func (a *agent) newPerHostCertSpec(i int, users []map[string]string) simulatedCert { + user := rand.Intn(2) == 0 && len(users) > 0 + username := "" + if user { + username = users[rand.Intn(len(users))]["username"] + } + return simulatedCert{ + commonName: uuid.NewString(), + subjectCommonName: fmt.Sprintf("Subject %d Common Name", i), + subjectOrg: fmt.Sprintf("Subject %d Inc.", i), + subjectOrgUnit: fmt.Sprintf("Subject %d Org Unit", i), + subjectCountry: "US", + issuerCommonName: fmt.Sprintf("Issuer %d Common Name", i), + issuerOrg: fmt.Sprintf("Issuer %d Inc.", i), + issuerCountry: "US", + keyAlgorithm: "rsaEncryption", + keyStrength: "2048", + keyUsage: "Data Encipherment, Key Encipherment, Digital Signature", + signingAlgorithm: "sha256WithRSAEncryption", + serial: uuid.NewString(), + // generate so that it may be expired (notAfter in [-1d, +99d]) + notValidAfterUnix: fmt.Sprint(time.Now().Add(-1 * certDay).Add(time.Duration(rand.Intn(100)) * certDay).Unix()), + // notBefore is always in the past (1-10 days) + notValidBeforeUnix: fmt.Sprint(time.Now().Add(-time.Duration(rand.Intn(10)+1) * certDay).Unix()), + user: user, + username: username, + } +} + +// churnPerHostCertSpecs rotates 1..N of this host's per-host certs by assigning new serials (and thus new SHA1s), +// simulating certificate renewal/reinstall. +func (a *agent) churnPerHostCertSpecs() { + if len(a.hostCertSpecs) == 0 { + return + } + n := rand.Intn(min(10, len(a.hostCertSpecs))) + 1 + for range n { + idx := rand.Intn(len(a.hostCertSpecs)) + a.hostCertSpecs[idx].serial = uuid.NewString() + a.hostCertSpecs[idx].commonName = uuid.NewString() + a.hostCertSpecs[idx].notValidAfterUnix = fmt.Sprint(time.Now().Add(-1 * certDay).Add(time.Duration(rand.Intn(100)) * certDay).Unix()) + } +} + +func boolStr(b bool) string { + if b { + return "1" + } + return "0" +} + +// darwinDN renders a slash-delimited distinguished name (e.g. /C=US/O=Org/OU=Unit/CN=Name) as osquery returns on macOS. +// Empty fields are omitted. +func darwinDN(country, org, orgUnit, commonName string) string { + var b strings.Builder + if country != "" { + b.WriteString("/C=" + escapeDarwinDNValue(country)) + } + if org != "" { + b.WriteString("/O=" + escapeDarwinDNValue(org)) + } + if orgUnit != "" { + b.WriteString("/OU=" + escapeDarwinDNValue(orgUnit)) + } + if commonName != "" { + b.WriteString("/CN=" + escapeDarwinDNValue(commonName)) + } + return b.String() +} + +// escapeDarwinDNValue backslash-escapes slashes inside an attribute value, as osquery does on macOS +func escapeDarwinDNValue(v string) string { + return strings.ReplaceAll(v, "/", `\/`) +} + +// windowsDN renders an X.500 (RFC 1779) distinguished name (e.g. "CN=Name, O=Org, OU=Unit, C=US") as osquery returns in +// subject2/issuer2 on Windows starting with osquery 5.23.1. +func windowsDN(country, org, orgUnit, commonName string) string { + var parts []string + if commonName != "" { + parts = append(parts, "CN="+quoteX500Value(commonName)) + } + if org != "" { + parts = append(parts, "O="+quoteX500Value(org)) + } + if orgUnit != "" { + parts = append(parts, "OU="+quoteX500Value(orgUnit)) + } + if country != "" { + parts = append(parts, "C="+quoteX500Value(country)) + } + return strings.Join(parts, ", ") +} + +// quoteX500Value double-quotes an attribute value that contains a comma (doubling any embedded quotes), as +// CERT_X500_NAME_STR does, e.g. O="Entrust, Inc.". +func quoteX500Value(v string) string { + if !strings.ContainsAny(v, `,"`) { + return v + } + return `"` + strings.ReplaceAll(v, `"`, `""`) + `"` +} + +// windowsUserSID returns a stable per-(host, user) security identifier so a user's certs classify as User scope and stay +// consistent across detail-query refreshes. +func (a *agent) windowsUserSID(username string) string { + var h uint32 = 2166136261 + for i := 0; i < len(username); i++ { + h = (h ^ uint32(username[i])) * 16777619 + } + rid := 1000 + int(h%5000) + return fmt.Sprintf("S-1-5-21-%d-%d-%d-%d", 1000000000+a.agentIndex, 2000000000, 3000000000, rid) +} + +func (a *agent) certificatesDarwin() []map[string]string { + specs := a.generateCertSpecs() + rows := make([]map[string]string, 0, len(specs)) + for _, c := range specs { + rows = append(rows, c.darwinRow()) + } + return rows +} + +func (c simulatedCert) darwinRow() map[string]string { + source := "system" + path := "/Library/Keychains/System.keychain" + if c.user { + source = "user" + path = fmt.Sprintf("/Users/%s/Library/Keychains/login.keychain-db", c.username) + } + return map[string]string{ + "ca": boolStr(c.ca), + "common_name": c.commonName, + "subject": darwinDN(c.subjectCountry, c.subjectOrg, c.subjectOrgUnit, c.subjectCommonName), + "issuer": darwinDN(c.issuerCountry, c.issuerOrg, "", c.issuerCommonName), + "key_algorithm": c.keyAlgorithm, + "key_strength": c.keyStrength, + "key_usage": c.keyUsage, + "signing_algorithm": c.signingAlgorithm, + "not_valid_after": c.notValidAfterUnix, + "not_valid_before": c.notValidBeforeUnix, + "serial": c.serial, + "sha1": c.sha1Hex(), + "source": source, + "path": path, + } +} + +func (a *agent) certificatesWindows() []map[string]string { + specs := a.generateCertSpecs() + // User certs are enumerated from more than one hive, so allocate room for ~2 + // rows per spec. + rows := make([]map[string]string, 0, len(specs)*2) + for _, c := range specs { + rows = append(rows, a.windowsRows(c)...) + } + return rows +} + +// windowsRows renders the osquery `certificates` rows for a cert on Windows. Machine-scoped certs produce one row. +// User-scoped certs produce the redundant rows osquery returns from the user's Personal hive and its companion _Classes +// hive (the Fleet server dedupes them by SHA1 + scope + username). +func (a *agent) windowsRows(c simulatedCert) []map[string]string { + base := map[string]string{ + "ca": boolStr(c.ca), + "common_name": c.commonName, + // subject2/issuer2 are the X.500 distinguished name columns Fleet's Windows certificates query selects, + // populated on Windows starting with osquery 5.23.1. + "subject2": windowsDN(c.subjectCountry, c.subjectOrg, c.subjectOrgUnit, c.subjectCommonName), + "issuer2": windowsDN(c.issuerCountry, c.issuerOrg, "", c.issuerCommonName), + "key_algorithm": c.keyAlgorithm, + "key_strength": c.keyStrength, + "key_usage": c.keyUsage, + "signing_algorithm": c.signingAlgorithm, + "not_valid_after": c.notValidAfterUnix, + "not_valid_before": c.notValidBeforeUnix, + "serial": c.serial, + "sha1": c.sha1Hex(), + } + + if !c.user { + row := maps.Clone(base) + row["sid"] = "" + row["username"] = "" + row["store_location"] = "LocalMachine" + row["path"] = "LocalMachine\\Personal" + return []map[string]string{row} + } + + sid := a.windowsUserSID(c.username) + personal := maps.Clone(base) + personal["sid"] = sid + personal["username"] = c.username + personal["store_location"] = "Users" + personal["path"] = fmt.Sprintf("Users\\%s\\Personal", sid) + + classes := maps.Clone(personal) + classes["path"] = fmt.Sprintf("Users\\%s_Classes\\Personal", sid) + + return []map[string]string{personal, classes} +}