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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
Reference in New Issue
Block a user