From 62b74122432544317a0487ba30423ed6b26abe77 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 22 Jan 2025 14:54:40 -0600 Subject: [PATCH 01/22] Disk encryption keys are now archived when created/updated (#25638) For #25609 Manual QA in progress. Putting this "In Review" since it is a P1. Video explaining the PR: https://youtu.be/bUwIdjBLqiM # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality --- changes/25609-archive-encryption-keys | 1 + server/datastore/mysql/apple_mdm.go | 38 --- server/datastore/mysql/apple_mdm_test.go | 19 +- server/datastore/mysql/disk_encryption.go | 305 ++++++++++++++++++ .../datastore/mysql/disk_encryption_test.go | 35 ++ server/datastore/mysql/hosts.go | 141 -------- server/datastore/mysql/hosts_test.go | 95 ++++-- server/datastore/mysql/labels_test.go | 6 +- server/datastore/mysql/linux_mdm_test.go | 10 +- server/datastore/mysql/microsoft_mdm.go | 2 +- server/datastore/mysql/microsoft_mdm_test.go | 12 +- ...094045_AddHostDiskEncryptionKeysArchive.go | 68 ++++ ...5_AddHostDiskEncryptionKeysArchive_test.go | 60 ++++ server/datastore/mysql/schema.sql | 26 +- server/fleet/datastore.go | 4 +- server/mock/datastore_mock.go | 12 +- server/service/integration_mdm_test.go | 14 +- server/service/orbit.go | 4 +- server/service/orbit_test.go | 5 +- server/service/osquery_utils/queries.go | 4 +- server/service/osquery_utils/queries_test.go | 5 +- 21 files changed, 599 insertions(+), 267 deletions(-) create mode 100644 changes/25609-archive-encryption-keys create mode 100644 server/datastore/mysql/disk_encryption.go create mode 100644 server/datastore/mysql/disk_encryption_test.go create mode 100644 server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive.go create mode 100644 server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive_test.go diff --git a/changes/25609-archive-encryption-keys b/changes/25609-archive-encryption-keys new file mode 100644 index 000000000000..a3848afbd53a --- /dev/null +++ b/changes/25609-archive-encryption-keys @@ -0,0 +1 @@ +Disk encryption keys are now archived when they are created or updated. They are never fully deleted from the database. diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index c7383c91653e..797dbaf5fca5 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -3551,27 +3551,6 @@ func (ds *Datastore) CleanupUnusedBootstrapPackages(ctx context.Context, pkgStor return ctxerr.Wrap(ctx, err, "cleanup unused bootstrap packages") } -func (ds *Datastore) CleanupDiskEncryptionKeysOnTeamChange(ctx context.Context, hostIDs []uint, newTeamID *uint) error { - return ds.withTx(ctx, func(tx sqlx.ExtContext) error { - return cleanupDiskEncryptionKeysOnTeamChangeDB(ctx, tx, hostIDs, newTeamID) - }) -} - -func cleanupDiskEncryptionKeysOnTeamChangeDB(ctx context.Context, tx sqlx.ExtContext, hostIDs []uint, newTeamID *uint) error { - _, err := getMDMAppleConfigProfileByTeamAndIdentifierDB(ctx, tx, newTeamID, mobileconfig.FleetFileVaultPayloadIdentifier) - if err != nil { - if fleet.IsNotFound(err) { - // the new team does not have a filevault profile so we need to delete the existing ones - if err := bulkDeleteHostDiskEncryptionKeysDB(ctx, tx, hostIDs); err != nil { - return ctxerr.Wrap(ctx, err, "reconcile filevault profiles on team change bulk delete host disk encryption keys") - } - } else { - return ctxerr.Wrap(ctx, err, "reconcile filevault profiles on team change get profile") - } - } - return nil -} - func getMDMAppleConfigProfileByTeamAndIdentifierDB(ctx context.Context, tx sqlx.QueryerContext, teamID *uint, profileIdentifier string) (*fleet.MDMAppleConfigProfile, error) { if teamID == nil { teamID = ptr.Uint(0) @@ -3603,23 +3582,6 @@ WHERE return &profile, nil } -func bulkDeleteHostDiskEncryptionKeysDB(ctx context.Context, tx sqlx.ExtContext, hostIDs []uint) error { - if len(hostIDs) == 0 { - return nil - } - - query, args, err := sqlx.In( - "DELETE FROM host_disk_encryption_keys WHERE host_id IN (?)", - hostIDs, - ) - if err != nil { - return ctxerr.Wrap(ctx, err, "building query") - } - - _, err = tx.ExecContext(ctx, query, args...) - return err -} - func (ds *Datastore) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst *fleet.MDMAppleSetupAssistant) (*fleet.MDMAppleSetupAssistant, error) { const stmt = ` INSERT INTO diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 84a284ac466a..a67e73908df8 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -984,7 +984,7 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) { var hostID uint err = sqlx.GetContext(context.Background(), ds.reader(context.Background()), &hostID, `SELECT id FROM hosts WHERE uuid = ?`, testUUID) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostID, "asdf", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, &fleet.Host{ID: hostID}, "asdf", "", nil) require.NoError(t, err) key, err := ds.GetHostDiskEncryptionKey(ctx, hostID) @@ -1999,7 +1999,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "foo", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "foo", "", nil) require.NoError(t, err) res, err = ds.GetMDMAppleProfilesSummary(ctx, nil) require.NoError(t, err) @@ -2041,7 +2041,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(1), res.Verified) // hosts[0] now has filevault fully enforced and verified - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1].ID, "bar", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1], "bar", "", nil) require.NoError(t, err) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[1].ID}, false, time.Now().Add(1*time.Hour)) require.NoError(t, err) @@ -2107,7 +2107,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[9].ID, "baz", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[9], "baz", "", nil) require.NoError(t, err) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour)) require.NoError(t, err) @@ -2652,10 +2652,11 @@ func testDeleteMDMAppleProfilesForHost(t *testing.T, ds *Datastore) { require.Nil(t, gotProfs) } -func createDiskEncryptionRecord(ctx context.Context, ds *Datastore, t *testing.T, hostId uint, key string, decryptable bool, threshold time.Time) { - err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostId, key, "", nil) +func createDiskEncryptionRecord(ctx context.Context, ds *Datastore, t *testing.T, host *fleet.Host, key string, decryptable bool, + threshold time.Time) { + err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, key, "", nil) require.NoError(t, err) - err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hostId}, decryptable, threshold) + err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, decryptable, threshold) require.NoError(t, err) } @@ -2685,7 +2686,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { ctx, ds, t, ) oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute) - createDiskEncryptionRecord(ctx, ds, t, verifyingHost.ID, "key-1", true, oneMinuteAfterThreshold) + createDiskEncryptionRecord(ctx, ds, t, verifyingHost, "key-1", true, oneMinuteAfterThreshold) fvProfileSummary, err := ds.GetMDMAppleFileVaultSummary(ctx, nil) require.NoError(t, err) @@ -2871,7 +2872,7 @@ func TestMDMAppleFileVaultSummary(t *testing.T) { require.NoError(t, err) upsertHostCPs([]*fleet.Host{verifyingTeam1Host}, []*fleet.MDMAppleConfigProfile{team1FVProfile}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t) - createDiskEncryptionRecord(ctx, ds, t, verifyingTeam1Host.ID, "key-2", true, oneMinuteAfterThreshold) + createDiskEncryptionRecord(ctx, ds, t, verifyingTeam1Host, "key-2", true, oneMinuteAfterThreshold) fvProfileSummary, err = ds.GetMDMAppleFileVaultSummary(ctx, &tm.ID) require.NoError(t, err) diff --git a/server/datastore/mysql/disk_encryption.go b/server/datastore/mysql/disk_encryption.go new file mode 100644 index 000000000000..96f278a68ab9 --- /dev/null +++ b/server/datastore/mysql/disk_encryption.go @@ -0,0 +1,305 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" + "github.com/go-kit/log/level" + "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" +) + +type encryptionKey struct { + Base string `db:"base64_encrypted"` + Salt string `db:"base64_encrypted_salt"` + KeySlot *uint + CreatedAt time.Time + NotFound bool +} + +func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, host *fleet.Host, encryptedBase64Key, clientError string, + decryptable *bool) error { + + existingKey, err := ds.getExistingHostDiskEncryptionKey(ctx, host) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting existing key, if present") + } + + // We use the same timestamp for base and archive tables so that it can be used as an additional debug tool if needed. + var incomingKey = encryptionKey{Base: encryptedBase64Key, CreatedAt: time.Now().UTC()} + err = ds.archiveHostDiskEncryptionKey(ctx, host, incomingKey, existingKey) + if err != nil { + return ctxerr.Wrap(ctx, err, "archiving key") + } + + if existingKey.NotFound { + _, err = ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, client_error, decryptable, created_at) +VALUES + (?, ?, ?, ?, ?)`, host.ID, incomingKey.Base, clientError, decryptable, incomingKey.CreatedAt) + if err == nil { + return nil + } + var mysqlErr *mysql.MySQLError + switch { + case errors.As(err, &mysqlErr) && mysqlErr.Number == 1062: + level.Error(ds.logger).Log("msg", "Primary key already exists in host_disk_encryption_keys. Falling back to update", "host_id", + host) + // This should never happen unless there is a bug in the code or an infra issue (like huge replication lag). + default: + return ctxerr.Wrap(ctx, err, "inserting key") + } + } + + _, err = ds.writer(ctx).ExecContext(ctx, ` +UPDATE host_disk_encryption_keys SET + /* if the key has changed, set decrypted to its initial value so it can be calculated again if necessary (if null) */ + decryptable = IF( + base64_encrypted = ? AND base64_encrypted != '', + decryptable, + ? + ), + base64_encrypted = ?, + client_error = ? +WHERE host_id = ? +`, incomingKey.Base, decryptable, incomingKey.Base, clientError, host.ID) + if err != nil { + return ctxerr.Wrap(ctx, err, "updating key") + } + return nil +} + +func (ds *Datastore) getExistingHostDiskEncryptionKey(ctx context.Context, host *fleet.Host) (encryptionKey, error) { + getExistingKeyStmt := `SELECT base64_encrypted, base64_encrypted_salt FROM host_disk_encryption_keys WHERE host_id = ?` + var existingKey encryptionKey + err := sqlx.GetContext(ctx, ds.reader(ctx), &existingKey, getExistingKeyStmt, host.ID) + switch { + case errors.Is(err, sql.ErrNoRows): + // no existing key, proceed to insert + existingKey.NotFound = true + case err != nil: + return encryptionKey{}, ctxerr.Wrap(ctx, err, "getting existing key") + } + return existingKey, nil +} + +func (ds *Datastore) archiveHostDiskEncryptionKey(ctx context.Context, host *fleet.Host, incomingKey encryptionKey, + existingKey encryptionKey) error { + // We archive only valid and different keys to reduce noise. + if (incomingKey.Base != "" && existingKey.Base != incomingKey.Base) || + (incomingKey.Salt != "" && existingKey.Salt != incomingKey.Salt) { + const insertKeyIntoArchiveStmt = ` +INSERT INTO host_disk_encryption_keys_archive (host_id, hardware_serial, base64_encrypted, base64_encrypted_salt, key_slot, created_at) +VALUES (?, ?, ?, ?, ?, ?)` + _, err := ds.writer(ctx).ExecContext(ctx, insertKeyIntoArchiveStmt, host.ID, host.HardwareSerial, incomingKey.Base, + incomingKey.Salt, + incomingKey.KeySlot, incomingKey.CreatedAt) + if err != nil { + return ctxerr.Wrap(ctx, err, "inserting key into archive") + } + } + return nil +} + +func (ds *Datastore) SaveLUKSData(ctx context.Context, host *fleet.Host, encryptedBase64Passphrase string, encryptedBase64Salt string, + keySlot uint) error { + if encryptedBase64Passphrase == "" || encryptedBase64Salt == "" { // should have been caught at service level + return errors.New("passphrase and salt must be set") + } + + existingKey, err := ds.getExistingHostDiskEncryptionKey(ctx, host) + if err != nil { + return ctxerr.Wrap(ctx, err, "getting existing LUKS key, if present") + } + + // We use the same timestamp for base and archive tables so that it can be used as an additional debug tool if needed. + var incomingKey = encryptionKey{Base: encryptedBase64Passphrase, Salt: encryptedBase64Salt, KeySlot: &keySlot, + CreatedAt: time.Now().UTC()} + err = ds.archiveHostDiskEncryptionKey(ctx, host, incomingKey, existingKey) + if err != nil { + return ctxerr.Wrap(ctx, err, "archiving LUKS key") + } + + if existingKey.NotFound { + _, err = ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, base64_encrypted_salt, key_slot, decryptable, created_at) +VALUES + (?, ?, ?, ?, TRUE, ?)`, host.ID, incomingKey.Base, incomingKey.Salt, incomingKey.KeySlot, incomingKey.CreatedAt) + if err == nil { + return nil + } + var mysqlErr *mysql.MySQLError + switch { + case errors.As(err, &mysqlErr) && mysqlErr.Number == 1062: + level.Error(ds.logger).Log("msg", "Primary key already exists in LUKS host_disk_encryption_keys. Falling back to update", + "host_id", + host) + // This should never happen unless there is a bug in the code or an infra issue (like huge replication lag). + default: + return ctxerr.Wrap(ctx, err, "inserting LUKS key") + } + } + + _, err = ds.writer(ctx).ExecContext(ctx, ` +UPDATE host_disk_encryption_keys SET + /* if the key has changed, set decrypted to its initial value so it can be calculated again if necessary (if null) */ + decryptable = TRUE, + base64_encrypted = ?, + base64_encrypted_salt = ?, + key_slot = ?, + client_error = '' +WHERE host_id = ? +`, incomingKey.Base, incomingKey.Salt, incomingKey.KeySlot, host.ID) + if err != nil { + return ctxerr.Wrap(ctx, err, "updating LUKS key") + } + return nil + +} + +func (ds *Datastore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool { + var pendingEscrowCount uint + _ = sqlx.GetContext(ctx, ds.reader(ctx), &pendingEscrowCount, ` + SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND reset_requested = TRUE`, hostID) + return pendingEscrowCount > 0 +} + +func (ds *Datastore) ClearPendingEscrow(ctx context.Context, hostID uint) error { + _, err := ds.writer(ctx).ExecContext(ctx, `UPDATE host_disk_encryption_keys SET reset_requested = FALSE WHERE host_id = ?`, hostID) + return err +} + +func (ds *Datastore) ReportEscrowError(ctx context.Context, hostID uint, errorMessage string) error { + _, err := ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, client_error) VALUES (?, '', ?) ON DUPLICATE KEY UPDATE client_error = VALUES(client_error) +`, hostID, errorMessage) + return err +} + +func (ds *Datastore) QueueEscrow(ctx context.Context, hostID uint) error { + _, err := ds.writer(ctx).ExecContext(ctx, ` +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, reset_requested) VALUES (?, '', TRUE) ON DUPLICATE KEY UPDATE reset_requested = TRUE +`, hostID) + return err +} + +func (ds *Datastore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error { + var hasKeyCount uint + err := sqlx.GetContext(ctx, ds.reader(ctx), &hasKeyCount, ` + SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND base64_encrypted != ''`, hostID) + if hasKeyCount > 0 { + return &fleet.BadRequestError{Message: "Key has already been escrowed for this host"} + } + + return err +} + +func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { + // NOTE(mna): currently we only verify encryption keys for macOS, + // Windows/bitlocker uses a different approach where orbit sends the + // encryption key and we encrypt it server-side with the WSTEP certificate, + // so it is always decryptable once received. + // + // To avoid sending Windows-related keys to verify as part of this call, we + // only return rows that have a non-empty encryption key (for Windows, the + // key is blanked if an error occurred trying to retrieve it on the host). + var keys []fleet.HostDiskEncryptionKey + err := sqlx.SelectContext(ctx, ds.reader(ctx), &keys, ` + SELECT + base64_encrypted, + host_id, + updated_at + FROM + host_disk_encryption_keys + WHERE + decryptable IS NULL AND + base64_encrypted != '' + `) + return keys, err +} + +func (ds *Datastore) SetHostsDiskEncryptionKeyStatus( + ctx context.Context, + hostIDs []uint, + decryptable bool, + threshold time.Time, +) error { + if len(hostIDs) == 0 { + return nil + } + + query, args, err := sqlx.In( + "UPDATE host_disk_encryption_keys SET decryptable = ? WHERE host_id IN (?) AND updated_at <= ?", + decryptable, hostIDs, threshold, + ) + if err != nil { + return err + } + _, err = ds.writer(ctx).ExecContext(ctx, query, args...) + return err +} + +func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) { + var key fleet.HostDiskEncryptionKey + err := sqlx.GetContext(ctx, ds.reader(ctx), &key, ` + SELECT + host_id, base64_encrypted, decryptable, updated_at, client_error + FROM + host_disk_encryption_keys + WHERE host_id = ?`, hostID) + if err != nil { + if err == sql.ErrNoRows { + msg := fmt.Sprintf("for host %d", hostID) + return nil, ctxerr.Wrap(ctx, notFound("HostDiskEncryptionKey").WithMessage(msg)) + } + return nil, ctxerr.Wrapf(ctx, err, "getting data from host_disk_encryption_keys for host_id %d", hostID) + } + return &key, nil +} + +func (ds *Datastore) CleanupDiskEncryptionKeysOnTeamChange(ctx context.Context, hostIDs []uint, newTeamID *uint) error { + return ds.withTx(ctx, func(tx sqlx.ExtContext) error { + return cleanupDiskEncryptionKeysOnTeamChangeDB(ctx, tx, hostIDs, newTeamID) + }) +} + +func cleanupDiskEncryptionKeysOnTeamChangeDB(ctx context.Context, tx sqlx.ExtContext, hostIDs []uint, newTeamID *uint) error { + // We are using Apple's encryption profile to determine if any hosts, including Windows and Linux, are encrypted. + // This is a safe assumption since encryption is enabled for the whole team. + _, err := getMDMAppleConfigProfileByTeamAndIdentifierDB(ctx, tx, newTeamID, mobileconfig.FleetFileVaultPayloadIdentifier) + if err != nil { + if fleet.IsNotFound(err) { + // the new team does not have a filevault profile so we need to delete the existing ones + if err := bulkDeleteHostDiskEncryptionKeysDB(ctx, tx, hostIDs); err != nil { + return ctxerr.Wrap(ctx, err, "reconcile filevault profiles on team change bulk delete host disk encryption keys") + } + } else { + return ctxerr.Wrap(ctx, err, "reconcile filevault profiles on team change get profile") + } + } + return nil +} + +func bulkDeleteHostDiskEncryptionKeysDB(ctx context.Context, tx sqlx.ExtContext, hostIDs []uint) error { + if len(hostIDs) == 0 { + return nil + } + + deleteStmt, deleteArgs, err := sqlx.In("DELETE FROM host_disk_encryption_keys WHERE host_id IN (?)", hostIDs) + if err != nil { + return ctxerr.Wrap(ctx, err, "building query") + } + + _, err = tx.ExecContext(ctx, deleteStmt, deleteArgs...) + return err +} diff --git a/server/datastore/mysql/disk_encryption_test.go b/server/datastore/mysql/disk_encryption_test.go new file mode 100644 index 000000000000..a55c2f0a0bf3 --- /dev/null +++ b/server/datastore/mysql/disk_encryption_test.go @@ -0,0 +1,35 @@ +package mysql + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDiskEncryption(t *testing.T) { + ds := CreateMySQLDS(t) + + cases := []struct { + name string + fn func(t *testing.T, ds *Datastore) + }{ + {"TestCleanupDiskEncryptionKeysOnTeamChange", testCleanupDiskEncryptionKeysOnTeamChange}, + } + + for _, c := range cases { + t.Helper() + t.Run(c.name, func(t *testing.T) { + defer TruncateTables(t, ds) + + c.fn(t, ds) + }) + } +} + +func testCleanupDiskEncryptionKeysOnTeamChange(t *testing.T, ds *Datastore) { + ctx := context.Background() + + // No-op test + assert.NoError(t, ds.CleanupDiskEncryptionKeysOnTeamChange(ctx, []uint{1, 2, 3}, nil)) +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index b93854bceea6..35318813b68a 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -3835,147 +3835,6 @@ func (ds *Datastore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID ) } -func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error { - _, err := ds.writer(ctx).ExecContext(ctx, ` -INSERT INTO host_disk_encryption_keys - (host_id, base64_encrypted, client_error, decryptable) -VALUES - (?, ?, ?, ?) -ON DUPLICATE KEY UPDATE - /* if the key has changed, set decrypted to its initial value so it can be calculated again if necessary (if null) */ - decryptable = IF( - base64_encrypted = VALUES(base64_encrypted) AND base64_encrypted != '', - decryptable, - VALUES(decryptable) - ), - base64_encrypted = VALUES(base64_encrypted), - client_error = VALUES(client_error) -`, hostID, encryptedBase64Key, clientError, decryptable) - return err -} - -func (ds *Datastore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error { - if encryptedBase64Passphrase == "" || encryptedBase64Salt == "" { // should have been caught at service level - return errors.New("passphrase and salt must be set") - } - - _, err := ds.writer(ctx).ExecContext(ctx, ` -INSERT INTO host_disk_encryption_keys - (host_id, base64_encrypted, base64_encrypted_salt, key_slot, client_error, decryptable) -VALUES - (?, ?, ?, ?, '', TRUE) -ON DUPLICATE KEY UPDATE - decryptable = TRUE, - base64_encrypted = VALUES(base64_encrypted), - base64_encrypted_salt = VALUES(base64_encrypted_salt), - key_slot = VALUES(key_slot), - client_error = '' -`, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot) - return err -} - -func (ds *Datastore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool { - var pendingEscrowCount uint - _ = sqlx.GetContext(ctx, ds.reader(ctx), &pendingEscrowCount, ` - SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND reset_requested = TRUE`, hostID) - return pendingEscrowCount > 0 -} - -func (ds *Datastore) ClearPendingEscrow(ctx context.Context, hostID uint) error { - _, err := ds.writer(ctx).ExecContext(ctx, `UPDATE host_disk_encryption_keys SET reset_requested = FALSE WHERE host_id = ?`, hostID) - return err -} - -func (ds *Datastore) ReportEscrowError(ctx context.Context, hostID uint, errorMessage string) error { - _, err := ds.writer(ctx).ExecContext(ctx, ` -INSERT INTO host_disk_encryption_keys - (host_id, base64_encrypted, client_error) VALUES (?, '', ?) ON DUPLICATE KEY UPDATE client_error = VALUES(client_error) -`, hostID, errorMessage) - return err -} - -func (ds *Datastore) QueueEscrow(ctx context.Context, hostID uint) error { - _, err := ds.writer(ctx).ExecContext(ctx, ` -INSERT INTO host_disk_encryption_keys - (host_id, base64_encrypted, reset_requested) VALUES (?, '', TRUE) ON DUPLICATE KEY UPDATE reset_requested = TRUE -`, hostID) - return err -} - -func (ds *Datastore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error { - var hasKeyCount uint - err := sqlx.GetContext(ctx, ds.reader(ctx), &hasKeyCount, ` - SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND base64_encrypted != ''`, hostID) - if hasKeyCount > 0 { - return &fleet.BadRequestError{Message: "Key has already been escrowed for this host"} - } - - return err -} - -func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { - // NOTE(mna): currently we only verify encryption keys for macOS, - // Windows/bitlocker uses a different approach where orbit sends the - // encryption key and we encrypt it server-side with the WSTEP certificate, - // so it is always decryptable once received. - // - // To avoid sending Windows-related keys to verify as part of this call, we - // only return rows that have a non-empty encryption key (for Windows, the - // key is blanked if an error occurred trying to retrieve it on the host). - var keys []fleet.HostDiskEncryptionKey - err := sqlx.SelectContext(ctx, ds.reader(ctx), &keys, ` - SELECT - base64_encrypted, - host_id, - updated_at - FROM - host_disk_encryption_keys - WHERE - decryptable IS NULL AND - base64_encrypted != '' - `) - return keys, err -} - -func (ds *Datastore) SetHostsDiskEncryptionKeyStatus( - ctx context.Context, - hostIDs []uint, - decryptable bool, - threshold time.Time, -) error { - if len(hostIDs) == 0 { - return nil - } - - query, args, err := sqlx.In( - "UPDATE host_disk_encryption_keys SET decryptable = ? WHERE host_id IN (?) AND updated_at <= ?", - decryptable, hostIDs, threshold, - ) - if err != nil { - return err - } - _, err = ds.writer(ctx).ExecContext(ctx, query, args...) - return err -} - -func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) { - var key fleet.HostDiskEncryptionKey - err := sqlx.GetContext(ctx, ds.reader(ctx), &key, ` - SELECT - host_id, base64_encrypted, decryptable, updated_at, client_error - FROM - host_disk_encryption_keys - WHERE host_id = ?`, hostID) - if err != nil { - if err == sql.ErrNoRows { - msg := fmt.Sprintf("for host %d", hostID) - return nil, ctxerr.Wrap(ctx, notFound("HostDiskEncryptionKey").WithMessage(msg)) - } - return nil, ctxerr.Wrapf(ctx, err, "getting data from host_disk_encryption_keys for host_id %d", hostID) - } - return &key, nil -} - func (ds *Datastore) SetOrUpdateHostOrbitInfo( ctx context.Context, hostID uint, version string, desktopVersion sql.NullString, scriptsEnabled sql.NullBool, ) error { diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 7b4319c5b0cc..f08c37bf5489 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -907,8 +907,8 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerifying}, 1) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsPending}, 5) // pending supported linux hosts - require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1].ID, "key1", "morton", 1)) // set host 1 to verified - require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed + require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1], "key1", "morton", 1)) // set host 1 to verified + require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerified}, 1) // hosts[1] listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsFailed}, 1) // hosts[2] @@ -941,8 +941,8 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID, hosts[5].ID})) listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsPending}, 5) // pending supported linux hosts - require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1].ID, "key1", "mutton", 2)) // set host 1 to verified - require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed + require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1], "key1", "mutton", 2)) // set host 1 to verified + require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerified}, 1) // hosts[1] listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsFailed}, 1) // hosts[2] @@ -3409,7 +3409,7 @@ func testHostsListMacOSSettingsDiskEncryptionStatus(t *testing.T, ds *Datastore) upsertHostCPs([]*fleet.Host{hosts[0], hosts[1]}, []*fleet.MDMAppleConfigProfile{noTeamFVProfile}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t) oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute) // host 0 needs to finish key rotation (action required), host 1 has finished key rotation but profile is verifying - createDiskEncryptionRecord(ctx, ds, t, hosts[1].ID, "key-1", true, oneMinuteAfterThreshold) + createDiskEncryptionRecord(ctx, ds, t, hosts[1], "key-1", true, oneMinuteAfterThreshold) listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerifying}, 1) listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 0) @@ -3429,7 +3429,7 @@ func testHostsListMacOSSettingsDiskEncryptionStatus(t *testing.T, ds *Datastore) listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionRemovingEnforcement}, 0) // simulate osquery ping from host 0 with unverified key after key rotation; should switch host to verifying - require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "key-1", "", nil)) + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "key-1", "", nil)) listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerifying}, 2) listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 0) @@ -6935,7 +6935,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { ) require.NoError(t, err) // set an encryption key - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host, "TESTKEY", "", nil) require.NoError(t, err) // set an mdm profile prof, err := ds.NewMDMAppleConfigProfile(context.Background(), *configProfileForTest(t, "N1", "I1", "U1")) @@ -8001,7 +8001,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) { // fill in disk encryption information require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true)) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hFleet.ID, "test-key", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hFleet, "test-key", "", nil) require.NoError(t, err) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hFleet.ID}, true, time.Now()) require.NoError(t, err) @@ -8033,6 +8033,14 @@ func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected require.NoError(t, err) require.Equal(t, expectedKey, got.Base64Encrypted) require.Equal(t, expectedDecryptable, got.Decryptable) + if expectedKey != "" { + var archiveKey string + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(context.Background(), q, &archiveKey, + `SELECT base64_encrypted FROM host_disk_encryption_keys_archive WHERE host_id = ? ORDER BY created_at DESC LIMIT 1`, hostID) + }) + assert.Equal(t, expectedKey, archiveKey) + } } func testLUKSDatastoreFunctions(t *testing.T, ds *Datastore) { @@ -8105,29 +8113,46 @@ func testLUKSDatastoreFunctions(t *testing.T, ds *Datastore) { require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID)) // no change when blank key or salt attempted to save - err = ds.SaveLUKSData(ctx, host1.ID, "", "", 0) + err = ds.SaveLUKSData(ctx, host1, "", "", 0) require.Error(t, err) require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID)) - err = ds.SaveLUKSData(ctx, host1.ID, "foo", "", 0) + err = ds.SaveLUKSData(ctx, host1, "foo", "", 0) require.Error(t, err) require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID)) // persists with passphrase and salt set - err = ds.SaveLUKSData(ctx, host2.ID, "bazqux", "fuzzmuffin", 0) + err = ds.SaveLUKSData(ctx, host2, "bazqux", "fuzzmuffin", 0) require.NoError(t, err) require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID)) require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID)) - key, err := ds.GetHostDiskEncryptionKey(ctx, host2.ID) - require.NoError(t, err) - require.Equal(t, "bazqux", key.Base64Encrypted) + checkLUKSEncryptionKey(t, ds, host2.ID, "bazqux", "fuzzmuffin") // persists when host hasn't had anything queued - err = ds.SaveLUKSData(ctx, host3.ID, "newstuff", "fuzzball", 1) + err = ds.SaveLUKSData(ctx, host3, "newstuff", "fuzzball", 1) require.NoError(t, err) require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID)) - key, err = ds.GetHostDiskEncryptionKey(ctx, host3.ID) + checkLUKSEncryptionKey(t, ds, host3.ID, "newstuff", "fuzzball") +} + +func checkLUKSEncryptionKey(t *testing.T, ds *Datastore, hostID uint, expectedKey string, expectedSalt string) { + got, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID) require.NoError(t, err) - require.Equal(t, "newstuff", key.Base64Encrypted) + require.Equal(t, expectedKey, got.Base64Encrypted) + if expectedKey != "" { + var archiveKey string + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(context.Background(), q, &archiveKey, + `SELECT base64_encrypted FROM host_disk_encryption_keys_archive WHERE host_id = ? ORDER BY created_at DESC LIMIT 1`, hostID) + }) + assert.Equal(t, expectedKey, archiveKey) + var archiveSalt string + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + return sqlx.GetContext(context.Background(), q, &archiveSalt, + `SELECT base64_encrypted_salt FROM host_disk_encryption_keys_archive WHERE host_id = ? ORDER BY created_at DESC LIMIT 1`, + hostID) + }) + assert.Equal(t, expectedSalt, archiveSalt) + } } func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { @@ -8171,10 +8196,10 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host, "AAA", "", nil) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2, "BBB", "", nil) require.NoError(t, err) h, err := ds.Host(context.Background(), host.ID) @@ -8185,7 +8210,7 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { require.NoError(t, err) checkEncryptionKeyStatus(t, ds, h.ID, "BBB", nil) - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "CCC", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2, "CCC", "", nil) require.NoError(t, err) h, err = ds.Host(context.Background(), host2.ID) @@ -8199,47 +8224,47 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true)) // same key doesn't change encryption status - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host, "AAA", "", nil) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true)) // different key resets encryption status - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "XZY", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host, "XZY", "", nil) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host.ID, "XZY", nil) // set the key with an initial decrypted status of true - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(true)) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3, "abc", "", ptr.Bool(true)) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true)) // same key, provided decrypted status is ignored (stored one is kept) - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(false)) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3, "abc", "", ptr.Bool(false)) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true)) // client error, key is removed and decrypted status is nulled - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "", "fail", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3, "", "fail", nil) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host3.ID, "", nil) // new key, provided decrypted status is applied - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "def", "", ptr.Bool(true)) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3, "def", "", ptr.Bool(true)) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host3.ID, "def", ptr.Bool(true)) // different key, provided decrypted status is applied - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "ghi", "", ptr.Bool(false)) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3, "ghi", "", ptr.Bool(false)) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host3.ID, "ghi", ptr.Bool(false)) // set an empty key (backfill for issue #15068) - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3, "", "", nil) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host3.ID, "", nil) // setting the decryptable value works even if the key is still empty - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "", "", ptr.Bool(false)) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3, "", "", ptr.Bool(false)) require.NoError(t, err) checkEncryptionKeyStatus(t, ds, host3.ID, "", ptr.Bool(false)) } @@ -8259,7 +8284,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) { PrimaryMac: "30-65-EC-6F-C4-58", }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "TESTKEY", "", nil) require.NoError(t, err) host2, err := ds.NewHost(context.Background(), &fleet.Host{ @@ -8276,7 +8301,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2, "TESTKEY", "", nil) require.NoError(t, err) threshold := time.Now().Add(time.Hour) @@ -8340,9 +8365,9 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "TESTKEY", "", nil) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2, "TESTKEY", "", nil) require.NoError(t, err) keys, err := ds.GetUnverifiedDiskEncryptionKeys(ctx) @@ -8365,7 +8390,7 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) { // update key of host 1 to empty with a client error, should not be reported // by GetUnverifiedDiskEncryptionKeys - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "", "failed", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "", "failed", nil) require.NoError(t, err) keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx) @@ -8794,7 +8819,7 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) { require.Equal(t, -1, *got.MDM.TestGetRawDecryptable()) // create the encryption key row, but unknown decryptable - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "abc", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "abc", "", nil) require.NoError(t, err) got, err = ds.Host(ctx, host.ID) diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index 3095b1c05790..6aa9250b5478 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -1240,8 +1240,8 @@ func testListHostsInLabelDiskEncryptionStatus(t *testing.T, ds *Datastore) { // verifying status upsertHostCPs([]*fleet.Host{hosts[0], hosts[1]}, []*fleet.MDMAppleConfigProfile{noTeamFVProfile}, fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, ctx, ds, t) oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute) - createDiskEncryptionRecord(ctx, ds, t, hosts[0].ID, "key-1", true, oneMinuteAfterThreshold) - createDiskEncryptionRecord(ctx, ds, t, hosts[1].ID, "key-1", true, oneMinuteAfterThreshold) + createDiskEncryptionRecord(ctx, ds, t, hosts[0], "key-1", true, oneMinuteAfterThreshold) + createDiskEncryptionRecord(ctx, ds, t, hosts[1], "key-1", true, oneMinuteAfterThreshold) listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerifying}, 2) listHostsCheckCount(t, ds, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 0) @@ -1595,7 +1595,7 @@ func testLabelsListHostsInLabelOSSettings(t *testing.T, db *Datastore) { require.NoError(t, db.SetOrUpdateMDMData(context.Background(), h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet, "")) } // add disk encryption key for h1 - require.NoError(t, db.SetOrUpdateHostDiskEncryptionKey(context.Background(), h1.ID, "test-key", "", ptr.Bool(true))) + require.NoError(t, db.SetOrUpdateHostDiskEncryptionKey(context.Background(), h1, "test-key", "", ptr.Bool(true))) // add disk encryption for h1 require.NoError(t, db.SetOrUpdateHostDisksEncryption(context.Background(), h1.ID, true)) diff --git a/server/datastore/mysql/linux_mdm_test.go b/server/datastore/mysql/linux_mdm_test.go index d0cecb405bea..8cc93f275e33 100644 --- a/server/datastore/mysql/linux_mdm_test.go +++ b/server/datastore/mysql/linux_mdm_test.go @@ -51,13 +51,13 @@ func TestLinuxDiskEncryptionSummary(t *testing.T) { // Add disk encryption keys // ubuntu - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0].ID, "base64_encrypted", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0], "base64_encrypted", "", nil) require.NoError(t, err) // fedora - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, fedoraHosts[0].ID, "base64_encrypted", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, fedoraHosts[0], "base64_encrypted", "", nil) require.NoError(t, err) // macos - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, macosHosts[0].ID, "base64_encrypted", "", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, macosHosts[0], "base64_encrypted", "", nil) require.NoError(t, err) summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) @@ -68,7 +68,7 @@ func TestLinuxDiskEncryptionSummary(t *testing.T) { require.Equal(t, uint(0), summary.Failed) // update ubuntu with key and client error - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0].ID, "base64_encrypted", "client error", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0], "base64_encrypted", "client error", nil) require.NoError(t, err) summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) @@ -79,7 +79,7 @@ func TestLinuxDiskEncryptionSummary(t *testing.T) { require.Equal(t, uint(1), summary.Failed) // add ubuntu with no key and client error - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[1].ID, "", "client error", nil) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[1], "", "client error", nil) require.NoError(t, err) summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil) diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index bdff02165402..74c46d1243e7 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -512,7 +512,7 @@ func (ds *Datastore) whereBitLockerStatus(status fleet.DiskEncryptionStatus) str whereEncrypted = `(hd.encrypted IS NOT NULL AND hd.encrypted = 1)` whereHostDisksUpdated = `(hd.updated_at IS NOT NULL AND hdek.updated_at IS NOT NULL AND hd.updated_at >= hdek.updated_at)` whereClientError = `(hdek.client_error IS NOT NULL AND hdek.client_error != '')` - withinGracePeriod = `(hdek.updated_at IS NOT NULL AND hdek.updated_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR))` + withinGracePeriod = `(hdek.updated_at IS NOT NULL AND hdek.updated_at >= DATE_SUB(NOW(6), INTERVAL 1 HOUR))` ) // TODO: what if windows sends us a key for an already encrypted volumne? could it get stuck diff --git a/server/datastore/mysql/microsoft_mdm_test.go b/server/datastore/mysql/microsoft_mdm_test.go index 9771fdff505f..6bad061f14cd 100644 --- a/server/datastore/mysql/microsoft_mdm_test.go +++ b/server/datastore/mysql/microsoft_mdm_test.go @@ -334,7 +334,7 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) { fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, }) - require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true))) + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))) checkExpected(t, nil, hostIDsByDEStatus{ // status is still pending because hosts_disks hasn't been updated yet fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, @@ -438,7 +438,7 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) { }) // ensure hosts[0] is set to verified for the rest of the tests - require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true))) + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))) require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) checkExpected(t, nil, hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, @@ -447,7 +447,7 @@ func testMDMWindowsDiskEncryption(t *testing.T, ds *Datastore) { t.Run("BitLocker failed status", func(t *testing.T) { // set hosts[1] to failed - require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1].ID, "", "test-error", ptr.Bool(false))) + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1], "", "test-error", ptr.Bool(false))) expected := hostIDsByDEStatus{ fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, @@ -767,7 +767,7 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) { checkExpected(t, nil, expected) require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) - require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true))) + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))) // simulate bitlocker verifying status by ensuring host_disks updated at timestamp is before host_disk_encryption_key updateHostDisks(t, hosts[0].ID, true, time.Now().Add(-10*time.Minute)) // status for hosts[0] now verifying because bitlocker status is verifying and host[0] has @@ -819,7 +819,7 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) { // all hosts are pending because no profiles and disk encryption is enabled checkExpected(t, nil, expected) - require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true))) + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "test-key", "", ptr.Bool(true))) // status is still pending because hosts_disks hasn't been updated yet checkExpected(t, nil, expected) @@ -870,7 +870,7 @@ func testMDMWindowsProfilesSummary(t *testing.T, ds *Datastore) { // all hosts are pending because no profiles and disk encryption is enabled checkExpected(t, nil, expected) - require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "", "some-bitlocker-error", nil)) + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0], "", "some-bitlocker-error", nil)) // status for hosts[0] now failed because any failed status takes precedence expected = hostIDsByProfileStatus{ fleet.MDMDeliveryFailed: []uint{hosts[0].ID}, diff --git a/server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive.go b/server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive.go new file mode 100644 index 000000000000..dc35ade9f0cb --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive.go @@ -0,0 +1,68 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20250121094045, Down_20250121094045) +} + +func Up_20250121094045(tx *sql.Tx) error { + _, err := tx.Exec(`ALTER TABLE host_disks + MODIFY COLUMN created_at TIMESTAMP(6) NOT NULL DEFAULT NOW(6), + MODIFY COLUMN updated_at TIMESTAMP(6) NULL DEFAULT NOW(6) ON UPDATE NOW(6)`) + if err != nil { + return fmt.Errorf("failed to alter host_disks table: %w", err) + } + + _, err = tx.Exec(`ALTER TABLE host_disk_encryption_keys + MODIFY COLUMN created_at TIMESTAMP(6) NOT NULL DEFAULT NOW(6), + MODIFY COLUMN updated_at TIMESTAMP(6) NULL DEFAULT NOW(6) ON UPDATE NOW(6)`) + if err != nil { + return fmt.Errorf("failed to alter host_disk_encryption_keys table: %w", err) + } + + stmt := ` +CREATE TABLE IF NOT EXISTS host_disk_encryption_keys_archive ( + -- Since we may never delete rows from this table, we use a large PRIMARY KEY + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + host_id int unsigned NOT NULL, + hardware_serial VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + base64_encrypted text COLLATE utf8mb4_unicode_ci NOT NULL, + base64_encrypted_salt varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + key_slot tinyint unsigned DEFAULT NULL, + created_at TIMESTAMP(6) NOT NULL DEFAULT NOW(6), + KEY idx_host_disk_encryption_keys_archive_host_created_at (host_id, created_at DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` + if _, err := tx.Exec(stmt); err != nil { + return fmt.Errorf("failed to create host_disk_encryption_keys_archive: %w", err) + } + + // Copy all existing rows from host_disk_encryption_keys to host_disk_encryption_keys_archive + const copyKeysToArchiveQuery = ` +INSERT INTO host_disk_encryption_keys_archive (host_id, base64_encrypted, base64_encrypted_salt, key_slot, created_at) +SELECT host_id, base64_encrypted, base64_encrypted_salt, key_slot, created_at +FROM host_disk_encryption_keys` + _, err = tx.Exec(copyKeysToArchiveQuery) + if err != nil { + return fmt.Errorf("failed to copy existing rows to host_disk_encryption_keys_archive: %w", err) + } + + // Update the hardware_serial column to match the host table + const updateHardwareSerialQuery = ` +UPDATE host_disk_encryption_keys_archive +JOIN hosts ON host_disk_encryption_keys_archive.host_id = hosts.id +SET host_disk_encryption_keys_archive.hardware_serial = hosts.hardware_serial` + _, err = tx.Exec(updateHardwareSerialQuery) + if err != nil { + return fmt.Errorf("failed to update host_disk_encryption_keys_archive.hardware_serial: %w", err) + } + + return nil +} + +func Down_20250121094045(_ *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive_test.go b/server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive_test.go new file mode 100644 index 000000000000..5a92bbdd1c85 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20250121094045_AddHostDiskEncryptionKeysArchive_test.go @@ -0,0 +1,60 @@ +package tables + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestUp_20250121094045(t *testing.T) { + db := applyUpToPrev(t) + + // Set up: 2 hosts and 3 keys + i := uint(1) + newHost := func(platform string) uint { + id := fmt.Sprintf("%d", i) + i++ + hostID := uint(execNoErrLastID(t, db, //nolint:gosec // dismiss G115 + `INSERT INTO hosts (hardware_serial, osquery_host_id, node_key, uuid, platform) VALUES (?, ?, ?, ?, ?)`, + id, id, id, id, platform, + )) + return hostID + } + ubuntuHostID := newHost("ubuntu") + macOSHostID := newHost("darwin") + + hostIDs := []uint{ubuntuHostID, macOSHostID, 9999} + for _, hostID := range hostIDs { + execNoErr(t, db, + `INSERT INTO host_disk_encryption_keys (host_id, base64_encrypted, base64_encrypted_salt, key_slot) VALUES (?, ?, ?, ?)`, + hostID, fmt.Sprintf("encrypted-%d", hostID), "salt", 1, + ) + } + timeBeforeMigration := time.Now() + + // Apply current migration. + applyNext(t, db) + + type archiveKey struct { + HostID uint `db:"host_id"` + HardwareSerial string `db:"hardware_serial"` + Base64Encrypted string `db:"base64_encrypted"` + CreatedAt time.Time `db:"created_at"` + } + var keys []archiveKey + require.NoError(t, + db.Select(&keys, + `SELECT host_id, hardware_serial, base64_encrypted, created_at FROM host_disk_encryption_keys_archive ORDER BY host_id ASC`)) + require.Len(t, keys, 3) + for i := range 3 { + require.Equal(t, hostIDs[i], keys[i].HostID) + require.Equal(t, fmt.Sprintf("encrypted-%d", hostIDs[i]), keys[i].Base64Encrypted) + require.GreaterOrEqual(t, keys[i].CreatedAt.Unix(), timeBeforeMigration.Unix()) + } + require.Equal(t, "1", keys[0].HardwareSerial) + require.Equal(t, "2", keys[1].HardwareSerial) + require.Equal(t, "", keys[2].HardwareSerial) + +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 26bec74e46f6..19bf20392ee4 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -306,8 +306,8 @@ CREATE TABLE `host_disk_encryption_keys` ( `base64_encrypted_salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `key_slot` tinyint unsigned DEFAULT NULL, `decryptable` tinyint(1) DEFAULT NULL, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `reset_requested` tinyint(1) NOT NULL DEFAULT '0', `client_error` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`host_id`), @@ -316,12 +316,26 @@ CREATE TABLE `host_disk_encryption_keys` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `host_disk_encryption_keys_archive` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `host_id` int unsigned NOT NULL, + `hardware_serial` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `base64_encrypted` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `base64_encrypted_salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `key_slot` tinyint unsigned DEFAULT NULL, + `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`), + KEY `idx_host_disk_encryption_keys_archive_host_created_at` (`host_id`,`created_at` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `host_disks` ( `host_id` int unsigned NOT NULL, `gigs_disk_space_available` decimal(10,2) NOT NULL DEFAULT '0.00', `percent_disk_space_available` decimal(10,2) NOT NULL DEFAULT '0.00', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` timestamp(6) NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `encrypted` tinyint(1) DEFAULT NULL, `gigs_total_disk_space` decimal(10,2) NOT NULL DEFAULT '0.00', PRIMARY KEY (`host_id`), @@ -1114,9 +1128,9 @@ CREATE TABLE `migration_status_tables` ( `is_applied` tinyint(1) NOT NULL, `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=349 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=350 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250107165731,1,'2020-01-01 01:01:01'),(347,20250109150150,1,'2020-01-01 01:01:01'),(348,20250110205257,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250107165731,1,'2020-01-01 01:01:01'),(347,20250109150150,1,'2020-01-01 01:01:01'),(348,20250110205257,1,'2020-01-01 01:01:01'),(349,20250121094045,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index bb04c7e696f4..bfc24faf455f 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -923,10 +923,10 @@ type Datastore interface { SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error // SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for // a host - SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error + SetOrUpdateHostDiskEncryptionKey(ctx context.Context, host *Host, encryptedBase64Key, clientError string, decryptable *bool) error // SaveLUKSData sets base64'd encrypted LUKS passphrase, key slot, and salt data for a host that has successfully // escrowed LUKS data - SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error + SaveLUKSData(ctx context.Context, host *Host, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error // GetUnverifiedDiskEncryptionKeys returns all the encryption keys that // are collected but their decryptable status is not known yet (ie: diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 611cb9204216..44e0c01f610d 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -653,9 +653,9 @@ type GetConfigEnableDiskEncryptionFunc func(ctx context.Context, teamID *uint) ( type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, encrypted bool) error -type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error +type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, host *fleet.Host, encryptedBase64Key string, clientError string, decryptable *bool) error -type SaveLUKSDataFunc func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error +type SaveLUKSDataFunc func(ctx context.Context, host *fleet.Host, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) @@ -5202,18 +5202,18 @@ func (s *DataStore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID u return s.SetOrUpdateHostDisksEncryptionFunc(ctx, hostID, encrypted) } -func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error { +func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, host *fleet.Host, encryptedBase64Key string, clientError string, decryptable *bool) error { s.mu.Lock() s.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = true s.mu.Unlock() - return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key, clientError, decryptable) + return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, host, encryptedBase64Key, clientError, decryptable) } -func (s *DataStore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error { +func (s *DataStore) SaveLUKSData(ctx context.Context, host *fleet.Host, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error { s.mu.Lock() s.SaveLUKSDataFuncInvoked = true s.mu.Unlock() - return s.SaveLUKSDataFunc(ctx, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot) + return s.SaveLUKSDataFunc(ctx, host, encryptedBase64Passphrase, encryptedBase64Salt, keySlot) } func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index f5291da435a0..29896b682c3b 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -1767,7 +1767,7 @@ func (s *integrationMDMTestSuite) TestEscrowBuddyBackwardsCompat() { assert.True(t, acResp.MDM.EnableDiskEncryption.Value) // set the status as non-decryptable so a notification should be sent - err := s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "", "", ptr.Bool(false)) + err := s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "", "", ptr.Bool(false)) require.NoError(t, err) // notification is false because the escrow buddy capability is not set @@ -1913,7 +1913,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleHostDiskEncryption() { require.NoError(t, err) base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey) - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64EncryptedKey, "", nil) + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, base64EncryptedKey, "", nil) require.NoError(t, err) // get that host - it has an encryption key with unknown decryptability, so @@ -2127,7 +2127,7 @@ func (s *integrationMDMTestSuite) TestWindowsMDMGetEncryptionKey() { encryptedKey, err := microsoft_mdm.Encrypt(recoveryKey, cert.Leaf) require.NoError(t, err) - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, encryptedKey, "", ptr.Bool(true)) + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, encryptedKey, "", ptr.Bool(true)) require.NoError(t, err) resp = getHostEncryptionKeyResponse{} @@ -2138,7 +2138,7 @@ func (s *integrationMDMTestSuite) TestWindowsMDMGetEncryptionKey() { fmt.Sprintf(`{"host_display_name": "%s", "host_id": %d}`, host.DisplayName(), host.ID), 0) // update the key to blank with a client error - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "", "failed", nil) + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "", "failed", nil) require.NoError(t, err) resp = getHostEncryptionKeyResponse{} @@ -2314,7 +2314,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleDiskEncryptionAggregate() { }) require.NoError(t, err) oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute) - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "test-key", "", nil) + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "test-key", "", nil) require.NoError(t, err) err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, decryptable, oneMinuteAfterThreshold) require.NoError(t, err) @@ -7561,7 +7561,7 @@ func (s *integrationMDMTestSuite) TestBitLockerEnforcementNotifications() { checkNotification(true) // host has disk encryption on, we don't know if the key is decriptable. Gets the notification - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost.ID, "test-key", "", nil) + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost, "test-key", "", nil) require.NoError(t, err) checkNotification(true) @@ -7609,7 +7609,7 @@ func (s *integrationMDMTestSuite) TestBitLockerEnforcementNotifications() { checkNotification(true) // host has disk encryption on, we don't know if the key is decriptable. Gets the notification - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost.ID, "test-key", "", nil) + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost, "test-key", "", nil) require.NoError(t, err) checkNotification(true) diff --git a/server/service/orbit.go b/server/service/orbit.go index f3bbaabb16c3..923320b27920 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -1047,7 +1047,7 @@ func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryption decryptable = ptr.Bool(true) } - if err := svc.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, encryptedEncryptionKey, clientError, decryptable); err != nil { + if err := svc.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, encryptedEncryptionKey, clientError, decryptable); err != nil { return ctxerr.Wrap(ctx, err, "set or update disk encryption key") } @@ -1110,7 +1110,7 @@ func (svc *Service) EscrowLUKSData(ctx context.Context, passphrase string, salt return err } - return svc.ds.SaveLUKSData(ctx, host.ID, encryptedPassphrase, encryptedSalt, validatedKeySlot) + return svc.ds.SaveLUKSData(ctx, host, encryptedPassphrase, encryptedSalt, validatedKeySlot) } func (svc *Service) validateAndEncrypt(ctx context.Context, passphrase string, salt string, keySlot *uint) (encryptedPassphrase string, encryptedSalt string, validatedKeySlot uint, err error) { diff --git a/server/service/orbit_test.go b/server/service/orbit_test.go index 135a70b06c4f..6ebe2cc6ebf3 100644 --- a/server/service/orbit_test.go +++ b/server/service/orbit_test.go @@ -194,8 +194,9 @@ func TestOrbitLUKSDataSave(t *testing.T) { ds.ReportEscrowErrorFuncInvoked = false passphrase, salt := "foo", "" var keySlot *uint - ds.SaveLUKSDataFunc = func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlotToPersist uint) error { - require.Equal(t, host.ID, hostID) + ds.SaveLUKSDataFunc = func(ctx context.Context, incomingHost *fleet.Host, encryptedBase64Passphrase string, + encryptedBase64Salt string, keySlotToPersist uint) error { + require.Equal(t, host.ID, incomingHost.ID) key := config.TestConfig().Server.PrivateKey decryptedPassphrase, err := mdm.DecodeAndDecrypt(encryptedBase64Passphrase, key) diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 35a5fb748bd1..7ec132f8add6 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -2068,7 +2068,7 @@ func directIngestDiskEncryptionKeyFileDarwin( if base64Key == "" { decryptable = ptr.Bool(false) } - return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64Key, "", decryptable) + return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, base64Key, "", decryptable) } // directIngestDiskEncryptionKeyFileLinesDarwin ingests the FileVault key from the `file_lines` @@ -2127,7 +2127,7 @@ func directIngestDiskEncryptionKeyFileLinesDarwin( decryptable = ptr.Bool(false) } - return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64Key, "", decryptable) + return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, base64Key, "", decryptable) } func directIngestMacOSProfiles( diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index 57ba06d58c65..a71b1deb806d 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -1529,11 +1529,12 @@ func TestDirectIngestDiskEncryptionKeyDarwin(t *testing.T) { } } - ds.SetOrUpdateHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error { + ds.SetOrUpdateHostDiskEncryptionKeyFunc = func(ctx context.Context, incomingHost *fleet.Host, encryptedBase64Key, clientError string, + decryptable *bool) error { if base64.StdEncoding.EncodeToString([]byte(wantKey)) != encryptedBase64Key { return errors.New("key mismatch") } - if host.ID != hostID { + if host.ID != incomingHost.ID { return errors.New("host ID mismatch") } if encryptedBase64Key == "" && (decryptable == nil || *decryptable == true) { From e90574b808cb570bfe6763bd7653c62e3db3b1d9 Mon Sep 17 00:00:00 2001 From: Allen Houchins <32207388+allenhouchins@users.noreply.github.com> Date: Wed, 22 Jan 2025 15:00:07 -0600 Subject: [PATCH 02/22] Enable activities webhook via GitOps (#25690) Dogfooding this feature: fleetdm/confidential#9337 --- .github/workflows/dogfood-gitops.yml | 1 + it-and-security/default.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/dogfood-gitops.yml b/.github/workflows/dogfood-gitops.yml index be7b92df68ab..c8413d6fbfd5 100644 --- a/.github/workflows/dogfood-gitops.yml +++ b/.github/workflows/dogfood-gitops.yml @@ -77,6 +77,7 @@ jobs: DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET: ${{ secrets.DOGFOOD_COMPANY_OWNED_IPADS_ENROLL_SECRET }} FLEET_SECRET_MANAGED_CHROME_ENROLLMENT_TOKEN: ${{ secrets.CLOUD_MANAGEMENT_ENROLLMENT_TOKEN }} DOGFOOD_PERSONALLY_OWNED_IPHONES_ENROLL_SECRET: ${{ secrets.DOGFOOD_PERSONALLY_OWNED_IPHONES_ENROLL_SECRET }} + DOGFOOD_ACTIVITIES_WEBHOOK_URL: ${{ secrets.DOGFOOD_ACTIVITIES_WEBHOOK_URL }} - name: Notify on Gitops failure if: failure() && github.ref_name == 'main' diff --git a/it-and-security/default.yml b/it-and-security/default.yml index a492f2926f43..418266f8bf86 100644 --- a/it-and-security/default.yml +++ b/it-and-security/default.yml @@ -77,6 +77,9 @@ org_settings: destination_url: "" enable_vulnerabilities_webhook: false host_batch_size: 0 + activities_webhook: + destination_url: $DOGFOOD_ACTIVITIES_WEBHOOK_URL + enable_activities_webhook: true policies: queries: - path: ./lib/all/queries/collect-fleetd-update-channels.yml From ede9086e0a6f1c8b73ed8842b1c5280824da6ce3 Mon Sep 17 00:00:00 2001 From: Reed Haynes Date: Wed, 22 Jan 2025 15:00:26 -0600 Subject: [PATCH 03/22] Update migration_test.sh (#25693) add prompt when downgrading hosts to 1.37 --- tools/tuf/test/migration/migration_test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/tuf/test/migration/migration_test.sh b/tools/tuf/test/migration/migration_test.sh index 890e300e7ffe..a01189decfd0 100755 --- a/tools/tuf/test/migration/migration_test.sh +++ b/tools/tuf/test/migration/migration_test.sh @@ -417,6 +417,7 @@ for host_hostname in "${all_hostnames[@]}"; do done done +prompt "Check that your orbit is on 1.38.2 on all your devices" echo "Downgrading to $OLD_FULL_VERSION..." cat << EOF > downgrade.yml --- From 5c6fe54a10507697edad92b39f487a99ddb08970 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 22 Jan 2025 18:01:05 -0300 Subject: [PATCH 04/22] Update migration_test.sh (#25696) --- tools/tuf/test/migration/migration_test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/tuf/test/migration/migration_test.sh b/tools/tuf/test/migration/migration_test.sh index a01189decfd0..aaba14f9b2ec 100755 --- a/tools/tuf/test/migration/migration_test.sh +++ b/tools/tuf/test/migration/migration_test.sh @@ -506,6 +506,7 @@ for pkgType in "${pkgTypes[@]}"; do done echo "Installing fleetd package on macOS..." +sudo ./orbit/tools/cleanup/cleanup_macos.sh sudo installer -pkg fleet-osquery.pkg -verbose -target / CURRENT_DIR=$(pwd) @@ -614,6 +615,7 @@ for pkgType in "${pkgTypes[@]}"; do done echo "Installing fleetd package on macOS..." +sudo ./orbit/tools/cleanup/cleanup_macos.sh sudo installer -pkg fleet-osquery.pkg -verbose -target / CURRENT_DIR=$(pwd) From fecc8bbc07576613c1d4829e87cea80655b6d70c Mon Sep 17 00:00:00 2001 From: Rebecca Cowart Date: Wed, 22 Jan 2025 16:36:13 -0500 Subject: [PATCH 05/22] Update rest-api.md (#25670) Fixed typo - misspelling of "endpoint" --- docs/REST API/rest-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 5fd111065a11..f9a8810b89ff 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -9702,7 +9702,7 @@ _Available in Fleet Premium._ Get the results of a software package install. -To get the results of an App Store app install, use the [List MDM commands](#list-mdm-commands) and [Get MDM command results](#get-mdm-command-results) API enpoints. Fleet uses an MDM command to install App Store apps. +To get the results of an App Store app install, use the [List MDM commands](#list-mdm-commands) and [Get MDM command results](#get-mdm-command-results) API endpoints. Fleet uses an MDM command to install App Store apps. | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | From c576dd5f602b51a2b6210e4defc4e25278595d52 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Wed, 22 Jan 2025 16:22:56 -0600 Subject: [PATCH 06/22] Update communications handbook page (#25705) Co-authored-by: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> --- handbook/company/communications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handbook/company/communications.md b/handbook/company/communications.md index c8527cfe48a9..bec991d2252e 100644 --- a/handbook/company/communications.md +++ b/handbook/company/communications.md @@ -90,7 +90,7 @@ We track competitors' capabilities and adjacent (or commonly integrated) product | Role | Google Workspace | Slack | GitHub | Gusto | Pilot | Plane | 1Password | |:-----|-----------------:|------:|-------:|------:|------:|------:|----------:| | CEO | ✅ Super admin | ✅ Primary workspace owner | ✅ Owner | ✅ Primary admin | ✅ Owner |✅ Owner | ✅ Owner | -| CTO | ❌ | ❌ | ✅ Owner | ❌ | ❌ | ✅ Admin | ❌ | +| CTO | ✅ Super admin | ❌ | ✅ Owner | ❌ | ❌ | ✅ Admin | ❌ | | Finance Engineer | ❌ | ❌ | ❌ | ✅ Admin | ✅ Admin |✅ Admin | ❌ | | Head of Digital Experience | ✅ Super admin | ✅ Owner | ✅ Owner| ✅ Admin | ❌ | ✅ Admin | ✅ Admin | | Apprentice | ✅ Super admin| ✅ Owner | ✅ Owner | ✅ Admin | ❌ | ✅ Admin | ✅ Admin | From 126426b2133e7d6705fe01269bd2c67b4dc0d531 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 22 Jan 2025 15:01:52 -0800 Subject: [PATCH 07/22] UI - Update metadata error states on Sso settings form (#25614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## For #25318 Screenshot 2025-01-20 at 12 29 32 PM - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- changes/25318-update-sso-settings-error-states | 1 + frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changes/25318-update-sso-settings-error-states diff --git a/changes/25318-update-sso-settings-error-states b/changes/25318-update-sso-settings-error-states new file mode 100644 index 000000000000..d1c840ca5a68 --- /dev/null +++ b/changes/25318-update-sso-settings-error-states @@ -0,0 +1 @@ +* Add clearer error states to metadata-related fields in the SSO settings form diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx index 6afd6269fbbd..74e4a61426f2 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx @@ -51,8 +51,10 @@ const validate = (formData: ISsoFormData) => { if (!metadata) { if (!metadataUrl) { - errors.metadata_url = "Metadata or Metadata URL must be present"; - errors.metadata = "Metadata or Metadata URL must be present"; + errors.metadata_url = + "Metadata URL is required (if metadata is not present)"; + errors.metadata = + "Metadata is required (if metadata URL is not present)"; } else if ( !validUrl({ url: metadataUrl, protocols: ["http", "https"] }) ) { From d6e026982a462754c99ccb3550c6b7fd49ae1d48 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 22 Jan 2025 20:04:13 -0300 Subject: [PATCH 08/22] Update migration_test.sh (#25707) --- tools/tuf/test/migration/migration_test.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/tuf/test/migration/migration_test.sh b/tools/tuf/test/migration/migration_test.sh index aaba14f9b2ec..b4868eddc65d 100755 --- a/tools/tuf/test/migration/migration_test.sh +++ b/tools/tuf/test/migration/migration_test.sh @@ -505,8 +505,11 @@ for pkgType in "${pkgTypes[@]}"; do --update-interval=30s done -echo "Installing fleetd package on macOS..." +echo "Uninstalling fleetd package from macOS..." sudo ./orbit/tools/cleanup/cleanup_macos.sh +echo "Sleeping 60 seconds..." +sleep 60 +echo "Installing fleetd package on macOS..." sudo installer -pkg fleet-osquery.pkg -verbose -target / CURRENT_DIR=$(pwd) @@ -614,8 +617,11 @@ for pkgType in "${pkgTypes[@]}"; do --update-interval=30s done -echo "Installing fleetd package on macOS..." +echo "Uninstalling fleetd package from macOS..." sudo ./orbit/tools/cleanup/cleanup_macos.sh +echo "Sleeping 60 seconds..." +sleep 60 +echo "Installing fleetd package on macOS..." sudo installer -pkg fleet-osquery.pkg -verbose -target / CURRENT_DIR=$(pwd) From a23c305e04b5ebad4f01a20c44a45ccd2b550fde Mon Sep 17 00:00:00 2001 From: Harrison Ravazzolo <38767391+harrisonravazzolo@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:00:28 -0800 Subject: [PATCH 09/22] Typo in gitops yaml file (#25681) Update `name` to proper syntax `org_name` --- docs/Configuration/yaml-files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md index f5be30ff1eb9..d49ebd04306b 100644 --- a/docs/Configuration/yaml-files.md +++ b/docs/Configuration/yaml-files.md @@ -424,7 +424,7 @@ org_settings: ### org_info -- `name` is the name of your organization (default: `""`) +- `org_name` is the name of your organization (default: `""`) - `logo_url` is a public URL of the logo for your organization (default: Fleet logo). - `org_logo_url_light_background` is a public URL of the logo for your organization that can be used with light backgrounds (default: Fleet logo). - `contact_url` is a URL that appears in error messages presented to end users (default: `"https://fleetdm.com/company/contact"`) From a1a43415d444110a130f28e8fef262d0996fac16 Mon Sep 17 00:00:00 2001 From: Konstantin Sykulev Date: Wed, 22 Jan 2025 18:00:54 -0600 Subject: [PATCH 10/22] Removed filename from fleet maintained apps response (#25685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This field was never implemented Screenshot 2025-01-21 at 8 33 56 PM --- docs/REST API/rest-api.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index f9a8810b89ff..6a01930ba60c 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -9553,7 +9553,6 @@ Returns information about the specified Fleet-maintained app. "fleet_maintained_app": { "id": 1, "name": "1Password", - "filename": "1Password-8.10.44-aarch64.zip", "version": "8.10.40", "platform": "darwin", "install_script": "#!/bin/sh\ninstaller -pkg \"$INSTALLER_PATH\" -target /", From 19947a0c089bc1fdf6bdffe155b863e793411b91 Mon Sep 17 00:00:00 2001 From: Harrison Ravazzolo <38767391+harrisonravazzolo@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:02:42 -0800 Subject: [PATCH 11/22] Add support for post_install_script (#25683) This is supported in gitops but not referenced in docu. --- docs/Configuration/yaml-files.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md index d49ebd04306b..65cbd36895ec 100644 --- a/docs/Configuration/yaml-files.md +++ b/docs/Configuration/yaml-files.md @@ -349,6 +349,7 @@ Use `labels_include_any` to target hosts that have any label in the array or `la - `pre_install_query.path` is the osquery query Fleet runs before installing the software. Software will be installed only if the [query returns results](https://fleetdm.com/tables) (default: `""`). - `install_script.path` specifies the command Fleet will run on hosts to install software. The [default script](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) is dependent on the software type (i.e. .pkg). - `uninstall_script.path` is the script Fleet will run on hosts to uninstall software. The [default script](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) is dependent on the software type (i.e. .pkg). +- `post_install_script.path` is the script Fleet will run on hosts after the software install. There is no default. - `self_service` specifies whether or not end users can install from **Fleet Desktop > Self-service**. #### Example @@ -361,6 +362,8 @@ install_script: path: ../lib/software/tailscale-install-script.ps1 uninstall_script: path: ../lib/software/tailscale-uninstall-script.ps1 +post_install_script: + path: ../lib/software/tailscale-config-script.ps1 self_service: true ``` From 5c0894ce0ae85f2ea2c6004f050882c4923afee8 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 22 Jan 2025 21:10:17 -0600 Subject: [PATCH 12/22] Website: Create queries.yml, add vitals and query pages, move policies. (#25701) Related to: https://github.com/fleetdm/confidential/issues/9096 Changes: - Created docs/queries.yml. A YAML file that contains the queries from the standard query library (`kind: query`) and the host vitals queries (`kind: built-in`). - Added the `vitals/*` page, a page that displays details about host vital queries used to gather information about. - Updated the /queries page to show queries from the new `docs/queries.yml` file, and moved policies to a new page (/policies) - Updated the view action for the query-detail page to look for/redirect to a policy page with a matching slug before returning a 404 response if a matching query is not found. This behavior will make it so all of the old URLs for policy pages will redirect users to the new URL. - Updated the website's "Docs" navigation menu to have links to the new vitals and policies pages. --- docs/queries.yml | 1594 +++++++++++++++++ .../api/controllers/view-policy-details.js | 77 + .../api/controllers/view-policy-library.js | 55 + website/api/controllers/view-query-detail.js | 18 +- website/api/controllers/view-query-library.js | 14 +- website/api/controllers/view-vital-details.js | 97 + .../images/icon-select-arrows-16x16@2x.png | Bin 0 -> 283 bytes .../assets/js/pages/policy-details.page.js | 99 + .../assets/js/pages/policy-library.page.js | 43 + website/assets/js/pages/query-detail.page.js | 4 +- website/assets/js/pages/query-library.page.js | 2 +- website/assets/js/pages/vital-details.page.js | 116 ++ website/assets/styles/importer.less | 3 + .../assets/styles/pages/policy-details.less | 410 +++++ .../assets/styles/pages/policy-library.less | 339 ++++ website/assets/styles/pages/query-detail.less | 62 +- .../assets/styles/pages/query-library.less | 20 +- .../assets/styles/pages/vital-details.less | 570 ++++++ website/config/policies.js | 3 + website/config/routes.js | 28 +- website/scripts/build-static-content.js | 154 +- website/views/layouts/layout.ejs | 23 +- website/views/pages/policy-details.ejs | 97 + website/views/pages/policy-library.ejs | 127 ++ website/views/pages/query-detail.ejs | 45 +- website/views/pages/query-library.ejs | 56 +- website/views/pages/vital-details.ejs | 145 ++ 27 files changed, 4076 insertions(+), 125 deletions(-) create mode 100644 docs/queries.yml create mode 100644 website/api/controllers/view-policy-details.js create mode 100644 website/api/controllers/view-policy-library.js create mode 100644 website/api/controllers/view-vital-details.js create mode 100644 website/assets/images/icon-select-arrows-16x16@2x.png create mode 100644 website/assets/js/pages/policy-details.page.js create mode 100644 website/assets/js/pages/policy-library.page.js create mode 100644 website/assets/js/pages/vital-details.page.js create mode 100644 website/assets/styles/pages/policy-details.less create mode 100644 website/assets/styles/pages/policy-library.less create mode 100644 website/assets/styles/pages/vital-details.less create mode 100644 website/views/pages/policy-details.ejs create mode 100644 website/views/pages/policy-library.ejs create mode 100644 website/views/pages/vital-details.ejs diff --git a/docs/queries.yml b/docs/queries.yml new file mode 100644 index 000000000000..17aa018795c8 --- /dev/null +++ b/docs/queries.yml @@ -0,0 +1,1594 @@ +# Host vital queries (from `server/service/osquery_utils/queries.go`) +apiVersion: v1 +kind: built-in +spec: + name: Battery + platform: windows, darwin + description: Determines battery health based on the cycle count, designed capacity, and max capacity of the battery. + query: | + SELECT + serial_number, cycle_count, designed_capacity, max_capacity + FROM battery + purpose: Informational + tags: built-in + discovery: battery +--- +apiVersion: v1 +kind: built-in +spec: + name: ChromeOS profile user info + platform: chrome + description: Retrieves information about profiles on ChromeOS devices + query: SELECT email FROM users + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Disk encryption (macOS) + platform: darwin + description: Retrieves the disk encryption status of a macOS device. + query: | + SELECT + 1 + FROM disk_encryption + WHERE user_uuid IS NOT "" + AND filevault_status = 'on' LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Disk encryption (Linux) + platform: linux + description: Retrieves the disk encryption status of a device running Linux. + query: | + SELECT + de.encrypted, m.path + FROM disk_encryption de + JOIN mounts m ON m.device_alias = de.name + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Disk encryption (Windows) + platform: windows + description: Retrieves the disk encryption status of a Windows device. + query: | + WITH encrypted(enabled) AS ( + SELECT CASE WHEN + NOT EXISTS(SELECT 1 FROM windows_optional_features WHERE name = 'BitLocker') + OR + (SELECT 1 FROM windows_optional_features WHERE name = 'BitLocker' AND state = 1) + THEN (SELECT 1 FROM bitlocker_info WHERE drive_letter = 'C:' AND protection_status = 1) + END) + SELECT 1 FROM encrypted WHERE enabled IS NOT NULL + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Disk space + platform: linux, darwin + description: Retrieves total amount of free disk space on a host. + query: | + SELECT + (blocks_available * 100 / blocks) AS percent_disk_space_available, + round((blocks_available * blocks_size * 10e-10),2) AS gigs_disk_space_available, + round((blocks * blocks_size * 10e-10),2) AS gigs_total_disk_space + FROM mounts + WHERE path = '/' LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Disk space (Windows) + platform: windows + description: Retrieves total amount of free disk space on a Windows host. + query: | + SELECT + ROUND((sum(free_space) * 100 * 10e-10) / (sum(size) * 10e-10)) AS percent_disk_space_available, + ROUND(sum(free_space) * 10e-10) AS gigs_disk_space_available, + ROUND(sum(size) * 10e-10) AS gigs_total_disk_space + FROM logical_drives + WHERE file_system = 'NTFS' LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Google Chrome profiles + platform: windows, darwin, linux, chrome + description: Retrieves the email address of Google Chrome profile on a host. + query: | + SELECT + email + FROM google_chrome_profiles + WHERE NOT ephemeral AND email <> '' + discovery: google_chrome_profiles + purpose: Informational + tags: built-in +# --- # Note: this vital is commented out because it requires the kubequery osquery extension. +# apiVersion: v1 +# kind: built-in +# spec: +# name: Kubequery info +# platform: windows, darwin, linux, chrome +# description: Retrieves information about Kubernetes clusters running kubequery. +# query: SELECT * FROM kubernetes_info +# # discovery: kubernetes_info # Note: this value is commented out because this table is from kubequery and does not exist in the osquery schema. +# purpose: Informational +# tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: MDM (macOS) + platform: darwin + description: Retrieves information about the mobile device management (MDM) solution this host is enrolled in. + query: | + SELECT + enrolled, server_url, installed_from_dep, payload_identifier + FROM mdm + discovery: mdm + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: MDM configuration profiles + platform: darwin + description: Retrieves information about mobile device management (MDM) configuration profiles installed on a macOS device. + query: | + SELECT + display_name, identifier, install_date + FROM macos_profiles + WHERE type = "Configuration" + discovery: macos_profiles + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: MDM Disk encryption key file + platform: darwin + description: Retrieves the encrypted FileVault recovery key for managed macOS devices. + query: | + WITH + de AS (SELECT IFNULL((SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT "" AND filevault_status = 'on' LIMIT 1), 0) as encrypted), + fv AS (SELECT base64_encrypted as filevault_key FROM filevault_prk) + SELECT encrypted, filevault_key FROM de LEFT JOIN fv + discovery: filevault_prk + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: MDM Disk encryption key file lines + platform: darwin + description: Retrieves the encrypted FileVault recovery key and checks for related file data on managed macOS devices. + query: | + WITH + de AS (SELECT IFNULL((SELECT 1 FROM disk_encryption WHERE user_uuid IS NOT "" AND filevault_status = 'on' LIMIT 1), 0) as encrypted), + fl AS (SELECT line FROM file_lines WHERE path = '/var/db/FileVaultPRK.dat') + SELECT encrypted, hex(line) as hex_line FROM de LEFT JOIN fl; + discovery: filevault_prk # TODO: this query's discovery query also checks for file_lines. + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: MDM (Windows) + platform: windows + description: Retrieves information about the mobile device management (MDM) solution a windows device is enrolled in. + query: | + WITH registry_keys AS ( + SELECT * + FROM registry + WHERE path LIKE 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Enrollments\%%' + ), + enrollment_info AS ( + SELECT + MAX(CASE WHEN name = 'UPN' THEN data END) AS upn, + MAX(CASE WHEN name = 'DiscoveryServiceFullURL' THEN data END) AS discovery_service_url, + MAX(CASE WHEN name = 'ProviderID' THEN data END) AS provider_id, + MAX(CASE WHEN name = 'EnrollmentState' THEN data END) AS state, + MAX(CASE WHEN name = 'AADResourceID' THEN data END) AS aad_resource_id + FROM registry_keys + GROUP BY key + ), + installation_info AS ( + SELECT data AS installation_type + FROM registry + WHERE path = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\InstallationType' + LIMIT 1 + ) + SELECT + e.aad_resource_id, + e.discovery_service_url, + e.provider_id, + i.installation_type + FROM installation_info i + LEFT JOIN enrollment_info e ON e.upn IS NOT NULL + WHERE COALESCE(e.state, '0') IN ('0', '1', '2', '3') + LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Munki info + platform: darwin + description: Retrives information about the last Munki run on a macOS device. + query: | + SELECT + version, errors, warnings + FROM munki_info + discovery: munki_info + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Network interfaces (Chrome) + platform: chrome + description: Retrives information about a ChromeOS device's current network. + query: | + SELECT + ipv4 AS address, mac + FROM network_interfaces LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Network interfaces (macOS/Linux) + platform: darwin, linux + description: Retrieves information about network interfaces on macOS and Linux devices + query: | + SELECT + ia.address, + id.mac + FROM + interface_addresses ia + JOIN interface_details id ON id.interface = ia.interface + JOIN routes r ON r.interface = ia.interface + WHERE + (r.destination = '0.0.0.0' OR r.destination = '::') AND r.netmask = 0 + AND r.type = 'gateway' + AND ( + inet_aton(ia.address) IS NOT NULL AND ( + split(ia.address, '.', 0) = '10' + OR (split(ia.address, '.', 0) = '172' AND (CAST(split(ia.address, '.', 1) AS INTEGER) & 0xf0) = 16) + OR (split(ia.address, '.', 0) = '192' AND split(ia.address, '.', 1) = '168') + ) + OR (inet_aton(ia.address) IS NULL AND regex_match(lower(ia.address), '^f[cd][0-9a-f][0-9a-f]:[0-9a-f:]+', 0) IS NOT NULL) + ) + ORDER BY + r.metric ASC, + inet_aton(ia.address) IS NOT NULL DESC + LIMIT 1; + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Network interfaces (Windows) + platform: windows + description: Retrieves information about network interfaces on devices running windows. + query: | + SELECT + ia.address, + id.mac + FROM + interface_addresses ia + JOIN interface_details id ON id.interface = ia.interface + JOIN routes r ON r.interface = ia.address + WHERE + (r.destination = '0.0.0.0' OR r.destination = '::') AND r.netmask = 0 + AND r.type = 'remote' + AND ( + inet_aton(ia.address) IS NOT NULL AND ( + split(ia.address, '.', 0) = '10' + OR (split(ia.address, '.', 0) = '172' AND (CAST(split(ia.address, '.', 1) AS INTEGER) & 0xf0) = 16) + OR (split(ia.address, '.', 0) = '192' AND split(ia.address, '.', 1) = '168') + ) + OR (inet_aton(ia.address) IS NULL AND regex_match(lower(ia.address), '^f[cd][0-9a-f][0-9a-f]:[0-9a-f:]+', 0) IS NOT NULL) + ) + ORDER BY + r.metric ASC, + inet_aton(ia.address) IS NOT NULL DESC + LIMIT 1; + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Orbit information + platform: darwin, linux, windows + description: Retreives configuration information the osquery version and configuration manager running on a device. + query: SELECT * FROM orbit_info + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Operating system information (Chrome) + platform: chrome + description: Retrieves information about a ChromeOS device's operating system. + query: | + SELECT + os.name, + os.major, + os.minor, + os.patch, + os.build, + os.arch, + os.platform, + os.version AS version, + os.version AS kernel_version + FROM + os_version os; + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Operating system information (macOS/Linux) + platform: darwin, linux + description: Retrieves information about a Unix-based device's operating system. + query: | + SELECT + os.name, + os.major, + os.minor, + os.patch, + os.extra, + os.build, + os.arch, + os.platform, + os.version AS version, + k.version AS kernel_version + FROM + os_version os, + kernel_info k + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Operating system information (Windows) + platform: windows + description: Retrieves information about a Windows device's operating system. + query: | + WITH display_version_table AS ( + SELECT data as display_version + FROM registry + WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion' + ), + ubr_table AS ( + SELECT data AS ubr + FROM registry + WHERE path ='HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\UBR' + ) + SELECT + os.name, + os.platform, + os.arch, + k.version as kernel_version, + COALESCE(CONCAT((SELECT version FROM os_version), '.', u.ubr), k.version) AS version, + COALESCE(d.display_version, '') AS display_version + FROM + os_version os, + kernel_info k + LEFT JOIN + display_version_table d + LEFT JOIN + ubr_table u + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Operating system version (Windows) + platform: windows + description: Retrieves operating system version information from a Windows device. + query: | + WITH display_version_table AS ( + SELECT data as display_version + FROM registry + WHERE path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion' + ), + ubr_table AS ( + SELECT data AS ubr + FROM registry + WHERE path ='HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\UBR' + ) + SELECT + os.name, + COALESCE(d.display_version, '') AS display_version, + COALESCE(CONCAT((SELECT version FROM os_version), '.', u.ubr), k.version) AS version + FROM + os_version os, + kernel_info k + LEFT JOIN + display_version_table d + LEFT JOIN + ubr_table u + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Osquery flags + platform: darwin, linux, windows + description: Retrieves the values of osquery configuration flags related to query scheduling, configuration updates, and logging intervals for active processes. + query: | + SELECT + name, value + FROM osquery_flags + WHERE name IN ("distributed_interval", "config_tls_refresh", "config_refresh", "logger_tls_period") + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Osquery information + platform: darwin, windows, linux + description: Gathers information about the osquery process running on a device. + query: SELECT * FROM osquery_info LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Scheduled osquery statistics + platform: darwin, windows, linux + description: Retrieves statistics about queries that are scheduled on a device. + query: | + SELECT *, + (SELECT value from osquery_flags where name = 'pack_delimiter') AS delimiter + FROM osquery_schedule + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Software (Chrome) + platform: chrome + description: Gathers information about software installed on a ChromeOS device. + query: | + SELECT + name AS name, + version AS version, + identifier AS extension_id, + browser_type AS browser, + 'chrome_extensions' AS source, + '' AS vendor, + '' AS installed_path + FROM chrome_extensions + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Software (macOS) + platform: darwin + description: Gathers information about software installed on a device running linux. + query: | + WITH cached_users AS (WITH cached_groups AS (select * from groups) + SELECT uid, username, type, groupname, shell + FROM users LEFT JOIN cached_groups USING (gid) + WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '')) + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'deb_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + '' AS installed_path + FROM deb_packages + WHERE status LIKE '% ok installed' + UNION + SELECT + package AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'portage_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + '' AS installed_path + FROM portage_packages + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'rpm_packages' AS source, + release AS release, + vendor AS vendor, + arch AS arch, + '' AS installed_path + FROM rpm_packages + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'npm_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM npm_packages + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + browser_type AS browser, + 'chrome_extensions' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM cached_users CROSS JOIN chrome_extensions USING (uid) + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + 'firefox' AS browser, + 'firefox_addons' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM cached_users CROSS JOIN firefox_addons USING (uid) + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'python_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM python_packages + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Software (Linux) + platform: linux + description: Gathers information about software installed on a device running linux. + query: | + WITH cached_users AS (WITH cached_groups AS (select * from groups) + SELECT uid, username, type, groupname, shell + FROM users LEFT JOIN cached_groups USING (gid) + WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '')) + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'deb_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + '' AS installed_path + FROM deb_packages + WHERE status LIKE '% ok installed' + UNION + SELECT + package AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'portage_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + '' AS installed_path + FROM portage_packages + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'rpm_packages' AS source, + release AS release, + vendor AS vendor, + arch AS arch, + '' AS installed_path + FROM rpm_packages + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'npm_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM npm_packages + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + browser_type AS browser, + 'chrome_extensions' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM cached_users CROSS JOIN chrome_extensions USING (uid) + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + 'firefox' AS browser, + 'firefox_addons' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM cached_users CROSS JOIN firefox_addons USING (uid) + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'python_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM python_packages + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Software codesign + platform: darwin + description: A software override query to append codesign information to macOS software entries. Requires fleetd + query: | + WITH cached_users AS (WITH cached_groups AS (select * from groups) + SELECT uid, username, type, groupname, shell + FROM users LEFT JOIN cached_groups USING (gid) + WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '')) + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'deb_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + '' AS installed_path + FROM deb_packages + WHERE status LIKE '% ok installed' + UNION + SELECT + package AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'portage_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + '' AS installed_path + FROM portage_packages + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'rpm_packages' AS source, + release AS release, + vendor AS vendor, + arch AS arch, + '' AS installed_path + FROM rpm_packages + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'npm_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM npm_packages + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + browser_type AS browser, + 'chrome_extensions' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM cached_users CROSS JOIN chrome_extensions USING (uid) + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + 'firefox' AS browser, + 'firefox_addons' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM cached_users CROSS JOIN firefox_addons USING (uid) + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'python_packages' AS source, + '' AS release, + '' AS vendor, + '' AS arch, + path AS installed_path + FROM python_packages + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Software Firefox + platform: darwin + description: A software override query to differentiate between Firefox and Firefox ESR on macOS. Requires fleetd + query: | + WITH app_paths AS ( + SELECT path + FROM apps + WHERE bundle_identifier = 'org.mozilla.firefox' + ), + remoting_name AS ( + SELECT value, path + FROM parse_ini + WHERE key = 'RemotingName' + AND path IN (SELECT CONCAT(path, '/Contents/Resources/application.ini') FROM app_paths) + ) + SELECT + CASE + WHEN remoting_name.value = 'firefox-esr' THEN 'Firefox ESR.app' + ELSE 'Firefox.app' + END AS name, + COALESCE(NULLIF(apps.bundle_short_version, ''), apps.bundle_version) AS version, + apps.bundle_identifier AS bundle_identifier, + '' AS extension_id, + '' AS browser, + 'apps' AS source, + '' AS vendor, + apps.last_opened_time AS last_opened_at, + apps.path AS installed_path + FROM apps + LEFT JOIN remoting_name ON apps.path = REPLACE(remoting_name.path, '/Contents/Resources/application.ini', '') + WHERE apps.bundle_identifier = 'org.mozilla.firefox' + discovery: parse_ini + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: VScode extensions + platform: darwin, windows, linux + description: Gathers information about Visual Studio Code extensions installed on a device. + query: | + WITH cached_users AS (WITH cached_groups AS (select * from groups) + SELECT uid, username, type, groupname, shell + FROM users LEFT JOIN cached_groups USING (gid) + WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '')) + SELECT + name, + version, + '' AS bundle_identifier, + uuid AS extension_id, + '' AS browser, + 'vscode_extensions' AS source, + publisher AS vendor, + '' AS last_opened_at, + path AS installed_path + FROM cached_users CROSS JOIN vscode_extensions USING (uid) + discovery: vscode_extensions + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Software (Windows) + platform: windows + description: Gathers information about software installed on a device running Windows. + query: | + WITH cached_users AS (WITH cached_groups AS (select * from groups) + SELECT uid, username, type, groupname, shell + FROM users LEFT JOIN cached_groups USING (gid) + WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '')) + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'programs' AS source, + publisher AS vendor, + install_location AS installed_path + FROM programs + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'python_packages' AS source, + '' AS vendor, + path AS installed_path + FROM python_packages + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'ie_extensions' AS source, + '' AS vendor, + path AS installed_path + FROM ie_extensions + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + browser_type AS browser, + 'chrome_extensions' AS source, + '' AS vendor, + path AS installed_path + FROM cached_users CROSS JOIN chrome_extensions USING (uid) + UNION + SELECT + name AS name, + version AS version, + identifier AS extension_id, + 'firefox' AS browser, + 'firefox_addons' AS source, + '' AS vendor, + path AS installed_path + FROM cached_users CROSS JOIN firefox_addons USING (uid) + UNION + SELECT + name AS name, + version AS version, + '' AS extension_id, + '' AS browser, + 'chocolatey_packages' AS source, + '' AS vendor, + path AS installed_path + FROM chocolatey_packages + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: System information + platform: windows + description: Retrieves information about a device's hardware. + query: SELECT * FROM system_info LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Uptime + platform: darwin, linux, windows + description: Retrieves the amount time passed since a device's last boot. + query: SELECT * FROM uptime LIMIT 1 + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Users + platform: darwin, linux, windows + description: Retrieves information about user accounts. + query: | + WITH cached_groups AS (select * from groups) + SELECT uid, username, type, groupname, shell + FROM users LEFT JOIN cached_groups USING (gid) + WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> '') + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Users (Chrome) + platform: chrome + description: Retrieves information about user accounts on a ChromeOS device. + query: | + SELECT + uid, username, email + FROM users; + purpose: Informational + tags: built-in +--- +apiVersion: v1 +kind: built-in +spec: + name: Windows update history + platform: windows + description: Retrieves the history of the update events on a Windows host. + query: | + SELECT + date, title + FROM windows_update_history + WHERE result_code = 'Succeeded'; + discovery: windows_update_history + purpose: Informational + tags: built-in +--- +# From docs/01-Using-Fleet/standard-query-library/standard-query-library.yml +apiVersion: v1 +kind: query +spec: + name: Get OpenSSL versions + platform: linux + description: Retrieves the OpenSSL version. + query: SELECT name AS name, version AS version, 'deb_packages' AS source FROM deb_packages WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'apt_sources' AS source FROM apt_sources WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'rpm_packages' AS source FROM rpm_packages WHERE name LIKE 'openssl%'; + purpose: Informational + tags: inventory + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get authorized SSH keys + platform: darwin, linux + description: Presence of authorized SSH keys may be unusual on laptops. Could be completely normal on servers, but may be worth auditing for unusual keys and/or changes. + query: SELECT username, authorized_keys. * FROM users CROSS JOIN authorized_keys USING (uid); + purpose: Informational + remediation: Check out the linked table (https://github.com/fleetdm/fleet/blob/32b4d53e7f1428ce43b0f9fa52838cbe7b413eed/handbook/queries/detect-hosts-with-high-severity-vulnerable-versions-of-openssl.md#table-of-vulnerable-openssl-versions) to determine if the installed version is a high severity vulnerability and view the corresponding CVE(s) + tags: built-in, ssh + contributors: mike-j-thomas +--- +apiVersion: v1 +kind: query +spec: + name: Get authorized keys for Domain Joined Accounts + platform: darwin, linux + description: List authorized_keys for each user on the system. + query: SELECT * FROM users CROSS JOIN authorized_keys USING(uid) WHERE username IN (SELECT distinct(username) FROM last); + purpose: Informational + tags: active directory, ssh + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get crashes + platform: darwin + description: Retrieve application, system, and mobile app crash logs. + query: SELECT uid, datetime, responsible, exception_type, identifier, version, crash_path FROM users CROSS JOIN crashes USING (uid); + purpose: Informational + tags: troubleshooting + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Chrome Extensions + platform: darwin, linux, windows + description: List installed Chrome Extensions for all users. + query: SELECT * FROM users CROSS JOIN chrome_extensions USING (uid); + purpose: Informational + tags: browser, built-in, inventory + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Linux software + platform: linux + description: Get all software installed on a Linux computer, including browser plugins and installed packages. Note that this does not include other running processes in the processes table. + query: SELECT name AS name, version AS version, 'Package (APT)' AS type, 'apt_sources' AS source FROM apt_sources UNION SELECT name AS name, version AS version, 'Package (deb)' AS type, 'deb_packages' AS source FROM deb_packages UNION SELECT package AS name, version AS version, 'Package (Portage)' AS type, 'portage_packages' AS source FROM portage_packages UNION SELECT name AS name, version AS version, 'Package (RPM)' AS type, 'rpm_packages' AS source FROM rpm_packages UNION SELECT name AS name, '' AS version, 'Package (YUM)' AS type, 'yum_sources' AS source FROM yum_sources UNION SELECT name AS name, version AS version, 'Package (NPM)' AS type, 'npm_packages' AS source FROM npm_packages UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages; + purpose: Informational + tags: inventory, built-in + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get installed macOS software + platform: darwin + description: Get all software installed on a macOS computer, including apps, browser plugins, and installed packages. Note that this does not include other running processes in the processes table. + query: SELECT name AS name, bundle_short_version AS version, 'Application (macOS)' AS type, 'apps' AS source FROM apps UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name As name, version AS version, 'Browser plugin (Safari)' AS type, 'safari_extensions' AS source FROM safari_extensions UNION SELECT name AS name, version AS version, 'Package (Homebrew)' AS type, 'homebrew_packages' AS source FROM homebrew_packages; + purpose: Informational + tags: inventory, built-in + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Safari extensions + platform: darwin + description: Retrieves the list of installed Safari Extensions for all users in the target system. + query: SELECT safari_extensions.* FROM users join safari_extensions USING (uid); + purpose: Informational + tags: browser, built-in, inventory + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get installed Windows software + platform: windows + description: Get all software installed on a Windows computer, including programs, browser plugins, and installed packages. Note that this does not include other running processes in the processes table. + query: SELECT name AS name, version AS version, 'Program (Windows)' AS type, 'programs' AS source FROM programs UNION SELECT name AS name, version AS version, 'Package (Python)' AS type, 'python_packages' AS source FROM python_packages UNION SELECT name AS name, version AS version, 'Browser plugin (IE)' AS type, 'ie_extensions' AS source FROM ie_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Chrome)' AS type, 'chrome_extensions' AS source FROM chrome_extensions UNION SELECT name AS name, version AS version, 'Browser plugin (Firefox)' AS type, 'firefox_addons' AS source FROM firefox_addons UNION SELECT name AS name, version AS version, 'Package (Chocolatey)' AS type, 'chocolatey_packages' AS source FROM chocolatey_packages; + purpose: Informational + tags: inventory, built-in + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get laptops with failing batteries + platform: darwin + description: Lists all laptops with under-performing or failing batteries. + query: SELECT * FROM battery WHERE health != 'Good' AND condition NOT IN ('', 'Normal'); + purpose: Informational + tags: troubleshooting, hardware, inventory + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get current users with active shell/console on the system + platform: darwin, linux, windows + description: Get current users with active shell/console on the system and associated process + query: SELECT user,host,time, p.name, p.cmdline, p.cwd, p.root FROM logged_in_users liu, processes p WHERE liu.pid = p.pid and liu.type='user' and liu.user <> '' ORDER BY time; + purpose: Informational + tags: hunting, built-in + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get unencrypted SSH keys for local accounts + platform: darwin, linux, windows + description: Identify SSH keys created without a passphrase which can be used in Lateral Movement (MITRE. TA0008) + query: SELECT uid, username, description, path, encrypted FROM users CROSS JOIN user_ssh_keys using (uid) WHERE encrypted=0; + purpose: Informational + tags: inventory, compliance, ssh, built-in + remediation: First, make the user aware about the impact of SSH keys. Then rotate the unencrypted keys detected. + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get unencrypted SSH keys for domain-joined accounts + platform: darwin, linux, windows + description: Identify SSH keys created without a passphrase which can be used in Lateral Movement (MITRE. TA0008) + query: SELECT uid, username, description, path, encrypted FROM users CROSS JOIN user_ssh_keys using (uid) WHERE encrypted=0 and username in (SELECT distinct(username) FROM last); + purpose: Informational + tags: inventory, compliance, ssh, active directory + remediation: First, make the user aware about the impact of SSH keys. Then rotate the unencrypted keys detected. + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get dynamic linker hijacking on Linux (MITRE. T1574.006) + platform: linux + description: Detect any processes that run with LD_PRELOAD environment variable + query: SELECT env.pid, env.key, env.value, p.name,p.path, p.cmdline, p.cwd FROM process_envs env join processes p USING (pid) WHERE key='LD_PRELOAD'; + purpose: Informational + tags: hunting, ATTACK, t1574 + remediation: Identify the process/binary detected and confirm with the system's owner. + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get dynamic linker hijacking on macOS (MITRE. T1574.006) + platform: darwin + description: Detect any processes that run with DYLD_INSERT_LIBRARIES environment variable + query: SELECT env.pid, env.key, env.value, p.name,p.path, p.cmdline, p.cwd FROM process_envs env join processes p USING (pid) WHERE key='DYLD_INSERT_LIBRARIES'; + purpose: Informational + tags: hunting, ATTACK, t1574 + remediation: Identify the process/binary detected and confirm with the system's owner. + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get etc hosts entries + platform: darwin, linux + description: Line-parsed /etc/hosts + query: SELECT * FROM etc_hosts WHERE address not in ('127.0.0.1', '::1'); + purpose: informational + tags: hunting, inventory + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get network interfaces + platform: darwin, linux, windows + description: Network interfaces MAC address + query: SELECT a.interface, a.address, d.mac FROM interface_addresses a JOIN interface_details d USING (interface) WHERE address not in ('127.0.0.1', '::1'); + purpose: informational + tags: hunting, inventory + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get local user accounts + platform: darwin, linux, windows + description: Local user accounts (including domain accounts that have logged on locally (Windows)). + query: SELECT uid, gid, username, description, directory, shell FROM users; + purpose: informational + tags: hunting, inventory + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get active user accounts on servers + platform: linux + description: Domain Joined environments normally have root or other service only accounts and users are SSH-ing using their Domain Accounts. + query: SELECT * FROM shadow WHERE password_status='active' and username!='root'; + purpose: informational + tags: hunting, inventory, Active Directory + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get Nmap scanner + platform: darwin, linux, windows + description: Get Nmap scanner process, as well as its user, parent, and process details. + query: SELECT p.pid, name, p.path, cmdline, cwd, start_time, parent, + (SELECT name FROM processes WHERE pid=p.parent) AS parent_name, + (SELECT username FROM users WHERE uid=p.uid) AS username + FROM processes as p WHERE cmdline like 'nmap%'; + purpose: Informational + tags: hunting, ATTACK, t1046 + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get Docker contained processes on a system + platform: darwin, linux + description: Docker containers Processes, can be used on normal systems or a kubenode. + query: SELECT c.id, c.name, c.image, c.image_id, c.command, c.created, c.state, c.status, p.cmdline FROM docker_containers c CROSS JOIN docker_container_processes p using(id); + purpose: Informational + tags: built-in, containers, inventory + contributors: anelshaer +--- +apiVersion: v1 +kind: query +spec: + name: Get Windows print spooler remote code execution vulnerability + platform: windows + description: Detects devices that are potentially vulnerable to CVE-2021-1675 because the print spooler service is not disabled. + query: SELECT CASE cnt WHEN 2 THEN "TRUE" ELSE "FALSE" END "Vulnerable" FROM (SELECT name start_type, COUNT(name) AS cnt FROM services WHERE name = 'NTDS' or (name = 'Spooler' and start_type <> 'DISABLED')) WHERE cnt = 2; + purpose: Informational + tags: vulnerability + contributors: maravedi +--- +apiVersion: v1 +kind: query +spec: + name: Get local users and their privileges + platform: darwin, linux, windows + description: Collects the local user accounts and their respective user group. + query: SELECT uid, username, type, groupname FROM users u JOIN groups g ON g.gid = u.gid; + purpose: informational + tags: inventory + contributors: noahtalerman +--- +apiVersion: v1 +kind: query +spec: + name: Get processes that no longer exist on disk + platform: linux, darwin, windows + description: Lists all processes of which the binary which launched them no longer exists on disk. Attackers often delete files from disk after launching a process to mask presence. + query: SELECT name, path, pid FROM processes WHERE on_disk = 0; + purpose: Incident response + tags: hunting, built-in + contributors: alphabrevity +--- +apiVersion: v1 +kind: query +spec: + name: Get user files matching a specific hash + platform: darwin, linux + description: Looks for specific hash in the Users/ directories for files that are less than 50MB (osquery file size limitation.) + query: SELECT path, sha256 FROM hash WHERE path IN (SELECT path FROM file WHERE size < 50000000 AND path LIKE '/Users/%/Documents/%%') AND sha256 = '16d28cd1d78b823c4f961a6da78d67a8975d66cde68581798778ed1f98a56d75'; + purpose: Informational + tags: hunting, built-in + contributors: alphabrevity +--- +apiVersion: v1 +kind: query +spec: + name: Get local administrator accounts on macOS + platform: darwin + description: The query allows you to check macOS systems for local administrator accounts. + query: SELECT uid, username, type FROM users u JOIN groups g ON g.gid = u.gid; + purpose: Informational + tags: hunting, inventory + contributors: alphabrevity +--- +apiVersion: v1 +kind: query +spec: + name: Get all listening ports, by process + platform: linux, darwin, windows + description: List ports that are listening on all interfaces, along with the process to which they are attached. + query: SELECT lp.address, lp.pid, lp.port, lp.protocol, p.name, p.path, p.cmdline FROM listening_ports lp JOIN processes p ON lp.pid = p.pid WHERE lp.address = "0.0.0.0"; + purpose: Informational + tags: hunting, network + contributors: alphabrevity +--- +apiVersion: v1 +kind: query +spec: + name: Get whether TeamViewer is installed/running + platform: windows + description: Looks for the TeamViewer service running on machines. This is often used when attackers gain access to a machine, running TeamViewer to allow them to access a machine. + query: SELECT display_name,status,s.pid,p.path FROM services AS s JOIN processes AS p USING(pid) WHERE s.name LIKE "%teamviewer%"; + purpose: Informational + tags: hunting, inventory + contributors: alphabrevity +--- +apiVersion: v1 +kind: query +spec: + name: Get malicious Python backdoors + platform: darwin, linux, windows + description: Watches for the backdoored Python packages installed on the system. See (http://www.nbu.gov.sk/skcsirt-sa-20170909-pypi/index.html) + query: SELECT CASE cnt WHEN 0 THEN "NONE_INSTALLED" ELSE "INSTALLED" END AS "Malicious Python Packages", package_name, package_version FROM (SELECT COUNT(name) AS cnt, name AS package_name, version AS package_version, path AS package_path FROM python_packages WHERE package_name IN ('acquisition', 'apidev-coop', 'bzip', 'crypt', 'django-server', 'pwd', 'setup-tools', 'telnet', 'urlib3', 'urllib')); + purpose: Informational + tags: hunting, inventory, malware + contributors: alphabrevity +--- +apiVersion: v1 +kind: query +spec: + name: Check for artifacts of the Floxif trojan + platform: windows + description: Checks for artifacts from the Floxif trojan on Windows machines. + query: SELECT * FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Piriform\\Agomo%'; + purpose: Informational + tags: hunting, malware + contributors: micheal-o +--- +apiVersion: v1 +kind: query +spec: + name: Get Shimcache table + platform: windows + description: Returns forensic data showing evidence of likely file execution, in addition to the last modified timestamp of the file, order of execution, full file path order of execution, and the order in which files were executed. + query: select * from Shimcache + purpose: Informational + tags: hunting + contributors: puffyCid +--- +apiVersion: v1 +kind: query +spec: + name: Get running docker containers + platform: darwin, linux + description: Returns the running Docker containers + query: SELECT id, name, image, image_id, state, status FROM docker_containers WHERE state = "running"; + purpose: Informational + tags: containers, inventory + contributors: DominusKelvin +--- +apiVersion: v1 +kind: query +spec: + name: Get applications hogging memory + platform: darwin, linux, windows + description: Returns top 10 applications or processes hogging memory the most. + query: SELECT pid, name, ROUND((total_size * '10e-7'), 2) AS memory_used FROM processes ORDER BY total_size DESC LIMIT 10; + purpose: Informational + tags: troubleshooting + contributors: DominusKelvin +--- +apiVersion: v1 +kind: query +spec: + name: Get servers with root login in the last 24 hours + platform: darwin, linux, windows + description: Returns servers with root login in the last 24 hours and the time the users were logged in. + query: SELECT * FROM last WHERE username = "root" AND time > (( SELECT unix_time FROM time ) - 86400 ); + purpose: Informational + tags: hunting + contributors: DominusKelvin +--- +apiVersion: v1 +kind: query +spec: + name: Detect active processes with Log4j running + platform: darwin, linux + description: "Returns a list of active processes and the Jar paths which are using Log4j. Version numbers are usually within the Jar filename. Note: This query is resource intensive and has caused problems on systems with limited swap space. Test on some systems before running this widely." + query: | + WITH target_jars AS ( + SELECT DISTINCT path + FROM ( + WITH split(word, str) AS( + SELECT '', cmdline || ' ' + FROM processes + UNION ALL + SELECT substr(str, 0, instr(str, ' ')), substr(str, instr(str, ' ') + 1) + FROM split + WHERE str != '') + SELECT word AS path + FROM split + WHERE word LIKE '%.jar' + UNION ALL + SELECT path + FROM process_open_files + WHERE path LIKE '%.jar' + ) + ) + SELECT path, matches + FROM yara + WHERE path IN (SELECT path FROM target_jars) + AND count > 0 + AND sigrule IN ( + 'rule log4jJndiLookup { + strings: + $jndilookup = "JndiLookup" + condition: + $jndilookup + }', + 'rule log4jJavaClass { + strings: + $javaclass = "org/apache/logging/log4j" + condition: + $javaclass + }' + ); + purpose: Detection + tags: vulnerability + contributors: zwass,tgauda +--- +apiVersion: v1 +kind: query +spec: + name: Get applications that were opened within the last 24 hours + platform: darwin + description: Returns applications that were opened within the last 24 hours starting with the last opened application. + query: SELECT * FROM apps WHERE last_opened_time > (( SELECT unix_time FROM time ) - 86400 ) ORDER BY last_opened_time DESC; + purpose: Informational + tags: inventory + contributors: DominusKelvin +--- +apiVersion: v1 +kind: query +spec: + name: Get applications that are not in the Applications directory + platform: darwin + description: Returns applications that are not in the `/Applications` directory + query: SELECT * FROM apps WHERE path NOT LIKE '/Applications/%'; + purpose: Informational + tags: hunting, inventory + contributors: DominusKelvin +--- +apiVersion: v1 +kind: query +spec: + name: Get subscription-based applications that have not been opened for the last 30 days + platform: darwin + description: Returns applications that are subscription-based and have not been opened for the last 30 days. You can replace the list of applications with those specific to your use case. + query: SELECT * FROM apps WHERE path LIKE '/Applications/%' AND name IN ("Photoshop.app", "Adobe XD.app", "Sketch.app", "Illustrator.app") AND last_opened_time < (( SELECT unix_time FROM time ) - 2592000000000 ); + purpose: Informational + tags: inventory + contributors: DominusKelvin +--- +apiVersion: v1 +kind: query +spec: + name: Get operating system information + platform: darwin, windows, linux + description: Returns the operating system name and version on the device. + query: SELECT name, version FROM os_version; + purpose: Informational + tags: inventory, built-in + contributors: noahtalerman +--- +apiVersion: v1 +kind: query +spec: + name: Get built-in antivirus status on macOS + platform: darwin + query: SELECT path, value AS version FROM plist WHERE (key = 'CFBundleShortVersionString' AND path = '/Library/Apple/System/Library/CoreServices/MRT.app/Contents/Info.plist') OR (key = 'CFBundleShortVersionString' AND path = '/Library/Apple/System/Library/CoreServices/XProtect.bundle/Contents/Info.plist'); + description: Reads the version numbers from the Malware Removal Tool (MRT) and built-in antivirus (XProtect) plists + purpose: Informational + tags: compliance, malware, hardening, built-in + contributors: GuillaumeRoss +--- +apiVersion: v1 +kind: query +spec: + name: Get antivirus status from the Windows Security Center + platform: windows + query: SELECT antivirus, signatures_up_to_date from windows_security_center CROSS JOIN windows_security_products WHERE type = 'Antivirus'; + description: Selects the antivirus and signatures status from Windows Security Center. + purpose: Informational + tags: compliance, malware, hardening, built-in + contributors: GuillaumeRoss +--- +apiVersion: v1 +kind: query +spec: + name: Get antivirus (ClamAV/clamd) and updater (freshclam) process status + platform: linux + query: SELECT pid, state, cmdline, name FROM processes WHERE name='clamd' OR name='freshclam'; + description: Selects the clamd and freshclam processes to ensure AV and its updater are running + purpose: Informational + tags: compliance, malware, hardening, built-in + contributors: GuillaumeRoss +--- +apiVersion: v1 +kind: query +spec: + name: Discover TLS certificates + platform: linux, windows, darwin + description: Retrieves metadata about TLS certificates for servers listening on the local machine. Enables mTLS adoption analysis and cert expiration notifications. + query: SELECT * FROM curl_certificate WHERE hostname IN (SELECT DISTINCT 'localhost:'||port FROM listening_ports WHERE protocol=6 AND address!='127.0.0.1' AND address!='::1'); + purpose: Informational + tags: network, tls + contributors: nabilschear +--- +apiVersion: v1 +kind: query +spec: + name: Discover Python Packages from Running Python Interpreters + platform: linux, darwin + description: Attempt to discover Python environments (in cwd, path to the python binary, and process command line) from running python interpreters and collect Python packages from those environments. + query: SELECT * FROM python_packages WHERE directory IN (SELECT DISTINCT directory FROM (SELECT SUBSTR(path,0,INSTR(path,'/bin/'))||'/lib' AS directory FROM processes WHERE path LIKE '%/bin/%' AND path LIKE '%python%' UNION SELECT SUBSTR(cmdline,0,INSTR(cmdline,'/bin/'))||'/lib' AS directory FROM processes WHERE cmdline LIKE '%python%' AND cmdline LIKE '%/bin/%' AND path LIKE '%python%' UNION SELECT cwd||'/lib' AS directory FROM processes WHERE path LIKE '%python%')); + purpose: Informational + tags: compliance, hunting + contributors: nabilschear +--- +apiVersion: v1 +kind: query +spec: + name: Identify the default mail, http and ftp applications + platforms: macOS + platform: darwin + description: Lists the currently enabled applications configured to handle mailto, http and ftp schemes. + query: SELECT * FROM app_schemes WHERE (scheme='mailto' OR scheme='http' OR scheme='ftp') AND enabled='1'; + purpose: Informational + tags: compliance, hunting + contributors: brunerd +--- +apiVersion: v1 +kind: query +spec: + name: Identify Apple development secrets (macOS) + query: SELECT * FROM keychain_items WHERE label LIKE '%ABCDEFG%'; + description: "Identifies certificates associated with Apple development signing and notarization. Replace ABCDEFG with your company's identifier." + tags: compliance, inventory, built-in + platform: darwin + contributors: GuillaumeRoss +--- +apiVersion: v1 +kind: query +spec: + name: Geolocate via ipapi.co + platform: darwin, linux, windows + description: Geolocate a host using the [ipapi.co](https://ipapi.co) in an emergency. Requires the curl table. [Learn more](https://fleetdm.com/guides/locate-assets-with-osquery). + query: >- + SELECT JSON_EXTRACT(result, '$.ip') AS ip, + JSON_EXTRACT(result, '$.city') AS city, + JSON_EXTRACT(result, '$.region') AS region, + JSON_EXTRACT(result, '$.country') AS country, + JSON_EXTRACT(result, '$.latitude') AS latitude, + JSON_EXTRACT(result, '$.longitude') AS longitude + FROM curl + WHERE url = 'http://ipapi.co/json'; + purpose: inventory + tags: inventory + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get Crowdstrike Falcon network content filter status + platform: darwin + description: Get the status of the Crowdstrike Falcon network content filter (as in "System Settings" > "Network > "Filters"). + query: /* Load up the plist */ WITH extensions_plist AS (SELECT *, rowid FROM plist WHERE path = '/Library/Preferences/com.apple.networkextension.plist') /* Find the first "Enabled" key after the key indicating the crowdstrike app */ SELECT value AS enabled FROM extensions_plist WHERE subkey = 'Enabled' AND rowid > (SELECT rowid FROM extensions_plist WHERE value = 'com.crowdstrike.falcon.App') LIMIT 1; + purpose: Informational + tags: crowdstrike, plist, network, content filter + contributors: zwass +--- +apiVersion: v1 +kind: query +spec: + name: Get a list of Visual Studio Code extensions + platform: darwin, linux, windows + description: Get a list of installed VS Code extensions (requires osquery > 5.11.0). + query: | + SELECT u.username, vs.* FROM users u CROSS JOIN vscode_extensions vs USING (uid); + purpose: Informational + tags: inventory + contributors: lucasmrod,sharon-fdm,zwass +--- +apiVersion: v1 +kind: query +spec: + name: List osquery table names + platform: darwin, linux, windows + description: List all table names in the schema of the currently installed version of osquery + query: SELECT DISTINCT name FROM osquery_registry; + purpose: Informational + tags: fleet, osquery, table, schema + contributors: nonpunctual diff --git a/website/api/controllers/view-policy-details.js b/website/api/controllers/view-policy-details.js new file mode 100644 index 000000000000..777c57936393 --- /dev/null +++ b/website/api/controllers/view-policy-details.js @@ -0,0 +1,77 @@ +module.exports = { + + + friendlyName: 'View policy detail', + + + description: 'Display "policy details" page.', + + + inputs: { + slug: { type: 'string', required: true, description: 'A slug uniquely identifying this policy in the library.', example: 'get-macos-disk-free-space-percentage' }, + }, + + + exits: { + success: { viewTemplatePath: 'pages/policy-details' }, + notFound: { responseType: 'notFound' }, + badConfig: { responseType: 'badConfig' }, + }, + + + fn: async function ({ slug }) { + + if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.policies)) { + throw {badConfig: 'builtStaticContent.policies'}; + } else if (!_.isString(sails.config.builtStaticContent.policyLibraryYmlRepoPath)) { + throw {badConfig: 'builtStaticContent.queryLibraryYmlRepoPath'}; + } + + // Serve appropriate content for policy. + // > Inspired by https://github.com/sailshq/sailsjs.com/blob/b53c6e6a90c9afdf89e5cae00b9c9dd3f391b0e7/api/controllers/documentation/view-documentation.js + let policy = _.find(sails.config.builtStaticContent.policies, { slug: slug }); + if (!policy) { + throw 'notFound'; + } + + // Find the related osquery table documentation for tables used in this query, and grab the keywordsForSyntaxHighlighting from each table used. + let allTablesInformation = _.filter(sails.config.builtStaticContent.markdownPages, (pageInfo)=>{ + return _.startsWith(pageInfo.url, '/tables/'); + }); + // Get all the osquery table names, we'll use this list to determine which tables are used. + let allTableNames = _.pluck(allTablesInformation, 'title'); + // Create an array of words in the query. + let queryWords = _.words(policy.query, /[^ \n;]+/g); + let columnNamesForSyntaxHighlighting = []; + let tableNamesForSyntaxHighlighting = []; + // Get all of the words that appear in both arrays + let intersectionBetweenQueryWordsAndTableNames = _.intersection(queryWords, allTableNames); + // For each matched osquery table, add the keywordsForSyntaxHighlighting and the names of the tables used into two arrays. + for(let tableName of intersectionBetweenQueryWordsAndTableNames) { + let tableMentionedInThisQuery = _.find(sails.config.builtStaticContent.markdownPages, {title: tableName}); + tableNamesForSyntaxHighlighting.push(tableMentionedInThisQuery.title); + let keyWordsForThisTable = tableMentionedInThisQuery.keywordsForSyntaxHighlighting; + columnNamesForSyntaxHighlighting = columnNamesForSyntaxHighlighting.concat(keyWordsForThisTable); + } + // Remove the table names from the array of column names to highlight. + columnNamesForSyntaxHighlighting = _.difference(columnNamesForSyntaxHighlighting, tableNamesForSyntaxHighlighting); + + + // Setting the meta title and description of this page using the query object, and falling back to a generic title or description if policy.name or policy.description are missing. + let pageTitleForMeta = policy.name ? policy.name + ' | Policy details' : 'Policy details'; + let pageDescriptionForMeta = policy.description ? policy.description : 'View more information about a policy in Fleet\'s standard query library'; + // Respond with view. + return { + policy, + queryLibraryYmlRepoPath: sails.config.builtStaticContent.policyLibraryYmlRepoPath, + pageTitleForMeta, + pageDescriptionForMeta, + columnNamesForSyntaxHighlighting, + tableNamesForSyntaxHighlighting, + algoliaPublicKey: sails.config.custom.algoliaPublicKey, + }; + + } + + +}; diff --git a/website/api/controllers/view-policy-library.js b/website/api/controllers/view-policy-library.js new file mode 100644 index 000000000000..a1b53e086e39 --- /dev/null +++ b/website/api/controllers/view-policy-library.js @@ -0,0 +1,55 @@ +module.exports = { + + + friendlyName: 'View policy library', + + + description: 'Display "Policy library" page.', + + + exits: { + + success: { + viewTemplatePath: 'pages/policy-library' + }, + badConfig: { responseType: 'badConfig' }, + + }, + + + fn: async function () { + + if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.policies)) { + throw {badConfig: 'builtStaticContent.policies'}; + } + let policies = _.where(sails.config.builtStaticContent.policies, {kind: 'policy'}); + let macOsPolicies = _.filter(policies, (policy)=>{ + let platformsForThisPolicy = policy.platform.split(','); + return _.includes(platformsForThisPolicy, 'darwin'); + }); + let windowsPolicies = _.filter(policies, (policy)=>{ + let platformsForThisPolicy = policy.platform.split(','); + return _.includes(platformsForThisPolicy, 'windows'); + }); + let linuxPolicies = _.filter(policies, (policy)=>{ + let platformsForThisPolicy = policy.platform.split(','); + return _.includes(platformsForThisPolicy, 'linux'); + }); + let chromePolicies = _.filter(policies, (policy)=>{ + let platformsForThisPolicy = policy.platform.split(','); + return _.includes(platformsForThisPolicy, 'chrome'); + }); + // Respond with view. + return { + macOsPolicies, + windowsPolicies, + linuxPolicies, + chromePolicies, + algoliaPublicKey: sails.config.custom.algoliaPublicKey, + }; + + + } + + +}; diff --git a/website/api/controllers/view-query-detail.js b/website/api/controllers/view-query-detail.js index b02ba24ee93c..a15ecf13846d 100644 --- a/website/api/controllers/view-query-detail.js +++ b/website/api/controllers/view-query-detail.js @@ -16,6 +16,10 @@ module.exports = { success: { viewTemplatePath: 'pages/query-detail' }, notFound: { responseType: 'notFound' }, badConfig: { responseType: 'badConfig' }, + redirectToPolicy: { + description: 'The requesting user has been redirected to a policy page.', + responseType: 'redirect' + }, }, @@ -23,6 +27,8 @@ module.exports = { if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.queries)) { throw {badConfig: 'builtStaticContent.queries'}; + } else if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.policies)) { + throw {badConfig: 'builtStaticContent.policies'}; } else if (!_.isString(sails.config.builtStaticContent.queryLibraryYmlRepoPath)) { throw {badConfig: 'builtStaticContent.queryLibraryYmlRepoPath'}; } @@ -31,7 +37,15 @@ module.exports = { // > Inspired by https://github.com/sailshq/sailsjs.com/blob/b53c6e6a90c9afdf89e5cae00b9c9dd3f391b0e7/api/controllers/documentation/view-documentation.js let query = _.find(sails.config.builtStaticContent.queries, { slug: slug }); if (!query) { - throw 'notFound'; + // If we didn't find a query matching this slug, check to see if there is a policy with a matching slug. + // Note: We do this because policies used to be on /queries/* pages. This way, all old URLs that policies used to live at will still bring users to the correct page. + let policyWithThisSlug = _.find(sails.config.builtStaticContent.policies, {kind: 'policy', slug: slug}); + if(policyWithThisSlug){ + // If we foudn a matchign policy, redirect the user. + throw {redirect: `/policies/${policyWithThisSlug.slug}`}; + } else { + throw 'notFound'; + } } // Find the related osquery table documentation for tables used in this query, and grab the keywordsForSyntaxHighlighting from each table used. @@ -41,7 +55,7 @@ module.exports = { // Get all the osquery table names, we'll use this list to determine which tables are used. let allTableNames = _.pluck(allTablesInformation, 'title'); // Create an array of words in the query. - let queryWords = _.words(query.query, /[^ ]+/g); + let queryWords = _.words(query.query, /[^ \n;]+/g); let columnNamesForSyntaxHighlighting = []; let tableNamesForSyntaxHighlighting = []; // Get all of the words that appear in both arrays diff --git a/website/api/controllers/view-query-library.js b/website/api/controllers/view-query-library.js index b65f7387fc48..1a748c755df6 100644 --- a/website/api/controllers/view-query-library.js +++ b/website/api/controllers/view-query-library.js @@ -18,24 +18,24 @@ module.exports = { if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.queries)) { throw {badConfig: 'builtStaticContent.queries'}; } - let policies = _.where(sails.config.builtStaticContent.queries, {kind: 'policy'}); - let macOsPolicies = _.filter(policies, (policy)=>{ + let policies = _.where(sails.config.builtStaticContent.queries, {kind: 'query'}); + let macOsQueries = _.filter(policies, (policy)=>{ let platformsForThisPolicy = policy.platform.split(','); return _.includes(platformsForThisPolicy, 'darwin'); }); - let windowsPolicies = _.filter(policies, (policy)=>{ + let windowsQueries = _.filter(policies, (policy)=>{ let platformsForThisPolicy = policy.platform.split(','); return _.includes(platformsForThisPolicy, 'windows'); }); - let linuxPolicies = _.filter(policies, (policy)=>{ + let linuxQueries = _.filter(policies, (policy)=>{ let platformsForThisPolicy = policy.platform.split(','); return _.includes(platformsForThisPolicy, 'linux'); }); // Respond with view. return { - macOsPolicies, - windowsPolicies, - linuxPolicies, + macOsQueries, + windowsQueries, + linuxQueries, algoliaPublicKey: sails.config.custom.algoliaPublicKey, }; diff --git a/website/api/controllers/view-vital-details.js b/website/api/controllers/view-vital-details.js new file mode 100644 index 000000000000..ea4a44676929 --- /dev/null +++ b/website/api/controllers/view-vital-details.js @@ -0,0 +1,97 @@ +module.exports = { + + + friendlyName: 'View vital details', + + + description: 'Display "Vital details" page.', + + + inputs: { + slug: { type: 'string', required: true, description: 'A slug uniquely identifying this query in the library.', example: 'get-macos-disk-free-space-percentage' }, + }, + + + exits: { + success: { viewTemplatePath: 'pages/vital-details' }, + notFound: { responseType: 'notFound' }, + badConfig: { responseType: 'badConfig' }, + }, + + + fn: async function ({ slug }) { + + if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.queries)) { + throw {badConfig: 'builtStaticContent.queries'}; + } else if (!_.isString(sails.config.builtStaticContent.queryLibraryYmlRepoPath)) { + throw {badConfig: 'builtStaticContent.queryLibraryYmlRepoPath'}; + } + + // Serve appropriate content for vital. + // > Inspired by https://github.com/sailshq/sailsjs.com/blob/b53c6e6a90c9afdf89e5cae00b9c9dd3f391b0e7/api/controllers/documentation/view-documentation.js + let thisVital = _.find(sails.config.builtStaticContent.queries, { slug: slug }); + if (!thisVital) { + throw 'notFound'; + } + + // Find the related osquery table documentation for tables used in this query, and grab the keywordsForSyntaxHighlighting from each table used. + let allTablesInformation = _.filter(sails.config.builtStaticContent.markdownPages, (pageInfo)=>{ + return _.startsWith(pageInfo.url, '/tables/'); + }); + // Get all the osquery table names, we'll use this list to determine which tables are used. + let allTableNames = _.pluck(allTablesInformation, 'title'); + // Create an array of words in the vital. + let queryWords = _.words(thisVital.query, /[^ \n;]+/g); + let columnNamesForSyntaxHighlighting = []; + let tableNamesForSyntaxHighlighting = []; + // Get all of the words that appear in both arrays + let intersectionBetweenQueryWordsAndTableNames = _.intersection(queryWords, allTableNames); + // For each matched osquery table, add the keywordsForSyntaxHighlighting and the names of the tables used into two arrays. + for(let tableName of intersectionBetweenQueryWordsAndTableNames) { + let tableMentionedInThisQuery = _.find(sails.config.builtStaticContent.markdownPages, {title: tableName}); + tableNamesForSyntaxHighlighting.push(tableMentionedInThisQuery.title); + let keyWordsForThisTable = tableMentionedInThisQuery.keywordsForSyntaxHighlighting; + columnNamesForSyntaxHighlighting = columnNamesForSyntaxHighlighting.concat(keyWordsForThisTable); + } + // Remove the table names from the array of column names to highlight. + columnNamesForSyntaxHighlighting = _.difference(columnNamesForSyntaxHighlighting, tableNamesForSyntaxHighlighting); + + // Setting the meta title and description of this page using the query object, and falling back to a generic title or description if vital.name or vital.description are missing. + let pageTitleForMeta = thisVital.name ? thisVital.name + ' | Vital details' : 'Vital details'; + let pageDescriptionForMeta = thisVital.description ? thisVital.description : 'Explore Fleet’s built-in queries for collecting and storing important device information.'; + let vitals = _.where(sails.config.builtStaticContent.queries, {kind: 'built-in'}); + let macOsVitals = _.filter(vitals, (vital)=>{ + let platformsForThisPolicy = vital.platform.split(', '); + return _.includes(platformsForThisPolicy, 'darwin'); + }); + let windowsVitals = _.filter(vitals, (vital)=>{ + let platformsForThisPolicy = vital.platform.split(', '); + return _.includes(platformsForThisPolicy, 'windows'); + }); + let linuxVitals = _.filter(vitals, (vital)=>{ + let platformsForThisPolicy = vital.platform.split(', '); + return _.includes(platformsForThisPolicy, 'linux'); + }); + let chromeVitals = _.filter(vitals, (vital)=>{ + let platformsForThisPolicy = vital.platform.split(', '); + return _.includes(platformsForThisPolicy, 'chrome'); + }); + // Respond with view. + return { + thisVital, + macOsVitals, + windowsVitals, + linuxVitals, + chromeVitals, + queryLibraryYmlRepoPath: sails.config.builtStaticContent.queryLibraryYmlRepoPath, + pageTitleForMeta, + pageDescriptionForMeta, + columnNamesForSyntaxHighlighting, + tableNamesForSyntaxHighlighting, + algoliaPublicKey: sails.config.custom.algoliaPublicKey, + }; + + } + + +}; diff --git a/website/assets/images/icon-select-arrows-16x16@2x.png b/website/assets/images/icon-select-arrows-16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cab5c52f8b8d191b478864c760161261898dc6e6 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy#sNMdu0T34yfh%9A~3uJhyntt zfn-p4DU=&g5*P-Q1xghMg_i|Hfy6DBT&V_X6DSGt3ud^l@IFCc|NMpmhw}?2^c!5> zT(uJ@*Y4@!7-Dhy?4>}VW(9%fhYL8IqG$h}zfHE(#k^sCo%)S>GlMF;DJZomTNnQkr5mXJ;!t%Gq_= zWtrmrA0Lw1IBY8K?ztVhd~S)styQhJqPRk*Ib}`-Bh%JfS={xreh8fZx}v%d=p+VD LS3j3^P6{ + return a.length < b.length ? 1 : -1; + }); + tableNamesForThisQuery = tableNamesForThisQuery.sort((a,b)=>{ + return a.length < b.length ? 1 : -1; + }); + (()=>{ + $('pre code').each((i, block) => { + let tableNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight + for(let tableName of tableNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the + for(let match of block.innerHTML.match(tableName)|| []){ + tableNamesToHighlight.push(match); + } + } + // Now iterate through the tableNamesToHighlight, replacing all matches in the elements innerHTML. + let replacementHMTL = block.innerHTML; + for(let keywordInExample of tableNamesToHighlight) { + let regexForThisExample = new RegExp(keywordInExample, 'g'); + replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+''); + } + $(block).html(replacementHMTL); + let columnNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight + for(let columnName of columnNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the + for(let match of block.innerHTML.match(columnName)||[]){ + columnNamesToHighlight.push(match); + } + } + + for(let keywordInExample of columnNamesToHighlight) { + let regexForThisExample = new RegExp(keywordInExample, 'g'); + replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+''); + } + $(block).html(replacementHMTL); + window.hljs.highlightElement(block); + // After we've highlighted our keywords, we'll highlight the rest of the codeblock + // If this example is a single-line, we'll do some basic formatting to make it more human-readable. + if($(block)[0].innerText.match(/\n/gmi)){ + $(block).addClass('has-linebreaks'); + } else { + $(block).addClass('no-linebreaks'); + } + }); + })(); + $('[purpose="copy-button"]').on('click', async function() { + let code = $(this).siblings('pre').find('code').text(); + $(this).addClass('copied'); + await setTimeout(()=>{ + $(this).removeClass('copied'); + }, 2000); + navigator.clipboard.writeText(code); + }); + }, + + // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ + // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ + // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ + methods: { + //… + }, +}); diff --git a/website/assets/js/pages/policy-library.page.js b/website/assets/js/pages/policy-library.page.js new file mode 100644 index 000000000000..39b33218e2d7 --- /dev/null +++ b/website/assets/js/pages/policy-library.page.js @@ -0,0 +1,43 @@ +parasails.registerPage('policy-library', { + // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ + // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ + // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ + data: { + selectedPlatform: 'macos', // Initially set to 'macos' + }, + + // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ + // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ + // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ + beforeMount: function () { + //… + }, + mounted: async function () { + // if(this.algoliaPublicKey) { // Note: Docsearch will only be enabled if sails.config.custom.algoliaPublicKey is set. If the value is undefined, the documentation search will be disabled. + // docsearch({ + // appId: 'NZXAYZXDGH', + // apiKey: this.algoliaPublicKey, + // indexName: 'fleetdm', + // container: '#docsearch-query', + // placeholder: 'Search', + // debug: false, + // searchParameters: { + // 'facetFilters': ['section:queries'] + // }, + // }); + // } + }, + + // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ + // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ + // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ + methods: { + + clickSelectPlatform: function(platform) { + this.selectedPlatform = platform; + }, + + }, + +}); + diff --git a/website/assets/js/pages/query-detail.page.js b/website/assets/js/pages/query-detail.page.js index 8622000bbcb7..64d651c8b4aa 100644 --- a/website/assets/js/pages/query-detail.page.js +++ b/website/assets/js/pages/query-detail.page.js @@ -46,7 +46,7 @@ parasails.registerPage('query-detail', { $('pre code').each((i, block) => { let tableNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight for(let tableName of tableNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the - for(let match of block.innerHTML.match(tableName+' ')||[]){ + for(let match of block.innerHTML.match(tableName)||[]){ tableNamesToHighlight.push(match); } } @@ -54,7 +54,7 @@ parasails.registerPage('query-detail', { let replacementHMTL = block.innerHTML; for(let keywordInExample of tableNamesToHighlight) { let regexForThisExample = new RegExp(keywordInExample, 'g'); - replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+' '); + replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+''); } $(block).html(replacementHMTL); let columnNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight diff --git a/website/assets/js/pages/query-library.page.js b/website/assets/js/pages/query-library.page.js index 204fb7c0b457..6060c6215044 100644 --- a/website/assets/js/pages/query-library.page.js +++ b/website/assets/js/pages/query-library.page.js @@ -33,7 +33,7 @@ parasails.registerPage('query-library', { // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ methods: { - clickSelectPlatform(platform) { + clickSelectPlatform: function(platform) { this.selectedPlatform = platform; }, diff --git a/website/assets/js/pages/vital-details.page.js b/website/assets/js/pages/vital-details.page.js new file mode 100644 index 000000000000..0ab476bae5ba --- /dev/null +++ b/website/assets/js/pages/vital-details.page.js @@ -0,0 +1,116 @@ +parasails.registerPage('vital-details', { + // ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗ + // ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣ + // ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝ + data: { + contributors: [], + selectedPlatform: 'macos', // Initially set to 'macos' + modal: '', + }, + + // ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗ + // ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣ + // ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝ + beforeMount: function () { + //… + }, + mounted: async function () { + // Set the selected platform from the hash in the user's URL. + // All links to vitals in the on-page navigation have the currently selected filter appended to them, this lets us persist the user's filter when they navigate to a new page. + if(['#macos','#linux','#windows','#chrome'].includes(window.location.hash)){ + this.selectedPlatform = window.location.hash.split('#')[1]; + } + + // Note: Docsearch will be disabled on this page until a search index has been created for it. + // Note: Docsearch will only be enabled if sails.config.custom.algoliaPublicKey is set. If the value is undefined, the documentation search will be disabled. + // if(this.algoliaPublicKey) { + // docsearch({ + // appId: 'NZXAYZXDGH', + // apiKey: this.algoliaPublicKey, + // indexName: 'fleetdm', + // container: '#docsearch-query', + // placeholder: 'Search', + // debug: false, + // searchParameters: { + // 'facetFilters': ['section:queries'] + // }, + // }); + // } + let columnNamesForThisQuery = []; + let tableNamesForThisQuery = []; + if(this.columnNamesForSyntaxHighlighting){ + columnNamesForThisQuery = this.columnNamesForSyntaxHighlighting; + } + if(this.tableNamesForSyntaxHighlighting){ + tableNamesForThisQuery = this.tableNamesForSyntaxHighlighting; + } + // Sorting the arrays of keywords by length to match larger keywords first. + columnNamesForThisQuery = columnNamesForThisQuery.sort((a,b)=>{ + return a.length < b.length ? 1 : -1; + }); + tableNamesForThisQuery = tableNamesForThisQuery.sort((a,b)=>{ + return a.length < b.length ? 1 : -1; + }); + (()=>{ + $('pre code').each((i, block) => { + let tableNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight + for(let tableName of tableNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the + for(let match of block.innerHTML.match(tableName)||[]){ + tableNamesToHighlight.push(match); + } + } + // Now iterate through the tableNamesToHighlight, replacing all matches in the elements innerHTML. + let replacementHMTL = block.innerHTML; + for(let keywordInExample of tableNamesToHighlight) { + let regexForThisExample = new RegExp(keywordInExample, 'g'); + replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+''); + } + $(block).html(replacementHMTL); + let columnNamesToHighlight = [];// Empty array to track the keywords that we will need to highlight + for(let columnName of columnNamesForThisQuery){// Going through the array of keywords for this table, if the entire word matches, we'll add it to the + for(let match of block.innerHTML.match(columnName)||[]){ + columnNamesToHighlight.push(match); + } + } + + for(let keywordInExample of columnNamesToHighlight) { + let regexForThisExample = new RegExp(keywordInExample, 'g'); + replacementHMTL = replacementHMTL.replace(regexForThisExample, ''+_.trim(keywordInExample)+''); + } + $(block).html(replacementHMTL); + window.hljs.highlightElement(block); + // After we've highlighted our keywords, we'll highlight the rest of the codeblock + // If this example is a single-line, we'll do some basic formatting to make it more human-readable. + if($(block)[0].innerText.match(/\n/gmi)){ + $(block).addClass('has-linebreaks'); + } else { + $(block).addClass('no-linebreaks'); + } + }); + })(); + $('[purpose="copy-button"]').on('click', async function() { + let code = $(this).siblings('pre').find('code').text(); + $(this).addClass('copied'); + await setTimeout(()=>{ + $(this).removeClass('copied'); + }, 2000); + navigator.clipboard.writeText(code); + }); + }, + + // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ + // ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗ + // ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ + methods: { + clickSelectPlatform: function (platform) { + this.selectedPlatform = platform; + }, + clickOpenTableOfContents: function () { + this.modal = 'table-of-contents'; + }, + closeModal: async function() { + this.modal = ''; + await this.forceRender(); + } + }, +}); diff --git a/website/assets/styles/importer.less b/website/assets/styles/importer.less index b44c9b3303d8..19e29e9058d2 100644 --- a/website/assets/styles/importer.less +++ b/website/assets/styles/importer.less @@ -82,3 +82,6 @@ @import 'pages/app-details.less'; @import 'pages/meetups.less'; @import 'pages/admin/query-generator.less'; +@import 'pages/vital-details.less'; +@import 'pages/policy-library.less'; +@import 'pages/policy-details.less'; diff --git a/website/assets/styles/pages/policy-details.less b/website/assets/styles/pages/policy-details.less new file mode 100644 index 000000000000..01c99bf83692 --- /dev/null +++ b/website/assets/styles/pages/policy-details.less @@ -0,0 +1,410 @@ +#policy-details { + // background-color: #FFF; + h3 { + padding-bottom: 24px; + padding-top: 32px; + color: #192147; + font-size: 24px; + font-weight: 800; + line-height: 120%; + margin-bottom: 0px; + } + + p { + color: #515774; + font-size: 16px; + font-weight: 400; + line-height: 150%; + } + + + [purpose='page-container'] { + padding: 64px 64px 32px 64px; + } + [purpose='page-content'] { + margin-left: auto; + margin-right: auto; + max-width: 1072px; + } + + [purpose='breadcrumbs-and-search'] { + margin-bottom: 64px; + max-width: 1072px; + font-size: 14px; + [purpose='breadcrumbs'] { + margin-right: 24px; + } + [purpose='search'] { + // Note: We're using classes here to override the default Docsearch styles; + button { + width: 100%; + cursor: text; + margin: 0; + } + .DocSearch-Button { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + border: 1px solid @core-fleet-black-25; + background-color: #FFF; + padding: 6px; + height: 36px; + margin: 0; + width: 256px; + } + .DocSearch-Button:hover { + box-shadow: none; + border: 1px solid @core-fleet-black-25; + color: @core-fleet-black-50; + } + .DocSearch-Search-Icon { + margin-left: 10px; + height: 16px; + width: 16px; + color: @core-fleet-black-50; + stroke-width: 3px; + } + .DocSearch-Button-Keys { + display: none; + } + .input-group:focus-within { + border: 1px solid @core-vibrant-blue; + } + .DocSearch-Button-Placeholder { + font-size: 16px; + font-weight: 400; + padding-left: 12px; + } + [purpose='disabled-search'] { + input { + padding-top: 6px; + padding-bottom: 6px; + border: none; + } &::placeholder { + font-size: 16px; + line-height: 24px; + color: #8B8FA2; + } + .input-group { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + border: 1px solid @core-fleet-black-25; + background: #FFF; + } + .input-group:focus-within { + border: 1px solid @core-vibrant-blue; + } + .form-control { + border-radius: 6px; + padding: 6px; + height: 36px; + margin: 0; + width: 212px; + } + .docsearch-input:focus-visible { + outline: none; + } + .ds-input:focus { + outline: rgba(0, 0, 0, 0); + } + .input-group-text { + color: @core-fleet-black-50; + } + .form-control { + height: 36px; + padding: 0px; + font-size: 16px; + } &:focus { + border: none; + } + } + } + + [purpose='breadcrumbs-category'] { + color: #8B8FA2; + margin-right: 8px; + font-size: 14px; + font-weight: 400; + line-height: 150%; /* */ + &:hover { + color: #192147; + text-decoration: none; + } + } + [purpose='breadcrumbs-title'] { + margin-left: 8px; + } + } + + [purpose='policy-name'] { + color: #192147; + font-size: 32px; + font-style: normal; + font-weight: 800; + line-height: 150%; + margin-bottom: 0px; + } + [purpose='policy-description'] { + color: #515774; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; + margin-bottom: 0px; + padding: 16px 0px 32px 0px; + } + [purpose='policy-attribution'] { + display: flex; + flex-direction: row; + align-items: center; + padding-top: 8px; + padding-bottom: 16px; + [purpose='contributor-profile-picture'] { + width: 28.8px; + height: 28.8px; + margin-right: 8px; + border-radius: 50%; + } + [purpose='contributor-profile-name'] { + font-size: 14px; + line-height: 150%; + } + [purpose='policy-link'] { + color: unset; + text-decoration: none; + + &:hover { + text-decoration: none; + } + } + + + } + [purpose='policy-details'] { + padding-right: 64px; + max-width: 800px; + width: 100%; + } + [purpose='policy-check'] { + padding-bottom: 24px; + } + + [purpose='right-sidebar'] { + width: 256px; + margin-left: 16px; + font-size: 14px; + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 500ms; + a:not([purpose='edit-button']) { + margin-bottom: 8px; + display: block; + color: #515774; + &:hover { + text-decoration: none; + color: @core-fleet-black; + } + } + } + [purpose='docs-links'] { + a { + display: block; + } + } + [purpose='social-share-buttons'] { + padding-bottom: 24px; + margin-bottom: 24px; + border-bottom: 1px solid #E2E4EA; + a { + margin-right: 16px; + } + img { + height: 20px; + width: 20px; + } + } + [purpose='policy-platform'] { + border-bottom: 1px solid #E2E4EA; + padding-bottom: 24px; + padding-top: 16px; + margin-bottom: 24px; + h4 { + color: #192147; + font-size: 14px; + font-weight: 700; + line-height: 150%; + padding-bottom: 8px; + margin-bottom: 8px; + } + img { + height: 24px; + margin-right: 16px; + } + } + [purpose='edit-button'] { + margin-top: 24px; + img { + width: 16px; + height: 16px; + display: inline; + margin-right: 8px; + } + padding: 6px 8px; + display: block; + color: @core-fleet-black-75; + text-decoration: none; + font-size: 14px; + line-height: 21px; + border-radius: 6px; + width: 102px; + background: rgba(25, 33, 71, 0.05); + &:hover { + background-color: rgba(25, 33, 71, 0.1); + } + &:active { + background-color: rgba(25, 33, 71, 0.1); + } + } + @import '../pages/docs/code-blocks.less'; // styles for code blocks and hljs + [purpose='codeblock'] { + padding: 0; + position: relative; + [purpose='copy-button'] { + position: absolute; + top: 11px; + right: 10px; + border-radius: 8px; + height: 32px; + width: 32px; + background: url('/images/icon-copy-16x16@2x.png'); + background-color: #F9FAFC; + background-size: 14px 14px; + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + &:hover { + background-color: #F2F2F5; + } + &.copied { + background: url('/images/icon-copy-clicked-checkmark-32x32@2x.png'); + background-size: 32px 32px; + background-repeat: no-repeat; + background-position: center; + } + } + } + + pre { + width: 100%; + max-width: 100%; + padding: 16px 44px 16px 24px; + border: 1px solid #E2E4EA; + background: #F9FAFC; + border-radius: 4px; + margin-top: 16px; + margin-bottom: 24px; + code { + color: #515774; + &.has-linebreaks { + white-space: break-spaces; + } + &.no-linebreaks { + word-break: break-word; + white-space: normal; + } + font-family: 'Source Code Pro'; + font-size: 14px; + font-weight: 400; + line-height: 150%; + [purpose='line-break']:not(:first-of-type)::before { + content: '\a'; + } + &:not(.nohighlight) { + .hljs-keyword { // SQL keywords (SELECT, FROM, WHERE, IN, etc.) + color: #AE6DDF; + } + .hljs-attr { // For table and column names + .hljs-keyword { + color: #FFF; + } + .hljs-string { // For words wrapped in quotation marks + color: #FFF; + } + color: #FFF; + background-color: #AE6DDF; + border-radius: 3px; + white-space: pre; + vertical-align: baseline; + span { + padding: 0; + } + } + .hljs-number { + color: #f5871f; + } + .hljs-string { // For words wrapped in quotation marks + color: #4fd061; + .hljs-keyword { + color: #4fd061; + } + } + } + + background-color: @ui-off-white; + border: none; + padding: 0; + } + } + + @media (max-width: 991px) { + [purpose='page-container'] { + padding: 32px; + } + [purpose='policy-details'] { + padding-right: 0px; + max-width: 100%; + } + [purpose='right-sidebar'] { + width: 100%; + margin-left: 0px; + } + [purpose='breadcrumbs-and-search'] { + margin-bottom: 32px; + } + pre { + code { + &.has-linebreaks { + white-space: pre; + } + } + } + } + + @media (max-width: 768px) { + [purpose='breadcrumbs-and-search'] { + max-width: 1072px; + font-size: 14px; + [purpose='breadcrumbs'] { + margin-bottom: 24px; + } + [purpose='search'] { + width: 100%; + .DocSearch-Button { + width: 100%; + } + } + } + } + + @media (max-width: 575px) { + [purpose='page-container'] { + padding: 32px 24px; + } + + } + + +} diff --git a/website/assets/styles/pages/policy-library.less b/website/assets/styles/pages/policy-library.less new file mode 100644 index 000000000000..45a6f590ed28 --- /dev/null +++ b/website/assets/styles/pages/policy-library.less @@ -0,0 +1,339 @@ +#policy-library { + // background-color: #FFF; + a { + color: #515774; + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + text-decoration-line: underline; + } + + [purpose='policy-search'] { + + // Note: We're using classes here to override the default Docsearch styles; + button { + width: 100%; + cursor: text; + margin: 0; + } + .DocSearch-Button { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + border: 1px solid @core-fleet-black-25; + background-color: #FFF; + padding: 8px 15px; + height: 36px; + margin: 0; + width: 221px; + } + .DocSearch-Button:hover { + box-shadow: none; + border: 1px solid @core-fleet-black-25; + color: @core-fleet-black-50; + } + .DocSearch-Search-Icon { + margin-left: 0px; + margin-right: 8px; + height: 16px; + width: 16px; + color: @core-fleet-black-50; + stroke-width: 3px; + } + .DocSearch-Button-Keys { + display: none; + } + .input-group:focus-within { + border: 1px solid @core-vibrant-blue; + } + .DocSearch-Button-Placeholder { + font-size: 16px; + line-height: 16px; + font-weight: 400; + padding-left: 0px; + } + [purpose='disabled-search'] { + input { + padding-top: 6px; + padding-bottom: 6px; + border: none; + } &::placeholder { + font-size: 16px; + line-height: 24px; + color: #8B8FA2; + } + .input-group { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + border: 1px solid @core-fleet-black-25; + background: #FFF; + } + .input-group:focus-within { + border: 1px solid @core-vibrant-blue; + } + .form-control { + border-radius: 6px; + padding: 6px; + height: 36px; + margin: 0; + width: 212px; + } + .docsearch-input:focus-visible { + outline: none; + } + .ds-input:focus { + outline: rgba(0, 0, 0, 0); + } + .input-group-text { + color: @core-fleet-black-50; + } + .form-control { + height: 36px; + padding: 0px; + font-size: 16px; + } &:focus { + border: none; + } + } + img { + height: 16px; + margin-right: 8px; + } + background: #FFF; + &::placeholder { + font-size: 16px; + color: @core-fleet-black-50; + } + } + [purpose='page-container'] { + padding: 64px; + } + [purpose='page-content'] { + margin-left: auto; + margin-right: auto; + max-width: 1072px; + } + [purpose='search-and-headline'] { + margin-bottom: 64px; + } + [purpose='page-headline'] { + max-width: 662px; + margin-right: 16px; + h2 { + color: #192147; + font-size: 32px; + font-weight: 800; + line-height: 120%; + margin-bottom: 16px; + } + } + [purpose='platform-filters'] { + border-bottom: 1px solid #E2E4EA; + margin-bottom: 48px; + [purpose='platform-filter'] { + width: 33%; + display: flex; + padding: 16px 40px; + justify-content: center; + align-items: center; + cursor: pointer; + img { + height: 20px; + margin-right: 10px; + } + p { + color: #192147; + text-align: center; + margin-bottom: 0px; + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ + white-space: nowrap; + } + &.selected { + border-bottom: 2px solid var(--text-text-brand, #192147); + p { + font-weight: 700; + } + } + } + + + + } + [purpose='policy'] { + padding: 32px 0px; + border-bottom: 1px solid #E2E4EA; + } + [purpose='read-more-link'] { + // margin-bottom: 16px; + span { + font-size: 14px; + color: ; + font-weight: 600; + } + } + [purpose='policy-link'] { + color: unset; + text-decoration: none; + + &:hover { + text-decoration: none; + } + } + [purpose='policy-name-and-description'] { + max-width: 663px; + } + [purpose='policy-name'] { + a { + text-decoration: none; + color: #192147; + font-size: 20px; + font-weight: 800; + line-height: 24px; + margin-bottom: 8px; + } + } + [purpose='policy-description'] { + margin-top: 16px; + p { + color: #515774; + + /* Body SM (FKA Card text) */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 21px; /* 150% */ + } + // max-width: 663px; + } + [purpose='contributor-profile-picture'] { + width: 24px; + height: 24px; + margin-right: 6px; + border-radius: 50%; + } + [purpose='contributor-profile-name'] { + font-size: 12px; + line-height: 18px; + } + + [purpose='requires-mdm-badge'] { + // text-transform: uppercase; + display: flex; + padding: 4px 6px; + justify-content: center; + align-items: center; + border-radius: 4px; + border: 1px solid #E2E4EA; + background: #F9FAFC; + color: #192147; + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 11px; + height: min-content; + white-space: nowrap; + // margin-bottom: 16px; + width: fit-content; + } + [purpose='premium-badge'] { + // margin-bottom: 16px; + margin-right: 16px; + border-radius: 4px; + border: 1px solid #0587FF; + background: #0587FF; + display: flex; + padding: 4px 6px; + justify-content: center; + align-items: center; + text-transform: uppercase; + font-weight: 700; + font-size: 10px; + line-height: 10px; + color: #FFF; + text-decoration: none; + width: fit-content; + } + [purpose='query-tag'] { + font-size: 12px; + font-weight: 700; + padding: 2px 8px; + border-radius: 20px; + background-color: #E2E4EA; + color: #515774; + } + + [purpose='query-list-empty-state'] { + margin-top: 40px; + margin-right: 30px; + margin-left: 30px; + a { + font-size: 16px; + } + } + + // .input-group { + // &.search { + // width: 221px; + // background: #FFF; + // height: 40px; + // } + // } + + // .input-group-text { + // color: #8b8fa2; + // height: 40px; + // border-color: #c5c7d1; + // border-top-left-radius: 8px; + // border-bottom-left-radius: 8px; + // } + + // .form-control { + // height: 40px; + // font-size: 16px; + // border-color: #c5c7d1; + // border-top-right-radius: 8px; + // border-bottom-right-radius: 8px; + // &:focus { + // border: 1px solid #c5c7d1; + // } + // } + + + @media (max-width: 991px) { + [purpose='page-container'] { + padding: 64px 32px; + } + [purpose='policy-name-and-description'] { + max-width: unset; + } + [purpose='read-more-link'] { + margin-bottom: 16px; + } + } + + @media (max-width: 768px) { + [purpose='policy-search'] { + margin-top: 32px; + width: 100%; + .input-group { + width: 100%; + } + } + + } + + @media (max-width: 575px) { + + } +} diff --git a/website/assets/styles/pages/query-detail.less b/website/assets/styles/pages/query-detail.less index 91ca8584fed2..3199763d719c 100644 --- a/website/assets/styles/pages/query-detail.less +++ b/website/assets/styles/pages/query-detail.less @@ -1,5 +1,5 @@ #query-detail { - + // background-color: #FFF; h3 { padding-bottom: 24px; padding-top: 32px; @@ -139,7 +139,7 @@ } } - [purpose='policy-name'] { + [purpose='query-name'] { color: #192147; font-size: 32px; font-style: normal; @@ -147,21 +147,20 @@ line-height: 150%; margin-bottom: 0px; } - [purpose='policy-description'] { + [purpose='query-description'] { color: #515774; font-size: 16px; font-style: normal; font-weight: 400; line-height: 150%; margin-bottom: 0px; - padding: 16px 0px 32px 0px; + padding: 48px 0px 32px 0px; } - [purpose='policy-attribution'] { + [purpose='query-attribution'] { display: flex; flex-direction: row; align-items: center; - padding-top: 8px; - padding-bottom: 16px; + padding-top: 16px; [purpose='contributor-profile-picture'] { width: 28.8px; height: 28.8px; @@ -172,7 +171,7 @@ font-size: 14px; line-height: 150%; } - [purpose='policy-link'] { + [purpose='query-link'] { color: unset; text-decoration: none; @@ -183,12 +182,17 @@ } - [purpose='policy-details'] { + [purpose='query-details'] { padding-right: 64px; max-width: 800px; width: 100%; + a { + color: #515774; + text-decoration: underline #C5C7D1; + text-underline-offset: 3px; + } } - [purpose='policy-check'] { + [purpose='query-check'] { padding-bottom: 24px; } @@ -199,6 +203,7 @@ transition-property: transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 500ms; + a:not([purpose='edit-button']) { margin-bottom: 8px; display: block; @@ -210,8 +215,34 @@ } } [purpose='docs-links'] { - a { - display: block; + [purpose='sidebar-link'] { + margin-bottom: 12px; + display: flex; + flex-direction: row; + align-items: center; + color: #515774; + img { + height: 16px; + width: 16px; + display: inline; + margin-right: 12px; + } + &:hover { + text-decoration: none; + color: @core-fleet-black; + img { + filter: brightness(0.5); + } + } + } + [purpose='edit-link'] { + padding-top: 24px; + padding-bottom: 16px; + margin-top: 24px; + border-top: 1px solid #E2E4EA; + a { + margin-bottom: 0px; + } } } [purpose='social-share-buttons'] { @@ -226,7 +257,7 @@ width: 20px; } } - [purpose='policy-platform'] { + [purpose='query-platform'] { border-bottom: 1px solid #E2E4EA; padding-bottom: 24px; padding-top: 16px; @@ -304,7 +335,7 @@ border: 1px solid #E2E4EA; background: #F9FAFC; border-radius: 4px; - margin-top: 16px; + margin-top: 40px; margin-bottom: 24px; code { color: #515774; @@ -312,6 +343,7 @@ white-space: break-spaces; } &.no-linebreaks { + word-break: break-word; white-space: normal; } font-family: 'Source Code Pro'; @@ -359,7 +391,7 @@ [purpose='page-container'] { padding: 32px; } - [purpose='policy-details'] { + [purpose='query-details'] { padding-right: 0px; max-width: 100%; } diff --git a/website/assets/styles/pages/query-library.less b/website/assets/styles/pages/query-library.less index 924a4dc887ad..e48a1cf6818e 100644 --- a/website/assets/styles/pages/query-library.less +++ b/website/assets/styles/pages/query-library.less @@ -1,5 +1,5 @@ #query-library { - + // background-color: #FFF; a { color: #515774; font-family: Inter; @@ -122,8 +122,21 @@ margin-bottom: 64px; } [purpose='page-headline'] { - max-width: 662px; + max-width: 664px; margin-right: 16px; + h2 { + color: #192147; + font-size: 32px; + font-weight: 800; + line-height: 120%; + margin-bottom: 16px; + } + p { + color: #515774; + font-size: 15px; + font-weight: 400; + line-height: 24px; /* 160% */ + } } [purpose='platform-filters'] { border-bottom: 1px solid #E2E4EA; @@ -322,6 +335,9 @@ .input-group { width: 100%; } + .DocSearch-Button { + width: 100%; + } } } diff --git a/website/assets/styles/pages/vital-details.less b/website/assets/styles/pages/vital-details.less new file mode 100644 index 000000000000..039f3bb8bf7d --- /dev/null +++ b/website/assets/styles/pages/vital-details.less @@ -0,0 +1,570 @@ +#vital-details { + // background-color: #FFF; + h3 { + padding-bottom: 24px; + padding-top: 32px; + color: #192147; + font-size: 24px; + font-weight: 800; + line-height: 120%; + margin-bottom: 0px; + } + + p { + color: #515774; + font-size: 16px; + font-weight: 400; + line-height: 150%; + margin-bottom: 0px; + } + + + [purpose='page-container'] { + padding: 64px 64px 32px 64px; + } + [purpose='page-content'] { + margin-left: auto; + margin-right: auto; + max-width: 1072px; + } + [purpose='search-and-headline'] { + margin-bottom: 48px; + } + + + [purpose='vitals-search'] { + // Note: We're using classes here to override the default Docsearch styles; + button { + width: 100%; + cursor: text; + margin: 0; + } + .DocSearch-Button { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + border: 1px solid @core-fleet-black-25; + background-color: #FFF; + padding: 8px 15px; + height: 36px; + margin: 0; + width: 221px; + } + .DocSearch-Button:hover { + box-shadow: none; + border: 1px solid @core-fleet-black-25; + color: @core-fleet-black-50; + } + .DocSearch-Search-Icon { + margin-left: 0px; + margin-right: 8px; + height: 16px; + width: 16px; + color: @core-fleet-black-50; + stroke-width: 3px; + } + .DocSearch-Button-Keys { + display: none; + } + .input-group:focus-within { + border: 1px solid @core-vibrant-blue; + } + .DocSearch-Button-Placeholder { + font-size: 16px; + line-height: 16px; + font-weight: 400; + padding-left: 0px; + } + [purpose='disabled-search'] { + input { + padding-top: 6px; + padding-bottom: 6px; + border: none; + } + &::placeholder { + font-size: 16px; + line-height: 24px; + color: #8B8FA2; + } + .input-group { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + border: 1px solid @core-fleet-black-25; + background: #FFF; + } + .input-group:focus-within { + border: 1px solid @core-vibrant-blue; + } + .form-control { + border-radius: 6px; + padding: 6px; + height: 36px; + margin: 0; + width: 212px; + } + .docsearch-input:focus-visible { + outline: none; + } + .ds-input:focus { + outline: rgba(0, 0, 0, 0); + } + .input-group-text { + color: @core-fleet-black-50; + } + .form-control { + height: 36px; + padding: 0px; + font-size: 16px; + } &:focus { + border: none; + } + } + img { + height: 16px; + margin-right: 8px; + } + background: #FFF; + &::placeholder { + font-size: 16px; + color: @core-fleet-black-50; + } + } + + [purpose='table-of-contents'] { + margin-right: 16px; + [purpose='vital-link'] { + color: #515774; + font-family: Inter; + font-size: 14px; + line-height: 150%; + width: 240px; + // text-wrap: nowrap; + padding: 4px 8px; + &:hover { + text-decoration: none; + border-radius: 4px; + background: #F9FAFC; + } + &.active { + border-radius: 4px; + background: #F9FAFC; + } + } + } + + [purpose='mobile-custom-select'] { + display: flex; + cursor: pointer; + height: 48px; + padding: 0px 12px; + justify-content: space-between; + align-items: center; + align-self: stretch; + border-radius: 6px; + border: 1px solid var(--border-border-primary, #E2E4EA); + background: var(--surface-surface-primary, #FFF); + width: 100%; + margin-bottom: 16px; + margin-top: 16px; + p { + color: var(--text-text-brand, #192147); + + /* Body MD (FKA p) */ + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ + } + img { + height: 16px; + } + &:hover { + border-radius: 6px; + border: 1px solid var(--border-border-primary, #E2E4EA); + background: var(--surface-surface-primary, #FFF); + box-shadow: 0px 0px 0px 2px rgba(25, 33, 71, 0.05); + } + } + + + + + + + + + [purpose='page-headline'] { + max-width: 662px; + margin-right: 16px; + h2 { + margin-bottom: 24px; + color: #192147; + font-size: 32px; + font-weight: 800; + line-height: 120%; + } + p { + margin-bottom: 24px; + color: #515774; + font-size: 15px; + line-height: 150%; + } + } + [purpose='platform-filters'] { + border-bottom: 1px solid #E2E4EA; + margin-bottom: 32px; + padding-top: 8px; + [purpose='platform-filter'] { + width: 25%; + display: flex; + padding: 16px 40px 24px 40px; + justify-content: center; + align-items: center; + cursor: pointer; + img { + height: 20px; + margin-right: 10px; + } + p { + color: #192147; + text-align: center; + margin-bottom: 0px; + font-family: Inter; + font-size: 16px; + font-weight: 400; + line-height: 24px; /* 150% */ + white-space: nowrap; + } + &.selected { + border-bottom: 2px solid var(--text-text-brand, #192147); + padding: 16px 40px 22px 40px; + p { + font-weight: 700; + } + } + } + + + + } + [purpose='vital-details-and-sidebar'] { + padding-top: 32px; + + } + + [purpose='vital-name'] { + color: #192147; + font-size: 32px; + font-style: normal; + font-weight: 800; + line-height: 150%; + margin-bottom: 24px; + margin-top: 16px; + } + [purpose='vital-description'] { + color: #515774; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 150%; + margin-bottom: 0px; + padding-bottom: 16px; + } + [purpose='vital-attribution'] { + display: flex; + flex-direction: row; + align-items: center; + padding-top: 8px; + padding-bottom: 16px; + [purpose='contributor-profile-picture'] { + width: 28.8px; + height: 28.8px; + margin-right: 8px; + border-radius: 50%; + } + [purpose='contributor-profile-name'] { + font-size: 14px; + line-height: 150%; + } + [purpose='vital-link'] { + color: unset; + text-decoration: none; + + &:hover { + text-decoration: none; + } + } + + + } + [purpose='vital-details'] { + padding-left: 32px; + max-width: 800px; + width: 100%; + } + [purpose='vital-check'] { + padding-bottom: 16px; + [purpose='codeblock'] { + padding: 0; + position: relative; + [purpose='copy-button'] { + position: absolute; + top: 11px; + right: 10px; + border-radius: 8px; + height: 32px; + width: 32px; + background: url('/images/icon-copy-16x16@2x.png'); + background-color: #F9FAFC; + background-size: 14px 14px; + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + &:hover { + background-color: #F2F2F5; + } + &.copied { + background: url('/images/icon-copy-clicked-checkmark-32x32@2x.png'); + background-size: 32px 32px; + background-repeat: no-repeat; + background-position: center; + } + } + } + + pre { + width: 100%; + max-width: 100%; + padding: 16px 44px 16px 24px; + border: 1px solid #E2E4EA; + background: #F9FAFC; + border-radius: 4px; + margin-top: 16px; + code { + color: #515774; + &.has-linebreaks { + white-space: break-spaces; + } + &.no-linebreaks { + word-break: break-word; + white-space: normal; + } + font-family: 'Source Code Pro'; + font-size: 14px; + font-weight: 400; + line-height: 150%; + .hljs-keyword { // SQL keywords (SELECT, FROM, WHERE, IN, etc.) + color: #AE6DDF; + } + [purpose='line-break']:not(:first-of-type)::before { + content: '\a'; + } + .hljs-attr { // For table and column names + .hljs-keyword { + color: #FFF; + } + .hljs-string { // For words wrapped in quotation marks + color: #FFF; + } + color: #FFF; + background-color: #AE6DDF; + border-radius: 3px; + white-space: pre; + vertical-align: baseline; + span { + padding: 0; + } + } + .hljs-number { + color: #f5871f; + } + .hljs-string { // For words wrapped in quotation marks + color: #4fd061; + .hljs-keyword { + color: #4fd061; + } + } + background-color: @ui-off-white; + border: none; + padding: 0; + } + } + + } + + [purpose='discovery-table-note'] { + display: flex; + padding: 16px 24px; + align-items: center; + gap: 8px; + align-self: stretch; + border-radius: 8px; + border: 1px solid #B4B2FE; + background: #F7F7FC; + margin-bottom: 40px; + img { + width: 16px; + height: 16px; + } + p { + margin-bottom: 0px; + color: #515774; + + /* Body SM (FKA Card text) */ + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 150%; + } + code { + color: #515774; + font-family: 'Source Code Pro'; + font-size: 14px; + line-height: 16px; /* 114.286% */ + border-radius: 2px; + background: #F1F0FF; + padding: 2px 1px; + } + a { + color: #515774; + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 150%; /* 150% */ + text-decoration: underline #C5C7D1; + text-underline-offset: 3px; + } + } + [purpose='edit-button'] { + img { + width: 12px; + height: 12px; + display: inline; + margin-left: 8px; + } + color: var(--UI-Fleet-Black-75, #515774); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 21px; /* 150% */ + text-decoration: underline #C5C7D1; + text-underline-offset: 4px; + } + [purpose='mobile-table-of-contents-header'] { + color: #192147; + font-size: 14px; + font-weight: 800; + line-height: 16.8px; + } + .modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + -webkit-transform: translate(-25%, 0%); + transform: translate(-25%, 0%); + } + .modal.show .modal-dialog { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + } + [parasails-component='modal'] { + + [purpose='modal-dialog'] { + margin-left: 16px; + [purpose='modal-content'] { + display: flex; + height: calc(~'100vh - 72px'); + margin-top: 0px; + max-width: 366px; + padding: 48px 24px 24px 24px; + flex-direction: column; + align-items: flex-start; + // gap: 24px; + flex-shrink: 0; + [purpose='table-of-contents'] { + margin-top: 16px; + max-height: 100%; + width: 100%; + overflow-y: scroll; + [purpose='vital-link'] { + color: #515774; + font-family: Inter; + font-size: 14px; + line-height: 150%; + width: 100%; + // text-wrap: nowrap; + padding: 4px 8px; + &:hover { + text-decoration: none; + border-radius: 4px; + background: #F9FAFC; + } + &.active { + border-radius: 4px; + background: #F9FAFC; + } + } + } + } + } + } + + @media (max-width: 991px) { + [purpose='page-container'] { + padding: 32px 32px 64px 32px; + } + + [purpose='breadcrumbs-and-search'] { + margin-bottom: 32px; + } + + } + + @media (max-width: 768px) { + [purpose='breadcrumbs-and-search'] { + max-width: 1072px; + font-size: 14px; + [purpose='breadcrumbs'] { + margin-bottom: 24px; + } + } + [purpose='vital-details'] { + padding-left: 0px; + max-width: 100%; + } + [purpose='vital-details-and-sidebar'] { + padding-top: 0px; + } + [purpose='search-and-headline'] { + margin-bottom: 32px; + } + [purpose='vitals-search'] { + width: 100%; + .DocSearch-Button { + width: 100%; + } + [purpose='disabled-search'] { + width: 100%; + } + } + } + + @media (max-width: 575px) { + [purpose='page-container'] { + padding: 32px 24px 64px 24px; + } + [purpose='platform-filters'] { + [purpose='platform-filter'] { + padding: 16px 24px 24px 24px; + &.selected { + padding: 16px 24px 22px 24px; + } + } + } + } + + +} diff --git a/website/config/policies.js b/website/config/policies.js index ed7b3b0cba7f..872a3e1a0410 100644 --- a/website/config/policies.js +++ b/website/config/policies.js @@ -26,7 +26,10 @@ module.exports.policies = { 'legal/view-privacy': true, 'deliver-contact-form-message': true, 'view-query-detail': true, + 'view-policy-details': true, 'view-query-library': true, + 'view-policy-library': true, + 'view-vital-details': true, 'docs/*': true, 'handbook/*': true, 'download-sitemap': true, diff --git a/website/config/routes.js b/website/config/routes.js index b961c0068355..c603cb6adea6 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -60,8 +60,8 @@ module.exports.routes = { action: 'view-query-library', locals: { currentSection: 'documentation', - pageTitleForMeta: 'Controls and policies', - pageDescriptionForMeta: 'A growing collection of useful controls and policies for organizations deploying Fleet and osquery.' + pageTitleForMeta: 'Queries', + pageDescriptionForMeta: 'A growing collection of optional queries you can run anytime to ask questions about your devices using Fleet and osquery.' } }, @@ -69,7 +69,28 @@ module.exports.routes = { action: 'view-query-detail',// Meta title and description set in view action locals: { currentSection: 'documentation', - // Note: this page's meta title and description are set in the page's view action + } + }, + + 'GET /vitals/:slug': { + action: 'view-vital-details',// Meta title and description set in view action + locals: { + currentSection: 'documentation', + } + }, + + 'GET /policies': { + action: 'view-policy-library', + locals: { + currentSection: 'documentation', + pageTitleForMeta: 'Policies', + pageDescriptionForMeta: 'A growing collection of useful controls and policies for organizations deploying Fleet and osquery.' + } + }, + 'GET /policies/:slug': { + action: 'view-policy-details',// Meta title and description set in view action + locals: { + currentSection: 'documentation', } }, @@ -761,6 +782,7 @@ module.exports.routes = { 'GET /unsubscribe': (req,res)=> { let originalQueryString = req.url.match(/\?(.+)$/) ? req.url.match(/\?(.+)$/)[1] : ''; return res.redirect(301, sails.config.custom.baseUrl+'/api/v1/unsubscribe-from-marketing-emails?'+originalQueryString);}, 'GET /unsubscribe-from-newsletter': (req,res)=> { let originalQueryString = req.url.match(/\?(.+)$/) ? req.url.match(/\?(.+)$/)[1] : ''; return res.redirect(301, sails.config.custom.baseUrl+'/api/v1/unsubscribe-from-all-newsletters?'+originalQueryString);}, 'GET /tables': '/tables/account_policy_data', + 'GET /vitals': '/vitals/battery', 'GET /imagine/launch-party': 'https://www.eventbrite.com/e/601763519887', 'GET /blackhat2023': 'https://github.com/fleetdm/fleet/tree/main/tools/blackhat-mdm', // Assets from @marcosd4h & @zwass Black Hat 2023 talk 'GET /fleetctl-preview': '/try-fleet', diff --git a/website/scripts/build-static-content.js b/website/scripts/build-static-content.js index a40e9a78aca7..d95a09924c3f 100644 --- a/website/scripts/build-static-content.js +++ b/website/scripts/build-static-content.js @@ -36,11 +36,11 @@ module.exports = { }//fi await sails.helpers.flow.simultaneously([ - async()=>{// Parse query library from YAML and prepare to bake them into the Sails app's configuration. - let RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO = 'docs/01-Using-Fleet/standard-query-library/standard-query-library.yml'; + async()=>{// Parse queries.yml library (Informational queries and host vital queries) from YAML and prepare to bake them into the Sails app's configuration. + let RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO = 'docs/queries.yml'; let yaml = await sails.helpers.fs.read(path.join(topLvlRepoPath, RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO)).intercept('doesNotExist', (err)=>new Error(`Could not find standard query library YAML file at "${RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO}". Was it accidentally moved? Raw error: `+err.message)); - let queriesWithProblematicResolutions = []; + let queriesWithProblematicDiscovery = []; let queriesWithProblematicContributors = []; let queriesWithProblematicTags = []; let queries = YAML.parseAllDocuments(yaml).map((yamlDocument)=>{ @@ -48,6 +48,137 @@ module.exports = { query.kind = yamlDocument.toJSON().kind; query.slug = _.kebabCase(query.name);// « unique slug to use for routing to this query's detail page // Remove the platform name from query names. This allows us to keep queries at their existing URLs while hiding them in the UI. + query.name = query.name.replace(/\s\(macOS\)|\(Windows\)|\(Linux\)|\(Chrome\)|\(macOS\/Linux\)$/, ''); + if ((query.discovery !== undefined && !_.isString(query.discovery)) || (query.kind !== 'built-in' && _.isString(query.discovery))) { + queriesWithProblematicDiscovery.push(query); + } + + if (query.tags) { + if(!_.isString(query.tags)) { + queriesWithProblematicTags.push(query); + } else { + // Splitting tags into an array to format them. + let tagsToFormat = query.tags.split(','); + let formattedTags = []; + for (let tag of tagsToFormat) { + if(tag !== '') {// « Ignoring any blank tags caused by trailing commas in the YAML. + // Removing any extra whitespace from tags and changing them to be in lower case. + formattedTags.push(_.trim(tag.toLowerCase())); + } + } + // Removing any duplicate tags. + query.tags = _.uniq(formattedTags); + } + } else { + query.tags = []; // « if there are no tags, we set query.tags to an empty array so it is always the same data type. + } + + // GitHub usernames may only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen. + if(query.kind !== 'built-in') {// Note: queries with kind: built-in do not have a contributors value. + if (!query.contributors || (query.contributors !== undefined && !_.isString(query.contributors)) || query.contributors.split(',').some((contributor) => contributor.match('^[^A-za-z0-9].*|[^A-Za-z0-9-]|.*[^A-za-z0-9]$'))) { + queriesWithProblematicContributors.push(query); + } + } + + return query; + }); + // Report any errors that were detected along the way in one fell swoop to avoid endless resubmitting of PRs. + if (queriesWithProblematicDiscovery.length >= 1) { + throw new Error('Failed parsing YAML for query library: The "discovery" of a query should either be absent (undefined) or a single string (not a list of strings). And "discovery" should only be present when a query\'s kind is "built-in". But one or more queries have an invalid "discovery": ' + _.pluck(queriesWithProblematicDiscovery, 'slug').sort()); + }//• + if (queriesWithProblematicTags.length >= 1) { + throw new Error('Failed parsing YAML for query library: The "tags" of a query should either be absent (undefined) or a single string (not a list of strings). "tags" should be be be seperated by a comma. But one or more queries have invalid "tags": ' + _.pluck(queriesWithProblematicTags, 'slug').sort()); + } + // Assert uniqueness of slugs. + if (queries.length !== _.uniq(_.pluck(queries, 'slug')).length) { + throw new Error('Failed parsing YAML for query library: Queries as currently named would result in colliding (duplicate) slugs. To resolve, rename the queries whose names are too similar. Note the duplicates: ' + _.pluck(queries, 'slug').sort()); + }//• + // Report any errors that were detected along the way in one fell swoop to avoid endless resubmitting of PRs. + if (queriesWithProblematicContributors.length >= 1) { + throw new Error('Failed parsing YAML for query library: The "contributors" of a query should be a single string of valid GitHub user names (e.g. "zwass", or "zwass,noahtalerman,mikermcneil"). But one or more queries have an invalid "contributors" value: ' + _.pluck(queriesWithProblematicContributors, 'slug').sort()); + }//• + + // Get a distinct list of all GitHub usernames from all of our queries. + // Map all queries to build a list of unique contributor names then build a dictionary of user profile information from the GitHub Users API + const githubUsernames = queries.reduce((list, query) => { + if (!queriesWithProblematicContributors.find((element) => element.slug === query.slug) && query.kind !== 'built-in') { + list = _.union(list, query.contributors.split(',')); + } + return list; + }, []); + + let githubDataByUsername = {}; + + // If a GitHub access token was provided, validate all users listed in the standard query library YAML. + if(githubAccessToken) { + await sails.helpers.flow.simultaneouslyForEach(githubUsernames, async(username)=>{ + githubDataByUsername[username] = await sails.helpers.http.get.with({ + url: 'https://api.github.com/users/' + encodeURIComponent(username), + headers: baseHeadersForGithubRequests, + }).intercept((err)=>{ + return new Error(`When validating users in standard-query-library.yml, an error when a request was sent to GitHub get the information about a user (username: ${username}). Error: ${err.stack}`); + }); + });//∞ + // Now expand queries with relevant profile data for the contributors. + for (let query of queries) { + // Skip built-in queries. + if(query.kind === 'built-in'){ + continue; + } + let usernames = query.contributors.split(','); + let contributorProfiles = []; + for (let username of usernames) { + contributorProfiles.push({ + name: githubDataByUsername[username].name, + handle: githubDataByUsername[username].login, + avatarUrl: githubDataByUsername[username].avatar_url, + htmlUrl: githubDataByUsername[username].html_url, + }); + } + query.contributors = contributorProfiles; + } + } else {// Otherwise, use the Github username as contributor's names and handles and use fake profile pictures. + for (let query of queries) { + // Skip host vital queries. + if(query.kind === 'built-in'){ + continue; + } + let usernames = query.contributors.split(','); + let contributorProfiles = []; + for (let username of usernames) { + contributorProfiles.push({ + name: username, + handle: username, + avatarUrl: 'https://placekitten.com/200/200', + htmlUrl: 'https://github.com/'+encodeURIComponent(username), + }); + } + query.contributors = contributorProfiles; + } + }//fi + + // Attach to what will become configuration for the Sails app. + builtStaticContent.queries = queries; + builtStaticContent.queryLibraryYmlRepoPath = RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO; + }, + async()=>{ + // Parse query library from YAML and prepare to bake the policies into the Sails app's configuration. + // Note: we will only be using the policies from the standard query library YAML. + let RELATIVE_PATH_TO_POLICY_LIBRARY_YML_IN_FLEET_REPO = 'docs/01-Using-Fleet/standard-query-library/standard-query-library.yml'; + // let RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO = 'docs/queries.yml'; + let yaml = await sails.helpers.fs.read(path.join(topLvlRepoPath, RELATIVE_PATH_TO_POLICY_LIBRARY_YML_IN_FLEET_REPO)).intercept('doesNotExist', (err)=>new Error(`Could not find standard query library YAML file at "${RELATIVE_PATH_TO_POLICY_LIBRARY_YML_IN_FLEET_REPO}". Was it accidentally moved? Raw error: `+err.message)); + let queriesWithProblematicResolutions = []; + let queriesWithProblematicContributors = []; + let queriesWithProblematicTags = []; + let policies = YAML.parseAllDocuments(yaml).map((yamlDocument)=>{ + let query = yamlDocument.toJSON().spec; + query.kind = yamlDocument.toJSON().kind; + // If this query is not a policy, we will skip it and return undefined. + if(query.kind === 'query'){ + return undefined; + } + query.slug = _.kebabCase(query.name);// « unique slug to use for routing to this query's detail page + // Remove the platform name from query names. This allows us to keep queries at their existing URLs while hiding them in the UI. query.name = query.name.replace(/\s\(macOS\)|\(Windows\)|\(Linux\)$/, ''); if ((query.resolution !== undefined && !_.isString(query.resolution)) || (query.kind !== 'policy' && _.isString(query.resolution))) { // console.log(typeof query.resolution); @@ -89,6 +220,9 @@ module.exports = { } return query; + }).filter((query)=>{ + // Remove all queries we previously returned undefined for. Otherwise, they will cause the validation to always fail. + return query !== undefined; }); // Report any errors that were detected along the way in one fell swoop to avoid endless resubmitting of PRs. if (queriesWithProblematicResolutions.length >= 1) { @@ -98,8 +232,8 @@ module.exports = { throw new Error('Failed parsing YAML for query library: The "tags" of a query should either be absent (undefined) or a single string (not a list of strings). "tags" should be be be seperated by a comma. But one or more queries have invalid "tags": ' + _.pluck(queriesWithProblematicTags, 'slug').sort()); } // Assert uniqueness of slugs. - if (queries.length !== _.uniq(_.pluck(queries, 'slug')).length) { - throw new Error('Failed parsing YAML for query library: Queries as currently named would result in colliding (duplicate) slugs. To resolve, rename the queries whose names are too similar. Note the duplicates: ' + _.pluck(queries, 'slug').sort()); + if (policies.length !== _.uniq(_.pluck(policies, 'slug')).length) { + throw new Error('Failed parsing YAML for query library: Queries as currently named would result in colliding (duplicate) slugs. To resolve, rename the queries whose names are too similar. Note the duplicates: ' + _.pluck(policies, 'slug').sort()); }//• // Report any errors that were detected along the way in one fell swoop to avoid endless resubmitting of PRs. if (queriesWithProblematicContributors.length >= 1) { @@ -108,7 +242,7 @@ module.exports = { // Get a distinct list of all GitHub usernames from all of our queries. // Map all queries to build a list of unique contributor names then build a dictionary of user profile information from the GitHub Users API - const githubUsernames = queries.reduce((list, query) => { + const githubUsernames = policies.reduce((list, query) => { if (!queriesWithProblematicContributors.find((element) => element.slug === query.slug)) { list = _.union(list, query.contributors.split(',')); } @@ -128,7 +262,7 @@ module.exports = { }); });//∞ // Now expand queries with relevant profile data for the contributors. - for (let query of queries) { + for (let query of policies) { let usernames = query.contributors.split(','); let contributorProfiles = []; for (let username of usernames) { @@ -142,7 +276,7 @@ module.exports = { query.contributors = contributorProfiles; } } else {// Otherwise, use the Github username as contributor's names and handles and use fake profile pictures. - for (let query of queries) { + for (let query of policies) { let usernames = query.contributors.split(','); let contributorProfiles = []; for (let username of usernames) { @@ -158,8 +292,8 @@ module.exports = { }//fi // Attach to what will become configuration for the Sails app. - builtStaticContent.queries = queries; - builtStaticContent.queryLibraryYmlRepoPath = RELATIVE_PATH_TO_QUERY_LIBRARY_YML_IN_FLEET_REPO; + builtStaticContent.policies = policies; + builtStaticContent.policyLibraryYmlRepoPath = RELATIVE_PATH_TO_POLICY_LIBRARY_YML_IN_FLEET_REPO; }, async()=>{// Parse markdown pages, compile & generate HTML files, and prepare to bake directory trees into the Sails app's configuration. let APP_PATH_TO_COMPILED_PAGE_PARTIALS = 'views/partials/built-from-markdown'; diff --git a/website/views/layouts/layout.ejs b/website/views/layouts/layout.ejs index 47f35ebc6371..24c1af65e0ee 100644 --- a/website/views/layouts/layout.ejs +++ b/website/views/layouts/layout.ejs @@ -167,10 +167,12 @@
Get started - Built-in checks - Raw data - App library - Tutorials & guides + Guides + Vitals + Queries + Policies + Software + Data tables API SUPPORT
@@ -231,10 +233,12 @@