Updated osquery-perf for certs (#48499)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #31294 

osquery-perf changes only

# 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

## Summary by CodeRabbit

* **New Features**
* Added a certificate inventory simulation to the performance tool,
including stable shared certificates and per-host generated
certificates.
* Enhanced macOS and Windows rendering to better match expected
certificate query output (including user vs machine scope formatting).

* **Bug Fixes**
* Improved per-host certificate cache behavior with realistic
refresh/churn.
* Added support for duplicated certificate identities across different
scopes.

* **Chores**
* Updated linting configuration to suppress gosec findings for the new
certificate simulation workload.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-07-07 16:30:11 +01:00
committed by GitHub
parent 8f3624cf0a
commit da59512037
3 changed files with 392 additions and 156 deletions
+3
View File
@@ -260,6 +260,9 @@ linters:
- path: cmd/osquery-perf/agent.go
linters:
- gosec
- path: cmd/osquery-perf/certificates.go
linters:
- gosec
- path: cmd/fleet/serve.go
linters:
- gosec
+4 -156
View File
@@ -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
+385
View File
@@ -0,0 +1,385 @@
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).
// churnPerHostCertSpecs may later rotate one of the pair and break the cross-scope match; not worth guarding against
// for a load-test simulator (the path is exercised on initial ingestion and this is a low-volume, per-host scenario).
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}
}