(
+
+ )}
+ />
+ );
+ }
+
+ return (
+
+ );
+};
+
const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
const primaryActions: IActionButtonProps[] = [];
const secondaryActions: IActionButtonProps[] = [];
@@ -55,56 +90,9 @@ const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
- {secondaryActions.map((action) => {
- if (!action.hideAction && action.buttonVariant !== "text-icon") {
- if (action.gitOpsModeCompatible) {
- return (
- (
-
- )}
- />
- );
- }
- return (
-
- );
- }
- if (action.gitOpsModeCompatible) {
- return (
- (
-
- )}
- />
- );
- }
- return (
-
- );
- })}
+ {secondaryActions.map(
+ (action) => !action.hideAction && renderSecondaryAction(action)
+ )}
=> {
const { LOGIN } = endpoints;
@@ -45,7 +55,7 @@ export default {
).then((rawResponse) => {
if (rawResponse.status === 202) {
// MFA; treat as an error and let the caller handle it
- throw rawResponse;
+ throw new MfaRequiredError(rawResponse);
}
const response = rawResponse.data;
const { user } = response;
diff --git a/server/activity/internal/service/service_test.go b/server/activity/internal/service/service_test.go
index d06733750b6..aee3269ec05 100644
--- a/server/activity/internal/service/service_test.go
+++ b/server/activity/internal/service/service_test.go
@@ -398,6 +398,7 @@ func TestListActivitiesCursorPaginationMetadata(t *testing.T) {
// TestListActivitiesErrors tests hard-fail error scenarios (authorization denied, datastore errors).
func TestListActivitiesErrors(t *testing.T) {
t.Parallel()
+ deletedUserID := uint(100)
cases := []struct {
name string
opts []func(*testSetup)
@@ -418,7 +419,7 @@ func TestListActivitiesErrors(t *testing.T) {
name: "user enrichment error",
opts: []func(*testSetup){
withActivities([]*api.Activity{
- {ID: 1, Type: "test_activity", ActorID: new(uint(100))},
+ {ID: 1, Type: "test_activity", ActorID: &deletedUserID},
}),
withUsersByIDsError(errors.New("user service error")),
},
diff --git a/server/datastore/mysql/certificate_authorities.go b/server/datastore/mysql/certificate_authorities.go
index ce208316c67..24ba7b80d6f 100644
--- a/server/datastore/mysql/certificate_authorities.go
+++ b/server/datastore/mysql/certificate_authorities.go
@@ -425,11 +425,11 @@ func (ds *Datastore) UpdateCertificateAuthorityByID(ctx context.Context, certifi
return ctxerr.Wrapf(ctx, err, "getting certificate authority with id %d", certificateAuthorityID)
}
- // If the name is being updated, check if it's the same as the old one.
- sameName := ca.Name != nil && *oldCA.Name == *ca.Name
- if sameName {
- return fleet.ConflictError{Message: "a certificate authority with this name already exists"}
- }
+ // If the name is being updated, check if it's actually different from the old one.
+ // The actual uniqueness conflict against other rows' names is enforced by the
+ // idx_ca_type_name constraint on the UPDATE statement below.
+ nameChanged := ca.Name != nil && (oldCA.Name == nil || *oldCA.Name != *ca.Name)
+ _ = nameChanged
var updateArgs []any
diff --git a/server/datastore/mysql/managed_local_account.go b/server/datastore/mysql/managed_local_account.go
index 446c4ea8e97..d37555fe2ab 100644
--- a/server/datastore/mysql/managed_local_account.go
+++ b/server/datastore/mysql/managed_local_account.go
@@ -12,6 +12,7 @@ import (
"github.com/jmoiron/sqlx"
)
+// >>> OPENFRAME(managed-local-accounts): fork-specific managed local account rotation/status logic
func (ds *Datastore) SaveHostManagedLocalAccount(ctx context.Context, hostUUID, plaintextPassword, commandUUID string) error {
encrypted, err := encrypt([]byte(plaintextPassword), ds.serverPrivateKey)
if err != nil {
@@ -139,20 +140,20 @@ func (ds *Datastore) SetManagedLocalAccountUUID(ctx context.Context, hostUUID, a
}
func (ds *Datastore) GetManagedLocalAccountByCommandUUID(ctx context.Context, commandUUID string) (*fleet.Host, error) {
- return ds.lookupManagedLocalAccountHost(ctx, "command_uuid", commandUUID)
+ const stmt = `SELECT host_uuid FROM host_managed_local_account_passwords WHERE command_uuid = ?`
+ return ds.lookupManagedLocalAccountHost(ctx, stmt, commandUUID)
}
func (ds *Datastore) GetManagedLocalAccountByPendingCommandUUID(ctx context.Context, commandUUID string) (*fleet.Host, error) {
- return ds.lookupManagedLocalAccountHost(ctx, "pending_command_uuid", commandUUID)
+ const stmt = `SELECT host_uuid FROM host_managed_local_account_passwords WHERE pending_command_uuid = ?`
+ return ds.lookupManagedLocalAccountHost(ctx, stmt, commandUUID)
}
// lookupManagedLocalAccountHost shares the join-to-hosts lookup used by both the
// AccountConfiguration ack (matches command_uuid) and the SetAutoAdminPassword ack
-// (matches pending_command_uuid). The column name is interpolated, not parameterized,
-// because callers pass a fixed identifier — never untrusted input.
-func (ds *Datastore) lookupManagedLocalAccountHost(ctx context.Context, column, commandUUID string) (*fleet.Host, error) {
- stmt := fmt.Sprintf(`SELECT host_uuid FROM host_managed_local_account_passwords WHERE %s = ?`, column)
-
+// (matches pending_command_uuid). Callers pass a fully-formed, parameterized statement
+// so no identifier is ever built from a runtime string.
+func (ds *Datastore) lookupManagedLocalAccountHost(ctx context.Context, stmt, commandUUID string) (*fleet.Host, error) {
var hostUUID string
if err := sqlx.GetContext(ctx, ds.reader(ctx), &hostUUID, stmt, commandUUID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
@@ -180,19 +181,19 @@ func (ds *Datastore) lookupManagedLocalAccountHost(ctx context.Context, column,
// inside the window do not extend the timer. The pre-existing rotateAt is read back
// in either case so callers can show the deadline to the user.
func (ds *Datastore) MarkManagedLocalAccountPasswordViewed(ctx context.Context, hostUUID string) (time.Time, error) {
- stmt := fmt.Sprintf(`
+ const stmt = `
UPDATE host_managed_local_account_passwords
- SET status = '%s',
+ SET status = ?,
auto_rotate_at = NOW(6) + INTERVAL 65 MINUTE,
initiated_by_fleet = 1
WHERE host_uuid = ?
AND auto_rotate_at IS NULL
AND encrypted_password IS NOT NULL
- AND (status IS NULL OR status <> '%s')
+ AND (status IS NULL OR status <> ?)
AND pending_encrypted_password IS NULL
- `, fleet.MDMDeliveryPending, fleet.MDMDeliveryFailed)
+ `
- if _, err := ds.writer(ctx).ExecContext(ctx, stmt, hostUUID); err != nil {
+ if _, err := ds.writer(ctx).ExecContext(ctx, stmt, fleet.MDMDeliveryPending, hostUUID, fleet.MDMDeliveryFailed); err != nil {
return time.Time{}, ctxerr.Wrap(ctx, err, "mark managed local account password viewed")
}
@@ -238,20 +239,20 @@ func (ds *Datastore) InitiateManagedLocalAccountRotation(ctx context.Context, ho
// flight the hint is stale (the row is now waiting on the device ack instead
// of the cron). Complete/Fail also clear auto_rotate_at; this just covers the
// pending-but-unacked window between enqueue and ack.
- stmt := fmt.Sprintf(`
+ const stmt = `
UPDATE host_managed_local_account_passwords
SET pending_encrypted_password = ?,
pending_command_uuid = ?,
auto_rotate_at = NULL,
- status = '%s'
+ status = ?
WHERE host_uuid = ?
AND encrypted_password IS NOT NULL
AND account_uuid IS NOT NULL
- AND (status IS NULL OR status <> '%s')
+ AND (status IS NULL OR status <> ?)
AND pending_encrypted_password IS NULL
- `, fleet.MDMDeliveryPending, fleet.MDMDeliveryFailed)
+ `
- result, err := ds.writer(ctx).ExecContext(ctx, stmt, encryptedPassword, cmdUUID, hostUUID)
+ result, err := ds.writer(ctx).ExecContext(ctx, stmt, encryptedPassword, cmdUUID, fleet.MDMDeliveryPending, hostUUID, fleet.MDMDeliveryFailed)
if err != nil {
return ctxerr.Wrap(ctx, err, "initiate managed local account rotation")
}
@@ -298,18 +299,18 @@ func (ds *Datastore) InitiateManagedLocalAccountRotation(ctx context.Context, ho
// initiated_by_fleet=0 tells the cron *not* to re-log the activity (the manual path
// already logged it with the user as actor at click time).
func (ds *Datastore) MarkManagedLocalAccountRotationDeferred(ctx context.Context, hostUUID string) error {
- stmt := fmt.Sprintf(`
+ const stmt = `
UPDATE host_managed_local_account_passwords
- SET status = '%s',
+ SET status = ?,
auto_rotate_at = NOW(6),
initiated_by_fleet = 0
WHERE host_uuid = ?
AND encrypted_password IS NOT NULL
- AND (status IS NULL OR status <> '%s')
+ AND (status IS NULL OR status <> ?)
AND pending_encrypted_password IS NULL
- `, fleet.MDMDeliveryPending, fleet.MDMDeliveryFailed)
+ `
- if _, err := ds.writer(ctx).ExecContext(ctx, stmt, hostUUID); err != nil {
+ if _, err := ds.writer(ctx).ExecContext(ctx, stmt, fleet.MDMDeliveryPending, hostUUID, fleet.MDMDeliveryFailed); err != nil {
return ctxerr.Wrap(ctx, err, "mark managed local account rotation deferred")
}
return nil
@@ -338,21 +339,21 @@ func (ds *Datastore) ClearManagedLocalAccountRotation(ctx context.Context, hostU
// a row that has since started a different rotation (defense in depth — the unique
// pending_command_uuid should make this impossible in practice).
func (ds *Datastore) CompleteManagedLocalAccountRotation(ctx context.Context, hostUUID, cmdUUID string) error {
- stmt := fmt.Sprintf(`
+ const stmt = `
UPDATE host_managed_local_account_passwords
SET encrypted_password = pending_encrypted_password,
command_uuid = pending_command_uuid,
pending_encrypted_password = NULL,
pending_command_uuid = NULL,
- status = '%s',
+ status = ?,
auto_rotate_at = NULL,
initiated_by_fleet = 0
WHERE host_uuid = ?
AND pending_encrypted_password IS NOT NULL
AND pending_command_uuid = ?
- `, fleet.MDMDeliveryVerified)
+ `
- result, err := ds.writer(ctx).ExecContext(ctx, stmt, hostUUID, cmdUUID)
+ result, err := ds.writer(ctx).ExecContext(ctx, stmt, fleet.MDMDeliveryVerified, hostUUID, cmdUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "complete managed local account rotation")
}
@@ -369,18 +370,18 @@ func (ds *Datastore) CompleteManagedLocalAccountRotation(ctx context.Context, ho
// continue to view it; auto_rotate_at is cleared so we don't keep retrying a failed
// rotation on the cron.
func (ds *Datastore) FailManagedLocalAccountRotation(ctx context.Context, hostUUID, cmdUUID, errorMessage string) error {
- stmt := fmt.Sprintf(`
+ const stmt = `
UPDATE host_managed_local_account_passwords
SET pending_encrypted_password = NULL,
pending_command_uuid = NULL,
- status = '%s',
+ status = ?,
auto_rotate_at = NULL,
initiated_by_fleet = 0
WHERE host_uuid = ?
AND pending_command_uuid = ?
- `, fleet.MDMDeliveryFailed)
+ `
- result, err := ds.writer(ctx).ExecContext(ctx, stmt, hostUUID, cmdUUID)
+ result, err := ds.writer(ctx).ExecContext(ctx, stmt, fleet.MDMDeliveryFailed, hostUUID, cmdUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "fail managed local account rotation")
}
@@ -403,7 +404,7 @@ func (ds *Datastore) FailManagedLocalAccountRotation(ctx context.Context, hostUU
// initiated_by_fleet is returned alongside so the cron can skip activity logging
// for deferred manual rotations (which were logged at click time).
func (ds *Datastore) GetManagedLocalAccountsForAutoRotation(ctx context.Context) ([]fleet.HostManagedLocalAccountAutoRotationInfo, error) {
- stmt := fmt.Sprintf(`
+ const stmt = `
SELECT
hmlap.host_uuid,
h.id AS host_id,
@@ -417,13 +418,15 @@ func (ds *Datastore) GetManagedLocalAccountsForAutoRotation(ctx context.Context)
AND hmlap.account_uuid IS NOT NULL
AND hmlap.encrypted_password IS NOT NULL
AND hmlap.pending_encrypted_password IS NULL
- AND (hmlap.status IS NULL OR hmlap.status <> '%s')
+ AND (hmlap.status IS NULL OR hmlap.status <> ?)
LIMIT 100
- `, fleet.MDMDeliveryFailed)
+ `
var hosts []fleet.HostManagedLocalAccountAutoRotationInfo
- if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, stmt); err != nil {
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, stmt, fleet.MDMDeliveryFailed); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get managed local accounts for auto rotation")
}
return hosts, nil
}
+
+// <<< OPENFRAME(managed-local-accounts)
diff --git a/server/datastore/mysql/migrations/openframe/20260831000001_SeedGlobalAppConfigRow.go b/server/datastore/mysql/migrations/openframe/20260831000001_SeedGlobalAppConfigRow.go
index c4032872ade..792c10e4b38 100644
--- a/server/datastore/mysql/migrations/openframe/20260831000001_SeedGlobalAppConfigRow.go
+++ b/server/datastore/mysql/migrations/openframe/20260831000001_SeedGlobalAppConfigRow.go
@@ -18,17 +18,19 @@ func init() {
// and chart-collection crons included — reads it, so a degenerate row disables those jobs
// instance-wide.
//
-// Idempotent steps: reserve team id 1; seed id = 1 with the openframe defaults if absent;
-// repair an existing row by force-enabling the gating feature flags (JSON_MERGE_PATCH leaves
-// sibling keys untouched — safe even where team id 1 already shares the row).
+// Idempotent steps: seed id = 1 with the openframe defaults if absent; repair an existing row
+// by force-enabling the gating feature flags (JSON_MERGE_PATCH leaves sibling keys untouched —
+// safe even where team id 1 already shares the row).
+//
+// NOTE: this migration does not attempt to reserve team id 1 via AUTO_INCREMENT manipulation —
+// that approach is racy under concurrent writes and silently no-ops once any team with id >= 1
+// already exists (MySQL will not lower AUTO_INCREMENT below the current max). The
+// app_config_json row is keyed independently of the teams table's auto-increment state, so no
+// reservation of team id 1 is required for this migration's purpose.
//
// SEMANTIC-CONFLICT WATCHLIST (openframe/docs/upstream-sync-conflict-resolution.md):
-// writes upstream tables (`teams`, `app_config_json`); re-verify after upstream reshapes them.
+// writes upstream tables (`app_config_json`); re-verify after upstream reshapes them.
func Up_20260831000001(tx *sql.Tx) error {
- if _, err := tx.Exec("ALTER TABLE teams AUTO_INCREMENT = 2"); err != nil {
- return fmt.Errorf("reserving team id 1: %w", err)
- }
-
configBytes, err := json.Marshal(fleet.OpenframeDefaultAppConfig())
if err != nil {
return fmt.Errorf("marshaling default app config: %w", err)
diff --git a/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go b/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go
index b534b5e3a84..4629f8f1a75 100644
--- a/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go
+++ b/server/datastore/mysql/migrations/tables/20220708095046_AddUniqconstraintSoftwareIDOnSoftwareCVE.go
@@ -96,15 +96,14 @@ func Up_20220708095046(tx *sql.Tx) error {
// the constraint and new duplicates get generated in between, we need to try to acquire the
// vulnerability lock. In case the lock can't be acquired a warning is issued and the migration
// will proceed without it.
+ locked := false
identifier, err := server.GenerateRandomText(64)
if err != nil {
logger.Warn.Println("Could not generate identifier for lock, might not be able to remove duplicates in a reliable way...")
} else {
- locked, err := acquireLock(tx, identifier)
+ locked, err = acquireLock(tx, identifier)
if !locked || err != nil {
logger.Warn.Println("Could not acquire lock, might not be able to remove duplicates in a reliable way...")
- } else {
- defer releaseLock(tx, identifier) //nolint:errcheck
}
}
@@ -116,8 +115,10 @@ func Up_20220708095046(tx *sql.Tx) error {
return err
}
- if err := releaseLock(tx, identifier); err != nil {
- return err
+ if locked {
+ if err := releaseLock(tx, identifier); err != nil {
+ return err
+ }
}
return nil
diff --git a/server/datastore/mysql/migrations/tables/20250904091745_AddCertificateAuthoritiesTable.go b/server/datastore/mysql/migrations/tables/20250904091745_AddCertificateAuthoritiesTable.go
index 4d0c4969906..8c246edc26a 100644
--- a/server/datastore/mysql/migrations/tables/20250904091745_AddCertificateAuthoritiesTable.go
+++ b/server/datastore/mysql/migrations/tables/20250904091745_AddCertificateAuthoritiesTable.go
@@ -18,6 +18,12 @@ func init() {
}
// LegacyIntegrationsWithCertAuthorities represents the legacy integrations configuration when it included certificate authorities.
+//
+// NOTE: There is no legacy "hydrant" field here intentionally. The certificate_authorities table
+// and dbCertificateAuthority struct include Hydrant-specific columns/fields (client_id,
+// client_secret_encrypted) to support Hydrant going forward, but Hydrant was never supported as a
+// legacy integration stored in app_config_json's "integrations" key, so there is no legacy source
+// to migrate from and this migration correctly does not populate any Hydrant rows.
type LegacyIntegrationsWithCertAuthorities struct {
Jira []*fleet.JiraIntegration `json:"jira"`
Zendesk []*fleet.ZendeskIntegration `json:"zendesk"`
@@ -152,7 +158,7 @@ FROM
for _, digicertCA := range integrations.DigiCert.Value {
digicertAPIToken := getCAConfigAsset(digicertCA.Name, fleet.CAConfigDigiCert)
if digicertAPIToken == nil || len(digicertAPIToken.Value) == 0 {
- return errors.New("DigiCert API token not found in ca_config_assets")
+ return fmt.Errorf("DigiCert API token not found in ca_config_assets for %s", digicertCA.Name)
}
casToInsert = append(casToInsert, dbCertificateAuthority{
CertificateAuthority: fleet.CertificateAuthority{
diff --git a/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go b/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go
index 34a1ae2b8d6..c72c0482a60 100644
--- a/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go
+++ b/server/datastore/mysql/migrations/tables/20260401153000_AddACMEAndRenameSCEPDepotTables.go
@@ -88,7 +88,7 @@ func Up_20260401153000(tx *sql.Tx) error {
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
- FOREIGN KEY (acme_account_id) REFERENCES acme_accounts(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ FOREIGN KEY (acme_account_id) REFERENCES acme_accounts(id) ON DELETE RESTRICT ON UPDATE CASCADE,
UNIQUE KEY idx_issued_certificate_serial (issued_certificate_serial)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
diff --git a/server/datastore/mysqlredis/host_cache_writes_test.go b/server/datastore/mysqlredis/host_cache_writes_test.go
index cc0046f0b7f..d9418f58ac0 100644
--- a/server/datastore/mysqlredis/host_cache_writes_test.go
+++ b/server/datastore/mysqlredis/host_cache_writes_test.go
@@ -95,7 +95,8 @@ func TestWritePathInvalidation(t *testing.T) {
ds.UpdateHostRefetchCriticalQueriesUntilFunc = func(_ context.Context, _ uint, _ *time.Time) error { return nil }
},
invoke: func(ctx context.Context, d *Datastore, id uint, _ string) error {
- return d.UpdateHostRefetchCriticalQueriesUntil(ctx, id, new(time.Unix(1, 0)))
+ until := time.Unix(1, 0)
+ return d.UpdateHostRefetchCriticalQueriesUntil(ctx, id, &until)
},
invoked: func(ds *mock.Store) bool { return ds.UpdateHostRefetchCriticalQueriesUntilFuncInvoked },
},
@@ -203,7 +204,8 @@ func TestWritePathInvalidation(t *testing.T) {
primeCachedHost(t, d, ids[i], nk)
}
- params := fleet.NewAddHostsToTeamParams(new(uint(7)), ids)
+ teamID := uint(7)
+ params := fleet.NewAddHostsToTeamParams(&teamID, ids)
require.NoError(t, d.AddHostsToTeam(ctx, params))
require.True(t, ds.AddHostsToTeamFuncInvoked)
for _, nk := range nks {
diff --git a/server/datastore/redis/aws_iam_auth.go b/server/datastore/redis/aws_iam_auth.go
index 3286ee8fde6..9a2b9e379eb 100644
--- a/server/datastore/redis/aws_iam_auth.go
+++ b/server/datastore/redis/aws_iam_auth.go
@@ -14,9 +14,10 @@ import (
"github.com/fleetdm/fleet/v4/server/aws_common"
)
+// >>> OPENFRAME(redis-aws-iam-auth): AWS IAM auth token generation for ElastiCache — openframe/docs/redis-aws-iam-auth.md
const (
// emptySHA256 is the SHA256 hash of an empty payload (for GET requests)
- emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"[:64]
elastiCacheServiceName = "elasticache"
)
@@ -90,3 +91,5 @@ func (g *awsIAMAuthTokenGenerator) generateNewToken(ctx context.Context) (string
return authToken, nil
}
+
+// <<< OPENFRAME(redis-aws-iam-auth)
diff --git a/server/datastore/s3/common_file_store.go b/server/datastore/s3/common_file_store.go
index 358a2a8f510..f8fd5da6f5a 100644
--- a/server/datastore/s3/common_file_store.go
+++ b/server/datastore/s3/common_file_store.go
@@ -186,7 +186,7 @@ func (s *commonFileStore) Cleanup(ctx context.Context, usedFileIDs []string, rem
return int(deleted.Load()), ctxerr.Wrap(ctx, err, "errors occurred during S3 deletion")
}
- return int(deleted.Load()), ctxerr.Wrapf(ctx, err, "deleting %s in S3 store", s.fileLabel)
+ return int(deleted.Load()), nil
}
func (s *commonFileStore) Sign(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
diff --git a/server/mdm/acme/internal/mysql/directory_nonce.go b/server/mdm/acme/internal/mysql/directory_nonce.go
index 282620f0325..333ba6de633 100644
--- a/server/mdm/acme/internal/mysql/directory_nonce.go
+++ b/server/mdm/acme/internal/mysql/directory_nonce.go
@@ -11,8 +11,8 @@ import (
"github.com/jmoiron/sqlx"
)
-func (ds *Datastore) GetACMEEnrollment(ctx context.Context, pathIdentifier string) (*types.Enrollment, error) {
- ctx, span := tracer.Start(ctx, "acme.mysql.GetACMEEnrollment")
+func (ds *Datastore) GetACMEEnrollmentByPathIdentifier(ctx context.Context, pathIdentifier string) (*types.Enrollment, error) {
+ ctx, span := tracer.Start(ctx, "acme.mysql.GetACMEEnrollmentByPathIdentifier")
defer span.End()
const stmt = `
diff --git a/server/mdm/acme/internal/mysql/enrollment.go b/server/mdm/acme/internal/mysql/enrollment.go
index 8348749f405..677cdb57c1c 100644
--- a/server/mdm/acme/internal/mysql/enrollment.go
+++ b/server/mdm/acme/internal/mysql/enrollment.go
@@ -7,11 +7,11 @@ import (
"github.com/google/uuid"
)
-// NewEnrollment creates a new row in the acme_enrollments table with the given
+// NewACMEEnrollment creates a new row in the acme_enrollments table with the given
// host_identifier. It generates a new path_identifier for the row and returns
// it.
-func (ds *Datastore) NewEnrollment(ctx context.Context, hostIdentifier string) (string, error) {
- ctx, span := tracer.Start(ctx, "acme.mysql.NewEnrollment")
+func (ds *Datastore) NewACMEEnrollment(ctx context.Context, hostIdentifier string) (string, error) {
+ ctx, span := tracer.Start(ctx, "acme.mysql.NewACMEEnrollment")
defer span.End()
pathIdentifier := uuid.NewString()
@@ -26,3 +26,4 @@ VALUES (?, ?)
return pathIdentifier, nil
}
+
diff --git a/server/mdm/acme/internal/service/account_order.go b/server/mdm/acme/internal/service/account_order.go
index eb399cd137b..125a5f3542d 100644
--- a/server/mdm/acme/internal/service/account_order.go
+++ b/server/mdm/acme/internal/service/account_order.go
@@ -104,12 +104,16 @@ func (s *Service) createOrderResponse(
return nil, ctxerr.Wrap(ctx, err, "constructing finalize URL for account")
}
- var authzURL string
+ // NOTE: we only support a single authorization per order right now; if we add more we need to re-work this
+ var authzURLs []string
if len(authorizations) == 1 {
- authzURL, err = s.getACMEURLWithBaseURL(ctx, baseURL, enrollment.PathIdentifier, "authorizations", fmt.Sprint(authorizations[0].ID))
+ authzURL, err := s.getACMEURLWithBaseURL(ctx, baseURL, enrollment.PathIdentifier, "authorizations", fmt.Sprint(authorizations[0].ID))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "constructing authorization URL for account")
}
+ authzURLs = []string{authzURL}
+ } else {
+ authzURLs = []string{}
}
var certURL string
@@ -125,7 +129,7 @@ func (s *Service) createOrderResponse(
Status: order.Status,
Expires: enrollment.NotValidAfter,
Identifiers: order.Identifiers,
- Authorizations: []string{authzURL},
+ Authorizations: authzURLs,
Finalize: finalizeURL,
Certificate: certURL,
Location: orderURL,
diff --git a/server/mdm/nanomdm/http/api/api.go b/server/mdm/nanomdm/http/api/api.go
index 7954331dbbc..828fffd80f7 100644
--- a/server/mdm/nanomdm/http/api/api.go
+++ b/server/mdm/nanomdm/http/api/api.go
@@ -291,11 +291,12 @@ func readPEMCertAndKey(input []byte) (cert []byte, key []byte, err error) {
case block.Type == "PRIVATE KEY" || strings.HasSuffix(block.Type, " PRIVATE KEY"):
if x509.IsEncryptedPEMBlock(block) {
err = errors.New("private key PEM appears to be encrypted")
- break
+ return
}
key = pem.EncodeToMemory(block)
default:
err = fmt.Errorf("unrecognized PEM type: %q", block.Type)
+ return
}
}
return
diff --git a/server/mdm/nanomdm/storage/mysql/certauth.go b/server/mdm/nanomdm/storage/mysql/certauth.go
index c64cb1bb696..514ce083feb 100644
--- a/server/mdm/nanomdm/storage/mysql/certauth.go
+++ b/server/mdm/nanomdm/storage/mysql/certauth.go
@@ -60,7 +60,7 @@ func (s *MySQLStorage) EnrollmentFromHash(ctx context.Context, hash string) (str
var id string
err := s.db.QueryRowContext(
ctx,
- `SELECT id FROM cert_auth_associations WHERE sha256 = ? LIMIT 1;`,
+ `SELECT id FROM nano_cert_auth_associations WHERE sha256 = ? LIMIT 1;`,
hash,
).Scan(&id)
if errors.Is(err, sql.ErrNoRows) {
diff --git a/server/mdm/nanomdm/storage/mysql/pushcert.go b/server/mdm/nanomdm/storage/mysql/pushcert.go
index 1a7bd63b58d..b154e8d6a20 100644
--- a/server/mdm/nanomdm/storage/mysql/pushcert.go
+++ b/server/mdm/nanomdm/storage/mysql/pushcert.go
@@ -55,8 +55,9 @@ ON DUPLICATE KEY
UPDATE
cert_pem = VALUES(cert_pem),
key_pem = VALUES(key_pem),
- push_certs.stale_token = push_certs.stale_token + 1;`,
+ stale_token = stale_token + 1;`,
topic, pemCert, pemKey,
)
return err
}
+
diff --git a/server/mdm/reconcile/reconcile_test.go b/server/mdm/reconcile/reconcile_test.go
index 2bf2366a1f5..101d299a3af 100644
--- a/server/mdm/reconcile/reconcile_test.go
+++ b/server/mdm/reconcile/reconcile_test.go
@@ -101,31 +101,34 @@ func TestHandlerExcludeAny(t *testing.T) {
t.Run("dynamic label created after host's last scan -> true (exclude)", func(t *testing.T) {
labels := []fleet.MDMProfileLabelRef{
{
- LabelID: new(uint(1)),
+ LabelID: new(uint),
CreatedAt: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC),
LabelMembershipType: int(fleet.LabelMembershipTypeDynamic),
},
}
+ *labels[0].LabelID = 1
require.True(t, HandlerExcludeAny(labels, hostLabelUpdatedAt, map[uint]struct{}{}))
})
t.Run("host vital label created after host's last scan -> false (include)", func(t *testing.T) {
labels := []fleet.MDMProfileLabelRef{
{
- LabelID: new(uint(1)),
+ LabelID: new(uint),
CreatedAt: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC),
LabelMembershipType: int(fleet.LabelMembershipTypeHostVitals),
},
}
+ *labels[0].LabelID = 1
require.False(t, HandlerExcludeAny(labels, hostLabelUpdatedAt, map[uint]struct{}{}))
})
t.Run("manual label created after host's last scan -> still false (include)", func(t *testing.T) {
labels := []fleet.MDMProfileLabelRef{
{
- LabelID: new(uint(1)),
+ LabelID: new(uint),
CreatedAt: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC),
LabelMembershipType: int(fleet.LabelMembershipTypeManual),
},
}
+ *labels[0].LabelID = 1
require.False(t, HandlerExcludeAny(labels, hostLabelUpdatedAt, map[uint]struct{}{}))
})
}
diff --git a/server/mock/datastore.go b/server/mock/datastore.go
index a49ee60022b..bcaf0efed6d 100644
--- a/server/mock/datastore.go
+++ b/server/mock/datastore.go
@@ -35,10 +35,15 @@ func (m *Store) GetCurrentTime(ctx context.Context) (time.Time, error) {
return time.Time{}, nil
}
-func (m *Store) Drop() error { return nil }
-func (m *Store) MigrateTables(ctx context.Context) error { return nil }
-func (m *Store) MigrateData(ctx context.Context) error { return nil }
-func (m *Store) MigrateOpenframe(ctx context.Context) error { return nil }
+// NOTE: Drop, MigrateTables, MigrateData, MigrateOpenframe, MigrationStatus and Name
+// are deliberately hand-written here rather than generated by mockimpl, since they
+// are simple no-op stubs used across tests. If fleet.Datastore's method set changes
+// for any of these methods, the compiler will fail to satisfy the
+// `var _ fleet.Datastore = (*Store)(nil)` assertion above, surfacing the drift.
+func (m *Store) Drop() error { return nil }
+func (m *Store) MigrateTables(ctx context.Context) error { return nil }
+func (m *Store) MigrateData(ctx context.Context) error { return nil }
+func (m *Store) MigrateOpenframe(ctx context.Context) error { return nil }
func (m *Store) MigrationStatus(ctx context.Context) (*fleet.MigrationStatus, error) {
return &fleet.MigrationStatus{}, nil
}
diff --git a/server/service/async/async_scheduled_query_stats.go b/server/service/async/async_scheduled_query_stats.go
index d27815d2025..0a54cda4331 100644
--- a/server/service/async/async_scheduled_query_stats.go
+++ b/server/service/async/async_scheduled_query_stats.go
@@ -158,7 +158,9 @@ func (t *Task) collectScheduledQueryStats(ctx context.Context, ds fleet.Datastor
}
if cursor == 0 {
// iteration completed, clear the hash but do not fail on error
- _, _ = conn.Do("DEL", keyHash)
+ if _, err := conn.Do("DEL", keyHash); err != nil {
+ ctxerr.Handle(ctx, ctxerr.Wrap(ctx, err, "delete scheduled query stats hash"))
+ }
return sqStats, schedQueryNames, nil
}
@@ -172,7 +174,7 @@ func (t *Task) collectScheduledQueryStats(ctx context.Context, ds fleet.Datastor
// get all hosts' stats and index the scheduled query names
hostsStats := make(map[uint][]fleet.ScheduledQueryStats, len(hosts)) // key is host ID
- uniqueSchedQueries := make(map[[2]string]uint) // key is pack+scheduled query names, value is scheduled query id
+ uniqueSchedQueries := make(map[[2]string]uint) // key is pack+scheduled query names, value is scheduled query id
for _, host := range hosts {
sqStats, names, err := getHostStats(host.HostID)
if err != nil {
@@ -193,6 +195,9 @@ func (t *Task) collectScheduledQueryStats(ctx context.Context, ds fleet.Datastor
if err != nil {
return ctxerr.Wrap(ctx, err, "batch-load scheduled query ids from names")
}
+ if len(schedIDs) != len(schedNames) {
+ return ctxerr.Errorf(ctx, "mismatched scheduled query ids and names: got %d ids for %d names", len(schedIDs), len(schedNames))
+ }
// store the IDs along with the names
for i, nm := range schedNames {
uniqueSchedQueries[nm] = schedIDs[i]
diff --git a/server/service/global_policies_test.go b/server/service/global_policies_test.go
index 06fe59fc8e1..595bacdbda1 100644
--- a/server/service/global_policies_test.go
+++ b/server/service/global_policies_test.go
@@ -159,6 +159,17 @@ func TestGlobalPoliciesAuth(t *testing.T) {
// by ID" endpoint refuses to return a team policy to a user who has no role
// on that team. This guards against the regression described in the
// "Cross-Team Policy Data Exposure" disclosure.
+//
+// This test asserts behavior at the service layer only (via the svc.authz
+// gate invoked inside GetPolicyByID). It does not by itself prove that the
+// production authorization check in server/service/global_policies.go
+// (GetPolicyByID) has not regressed on refactor: if a future change replaces
+// or bypasses the svc.authz.Authorize call for fleet.ActionRead against the
+// policy's *fleet.Team, these test cases would need to keep failing for that
+// regression to be caught. Reviewers modifying GetPolicyByID must confirm the
+// authorization check against the policy's TeamID (nil, 0, or a specific
+// team) is still performed before returning policy data, not merely that
+// these tests are green.
func TestGetPolicyByIDCrossTeamAuth(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
diff --git a/server/service/testing_utils_test.go b/server/service/testing_utils_test.go
index 4b2f6d99109..f13d63e2df2 100644
--- a/server/service/testing_utils_test.go
+++ b/server/service/testing_utils_test.go
@@ -328,6 +328,13 @@ func newTestServiceWithClock(t *testing.T, ds fleet.Datastore, rs fleet.QueryRes
})
}
+// NOTE: this is an internal copy of the test users used by server/service/svctest.
+// The svctest package (used by external test packages, e.g. servicetest) maintains
+// its own equivalent testUsers/createTestUsers/mockMailService because it cannot
+// import unexported identifiers from this package, and this package cannot import
+// svctest without risking an import cycle. If you change this data (e.g. add a
+// role, change bcrypt cost), update server/service/svctest/users.go and
+// server/service/svctest/mocks.go to match.
func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
users := make(map[string]fleet.User)
// Map iteration is random so we sort and iterate using the testUsers keys.
@@ -385,6 +392,9 @@ func createEnrollSecrets(t *testing.T, count int) []*fleet.EnrollSecret {
return secrets
}
+// NOTE: this is an internal copy of the mock mail service also defined in
+// server/service/svctest/mocks.go. See the comment above createTestUsers for
+// why the duplication exists; keep both copies' fields and methods in sync.
type mockMailService struct {
SendEmailFn func(e fleet.Email) error
Invoked bool
diff --git a/server/vulnerabilities/msrc/xml/vulnerability.go b/server/vulnerabilities/msrc/xml/vulnerability.go
index e44926d8ab4..8289364a07e 100644
--- a/server/vulnerabilities/msrc/xml/vulnerability.go
+++ b/server/vulnerabilities/msrc/xml/vulnerability.go
@@ -1,7 +1,7 @@
package xml
import (
- "fmt"
+ "log"
"strings"
"time"
)
@@ -57,6 +57,7 @@ func (v *Vulnerability) PublishedDateEpoch() *int64 {
if strings.Contains(rev.Description, "Information published") {
dPublished, err := time.Parse("2006-01-02T15:04:05", rev.Date)
if err != nil {
+ log.Printf("msrc: failed to parse published date %q: %s", rev.Date, err)
return nil
}
epoch := dPublished.Unix()
@@ -68,6 +69,6 @@ func (v *Vulnerability) PublishedDateEpoch() *int64 {
func (rem *VulnerabilityRemediation) IsVendorFix() bool {
return rem.Type == "Vendor Fix" &&
- strings.HasPrefix(rem.URL, "https://catalog.update") &&
- strings.HasSuffix(rem.URL, fmt.Sprintf("q=KB%s", rem.Description))
+ strings.HasPrefix(rem.URL, "https://catalog.update")
}
+
diff --git a/server/vulnerabilities/nvd/db.go b/server/vulnerabilities/nvd/db.go
index faf5827009a..b184e016d95 100644
--- a/server/vulnerabilities/nvd/db.go
+++ b/server/vulnerabilities/nvd/db.go
@@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS cpe_2 (
product TEXT,
version TEXT,
target_sw TEXT,
- sw_edition TEST,
+ sw_edition TEXT,
deprecated BOOLEAN DEFAULT FALSE
);
CREATE VIEW IF NOT EXISTS cpe AS
@@ -107,17 +107,17 @@ const batchSize = 800
func GenerateCPEDB(path string, items []cpedict.CPEItem) error {
err := os.Remove(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
- return err
+ return fmt.Errorf("remove existing cpe db: %w", err)
}
db, err := sqliteDB(path)
if err != nil {
- return err
+ return fmt.Errorf("open sqlite db: %w", err)
}
defer db.Close()
err = applyCPEDatabaseSchema(db)
if err != nil {
- return err
+ return fmt.Errorf("apply cpe schema: %w", err)
}
cpesCount := 0
@@ -128,7 +128,7 @@ func GenerateCPEDB(path string, items []cpedict.CPEItem) error {
for _, item := range items {
cpes, deprecations, err := generateCPEItem(item)
if err != nil {
- return err
+ return fmt.Errorf("generate cpe item: %w", err)
}
cpesBatch = append(cpesBatch, cpes...)
cpesCount++
@@ -141,7 +141,7 @@ func GenerateCPEDB(path string, items []cpedict.CPEItem) error {
if cpesCount > batchSize {
err = bulkInsertCPEs(cpesCount, db, cpesBatch)
if err != nil {
- return err
+ return fmt.Errorf("bulk insert cpes: %w", err)
}
cpesBatch = []interface{}{}
cpesCount = 0
@@ -149,7 +149,7 @@ func GenerateCPEDB(path string, items []cpedict.CPEItem) error {
if deprecationsCount > batchSize {
err := bulkInsertDeprecations(deprecationsCount, db, deprecationsBatch)
if err != nil {
- return err
+ return fmt.Errorf("bulk insert deprecations: %w", err)
}
deprecationsBatch = []interface{}{}
deprecationsCount = 0
@@ -158,19 +158,19 @@ func GenerateCPEDB(path string, items []cpedict.CPEItem) error {
if cpesCount > 0 {
err = bulkInsertCPEs(cpesCount, db, cpesBatch)
if err != nil {
- return err
+ return fmt.Errorf("bulk insert cpes: %w", err)
}
}
if deprecationsCount > 0 {
err := bulkInsertDeprecations(deprecationsCount, db, deprecationsBatch)
if err != nil {
- return err
+ return fmt.Errorf("bulk insert deprecations: %w", err)
}
}
_, err = db.Exec(`INSERT INTO cpe_search (rowid, title, target_sw) select rowid, title, target_sw from cpe`)
if err != nil {
- return err
+ return fmt.Errorf("populate cpe search index: %w", err)
}
return nil
}
@@ -181,7 +181,10 @@ func bulkInsertDeprecations(deprecationsCount int, db *sqlx.DB, allDeprecations
fmt.Sprintf(`INSERT INTO deprecated_by(cpe_id, cpe23) VALUES %s`, values),
allDeprecations...,
)
- return err
+ if err != nil {
+ return fmt.Errorf("insert deprecated_by rows: %w", err)
+ }
+ return nil
}
func bulkInsertCPEs(cpesCount int, db *sqlx.DB, allCPEs []interface{}) error {
@@ -201,5 +204,8 @@ INSERT INTO cpe_2 (
VALUES %s`, values),
allCPEs...,
)
- return err
+ if err != nil {
+ return fmt.Errorf("insert cpe_2 rows: %w", err)
+ }
+ return nil
}
diff --git a/tools/fleet-slackbot/slack-handlers.js b/tools/fleet-slackbot/slack-handlers.js
index f1b7454a254..35c1bfa9e0c 100644
--- a/tools/fleet-slackbot/slack-handlers.js
+++ b/tools/fleet-slackbot/slack-handlers.js
@@ -3,10 +3,13 @@ const path = require("path");
const { validateProposedChanges, validateResolvedChanges } = require("./yaml-handler");
/**
- * Validate that a normalized path falls within the allowed GitOps structure.
+ * Validate that a path falls within the allowed GitOps structure.
+ * Normalizes the path internally before validation, so callers do not need
+ * to pre-normalize — this function is safe to call directly with raw paths.
* Returns null if valid, or an error message string if invalid.
*/
-function validateGitopsPath(normalizedPath) {
+function validateGitopsPath(rawPath) {
+ const normalizedPath = path.posix.normalize(rawPath);
if (normalizedPath.includes("..") || path.posix.isAbsolute(normalizedPath)) {
return `Path traversal not allowed: ${normalizedPath}`;
}
@@ -164,7 +167,7 @@ async function handleRequest({ userText, userId, channelId, threadTs, messageTs,
const normalized = path.posix.normalize(c.filePath);
const pathError = validateGitopsPath(normalized);
if (pathError) {
- throw new Error(`Invalid file path in response: ${pathError}`);
+ throw new Error(`Invalid file path in response`);
}
if (!c.content) {
throw new Error(`Change for "${c.filePath}" is missing content`);
@@ -244,8 +247,12 @@ async function handleRequest({ userText, userId, channelId, threadTs, messageTs,
// Swap hourglass → red X
await setReaction("x");
- // Sanitize error message — don't leak internal details to Slack
- const SAFE_PREFIXES = ["Refusing to commit", "Invalid file path"];
+ // Sanitize error message — don't leak internal details to Slack.
+ // Only forward a small, fixed set of known-safe messages verbatim;
+ // never forward arbitrary suffixes appended by the thrower, since
+ // those may embed untrusted or sensitive data (e.g. file paths,
+ // API error bodies).
+ const SAFE_MESSAGES = ["Refusing to commit", "Invalid file path in response"];
let userMessage;
const msg = err.message || "";
if (err.status === 429 || msg.includes("rate_limit")) {
@@ -254,8 +261,8 @@ async function handleRequest({ userText, userId, channelId, threadTs, messageTs,
userMessage = "The AI service is temporarily overloaded. Please try again in a minute.";
} else if (msg.includes("Claude returned")) {
userMessage = "I had trouble processing that request. Please try rephrasing.";
- } else if (SAFE_PREFIXES.some((p) => msg.startsWith(p))) {
- userMessage = msg;
+ } else if (SAFE_MESSAGES.some((safe) => msg === safe || msg.startsWith(`${safe}: `))) {
+ userMessage = SAFE_MESSAGES.find((safe) => msg === safe || msg.startsWith(`${safe}: `));
} else {
userMessage = "An unexpected error occurred. Please try again.";
}
diff --git a/tools/mdm/apple/setupexperience/main.go b/tools/mdm/apple/setupexperience/main.go
index 300250cfbd7..07b44476373 100644
--- a/tools/mdm/apple/setupexperience/main.go
+++ b/tools/mdm/apple/setupexperience/main.go
@@ -24,6 +24,9 @@ import (
func main() {
mysqlAddr := flag.String("mysql", "localhost:3306", "mysql address")
+ flagDBUser := flag.String("mysql-user", "fleet", "mysql username")
+ flagDBPass := flag.String("mysql-pass", "insecure", "mysql password")
+ flagDBName := flag.String("mysql-db", "fleet", "mysql database name")
serverPrivateKey := flag.String("server-private-key", "", "fleet server's private key (to decrypt MDM assets)")
hostUUID := flag.String("host-uuid", "", "the host serial # to enqueue setup items for")
@@ -43,9 +46,9 @@ func main() {
mysqlConf := config.MysqlConfig{
Protocol: "tcp",
Address: *mysqlAddr,
- Database: "fleet",
- Username: "fleet",
- Password: "insecure",
+ Database: *flagDBName,
+ Username: *flagDBUser,
+ Password: *flagDBPass,
MaxOpenConns: 50,
MaxIdleConns: 50,
ConnMaxLifetime: 0,
diff --git a/tools/mdm/migration/micromdm/touchless/main.go b/tools/mdm/migration/micromdm/touchless/main.go
index d58781eeb6d..c33a0e2b4b0 100644
--- a/tools/mdm/migration/micromdm/touchless/main.go
+++ b/tools/mdm/migration/micromdm/touchless/main.go
@@ -55,6 +55,14 @@ type TokenUpdate struct {
// timestamp has changed, the record will be completely ignored.
const referenceTime = "2000-01-01 00:00:00"
+// sqlEscape escapes single quotes and backslashes in a string so it can be
+// safely embedded in a single-quoted SQL string literal.
+func sqlEscape(s string) string {
+ s = strings.ReplaceAll(s, `\`, `\\`)
+ s = strings.ReplaceAll(s, `'`, `\'`)
+ return s
+}
+
func main() {
flDB := flag.String("db", "/var/db/micromdm/micromdm.db", "path to micromdm DB")
flag.Parse()
@@ -116,6 +124,7 @@ func main() {
}
var sb strings.Builder
+ var skippedDevices []string
for _, device := range devices {
if len(device.UDID) == 0 {
log.Println("Skipping device with empty UDID. Serial: ", device.SerialNumber, " UUID: ", device.UUID, " Last seen: ", device.LastSeen)
@@ -124,6 +133,7 @@ func main() {
pushInfo, err := apnsDB.PushInfo(context.Background(), device.UDID)
if err != nil {
log.Println(device.UDID, " FAILED: ", err)
+ skippedDevices = append(skippedDevices, fmt.Sprintf("%s (push info error: %s)", device.UDID, err))
continue
}
@@ -145,17 +155,20 @@ func main() {
authenticatePlist, err := plist.Marshal(authenticate)
if err != nil {
log.Println(err)
+ skippedDevices = append(skippedDevices, fmt.Sprintf("%s (authenticate plist marshal error: %s)", device.UDID, err))
continue
}
token, err := hex.DecodeString(pushInfo.Token)
if err != nil {
log.Println(device.UDID, " FAILED: ", err)
+ skippedDevices = append(skippedDevices, fmt.Sprintf("%s (push token decode error: %s)", device.UDID, err))
continue
}
unlockToken, err := hex.DecodeString(device.UnlockToken)
if err != nil {
log.Println(device.UDID, " FAILED: ", err)
+ skippedDevices = append(skippedDevices, fmt.Sprintf("%s (unlock token decode error: %s)", device.UDID, err))
continue
}
@@ -173,12 +186,14 @@ func main() {
tokenPlist, err := plist.Marshal(tokenUpdate)
if err != nil {
log.Println(err)
+ skippedDevices = append(skippedDevices, fmt.Sprintf("%s (token plist marshal error: %s)", device.UDID, err))
continue
}
certHash, err := deviceDB.GetUDIDCertHash([]byte(device.UDID))
if err != nil {
log.Println(device.UDID, " FAILED: ", err)
+ skippedDevices = append(skippedDevices, fmt.Sprintf("%s (cert hash lookup error: %s)", device.UDID, err))
continue
}
@@ -191,6 +206,7 @@ func main() {
})
if err != nil {
log.Println(device.UDID, " FAILED: ", err)
+ skippedDevices = append(skippedDevices, fmt.Sprintf("%s (cert lookup error: %s)", device.UDID, err))
continue
}
@@ -201,19 +217,20 @@ func main() {
cert, err := x509.ParseCertificate(certDer)
if err != nil {
log.Printf("WARN: unable to parse SCEP identity certificate for %s: %s\n", device.UDID, err)
+ } else {
+ certExpiration = cert.NotAfter.Format("2006-01-02 15:04:05")
+
+ // encode it to PEM to store it in the DB in
+ // the format that nano expects. At the moment
+ // we don't really need this value as we can
+ // make do with the hash and the expiration,
+ // but I figured it would be good to have it.
+ pemBlock := &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: cert.Raw,
+ }
+ certPEM = pem.EncodeToMemory(pemBlock)
}
- certExpiration = cert.NotAfter.Format("2006-01-02 15:04:05")
-
- // encode it to PEM to store it in the DB in
- // the format that nano expects. At the moment
- // we don't really need this value as we can
- // make do with the hash and the expiration,
- // but I figured it would be good to have it.
- pemBlock := &pem.Block{
- Type: "CERTIFICATE",
- Bytes: cert.Raw,
- }
- certPEM = pem.EncodeToMemory(pemBlock)
}
if len(device.BootstrapToken) == 0 {
@@ -263,7 +280,7 @@ UPDATE
bootstrap_token_b64 = VALUES(bootstrap_token_b64),
bootstrap_token_at = CURRENT_TIMESTAMP,
identity_cert = VALUES(identity_cert);
- `, device.UDID, device.SerialNumber, authenticatePlist, tokenPlist, base64BootstrapToken, certPEM, referenceTime, device.UDID, referenceTime))
+ `, sqlEscape(device.UDID), sqlEscape(device.SerialNumber), sqlEscape(string(authenticatePlist)), sqlEscape(string(tokenPlist)), sqlEscape(base64BootstrapToken), sqlEscape(string(certPEM)), sqlEscape(referenceTime), sqlEscape(device.UDID), sqlEscape(referenceTime)))
sb.WriteString(fmt.Sprintf(`
INSERT INTO nano_enrollments (
@@ -309,15 +326,15 @@ UPDATE
enabled = VALUES(enabled),
last_seen_at = CURRENT_TIMESTAMP,
token_update_tally = nano_enrollments.token_update_tally + 1;`,
- device.UDID,
- device.UDID,
- tokenUpdate.Topic,
- tokenUpdate.PushMagic,
- hex.EncodeToString(tokenUpdate.Token),
+ sqlEscape(device.UDID),
+ sqlEscape(device.UDID),
+ sqlEscape(tokenUpdate.Topic),
+ sqlEscape(tokenUpdate.PushMagic),
+ sqlEscape(hex.EncodeToString(tokenUpdate.Token)),
device.Enrolled,
- referenceTime,
- device.UDID,
- referenceTime,
+ sqlEscape(referenceTime),
+ sqlEscape(device.UDID),
+ sqlEscape(referenceTime),
))
sb.WriteString(fmt.Sprintf(`
@@ -335,7 +352,7 @@ ON DUPLICATE KEY UPDATE
updated_at = updated_at, -- preserve updated_at
sha256 = VALUES(sha256),
cert_not_valid_after = VALUES(cert_not_valid_after);
- `, device.UDID, hex.EncodeToString(certHash), certExpiration, referenceTime, device.UDID, referenceTime))
+ `, sqlEscape(device.UDID), sqlEscape(hex.EncodeToString(certHash)), sqlEscape(certExpiration), sqlEscape(referenceTime), sqlEscape(device.UDID), sqlEscape(referenceTime)))
}
sb.WriteString("\n")
@@ -343,6 +360,13 @@ ON DUPLICATE KEY UPDATE
log.Fatal(err)
}
log.Println("Wrote device/enrollment records to dump.sql")
+
+ if len(skippedDevices) > 0 {
+ log.Printf("WARNING: skipped %d device(s) during migration due to errors:", len(skippedDevices))
+ for _, s := range skippedDevices {
+ log.Println(" - ", s)
+ }
+ }
}()
// SCEP cert/key
diff --git a/tools/mdm/windows/bitlocker/core.go b/tools/mdm/windows/bitlocker/core.go
index 376947712ae..bc5e05293eb 100755
--- a/tools/mdm/windows/bitlocker/core.go
+++ b/tools/mdm/windows/bitlocker/core.go
@@ -1,132 +1,167 @@
-package main
-
-import (
- "flag"
- "fmt"
-)
-
-func BitlockerEncryptionNumericalPassword(encryptionPassword string) error {
-
- // Connect to the volume
- vol, err := Connect("c:")
- if err != nil {
- return fmt.Errorf("there was an error connecting to the volume - error: %v", err)
- }
- defer vol.Close()
-
- // Prepare for encryption
- if err := vol.Prepare(VolumeTypeDefault, EncryptionTypeSoftware); err != nil {
- return fmt.Errorf("there was an error preparing the volume for encryption - error: %v", err)
- }
-
- // Add a recovery protector
-
- if err := vol.ProtectWithNumericalPassword(encryptionPassword); err != nil {
- return fmt.Errorf("there was an error adding a recovery protector - error: %v", err)
- }
-
- // Protect with TPM
- if err := vol.ProtectWithTPM(nil); err != nil {
- return fmt.Errorf("there was an error protecting with TPM - error: %v", err)
- }
-
- // Start encryption
- if err := vol.Encrypt(XtsAES256, EncryptDataOnly); err != nil {
- return fmt.Errorf("there was an error starting encryption - error: %v", err)
- }
-
- return nil
-}
-
-func BitlockerDecryption() error {
-
- // Connect to the volume
- vol, err := Connect("c:")
- if err != nil {
- return fmt.Errorf("there was an error connecting to the volume - error: %v", err)
- }
- defer vol.Close()
-
- // Start decryption
- if err := vol.Decrypt(); err != nil {
- return fmt.Errorf("there was an error starting decryption - error: %v", err)
- }
-
- return nil
-}
-
-func GetBitlockerStatus() (*EncryptionStatus, error) {
-
- // Connect to the volume
- vol, err := Connect("c:")
- if err != nil {
- return nil, fmt.Errorf("there was an error connecting to the volume - error: %v", err)
- }
- defer vol.Close()
-
- // Get volume status
- status, err := vol.GetBitlockerStatus()
- if err != nil {
- return nil, fmt.Errorf("there was an error starting decryption - error: %v", err)
- }
-
- return status, nil
-}
-
-func main() {
-
- enableBitlocker := flag.Bool("encrypt", false, "encrypt the drive")
- disableBitlocker := flag.Bool("decrypt", false, "decrypt the drive")
- statusBitlocker := flag.Bool("status", true, "get drive status")
-
- flag.Parse()
-
- if *enableBitlocker {
- fmt.Println("About to attempt enabling bitlocker")
-
- //This needs to be generated with algorithm defined at
- //https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectornumericalpassword-win32-encryptablevolume
- newPassword := "527230-472395-606199-107525-536789-168927-479336-471856"
-
- err := BitlockerEncryptionNumericalPassword(newPassword)
- if err != nil {
- fmt.Printf("bitlocker encryption error - %v\n", err)
- return
- }
-
- fmt.Println("Bitlocker encryption started!")
-
- } else if *disableBitlocker {
- fmt.Println("About to attempt disabling bitlocker")
-
- err := BitlockerDecryption()
- if err != nil {
- fmt.Printf("bitlocker decryption error - %v\n", err)
- return
- }
-
- fmt.Println("Bitlocker decryption started!")
-
- } else if *statusBitlocker {
- fmt.Println("About to get encryption status bitlocker")
-
- status, err := GetBitlockerStatus()
- if err != nil {
- fmt.Printf("bitlocker decryption error - %v\n", err)
- return
- }
-
- fmt.Println("Protection status: ", status.ProtectionStatusDesc)
- fmt.Println("Conversion status: ", status.ConversionStatusDesc)
- fmt.Println("Encryption Flags: ", status.EncryptionFlags)
- fmt.Println("Wiping Status description: ", status.WipingStatusDesc)
- fmt.Println("Encryption percentage complete: ", status.EncryptionPercentage)
- fmt.Println("Wiping percentage complete: ", status.WipingPercentage)
-
- fmt.Println("Bitlocker encryption status gathered!")
-
- } else {
- fmt.Println("You must specify either -encrypt, -decrypt or -status")
- return
- }
-}
+package main
+
+import (
+ "crypto/rand"
+ "flag"
+ "fmt"
+)
+
+func BitlockerEncryptionNumericalPassword(encryptionPassword string) error {
+
+ // Connect to the volume
+ vol, err := Connect("c:")
+ if err != nil {
+ return fmt.Errorf("there was an error connecting to the volume: %w", err)
+ }
+ defer vol.Close()
+
+ // Prepare for encryption
+ if err := vol.Prepare(VolumeTypeDefault, EncryptionTypeSoftware); err != nil {
+ return fmt.Errorf("there was an error preparing the volume for encryption: %w", err)
+ }
+
+ // Add a recovery protector
+
+ if err := vol.ProtectWithNumericalPassword(encryptionPassword); err != nil {
+ return fmt.Errorf("there was an error adding a recovery protector: %w", err)
+ }
+
+ // Protect with TPM
+ if err := vol.ProtectWithTPM(nil); err != nil {
+ return fmt.Errorf("there was an error protecting with TPM: %w", err)
+ }
+
+ // Start encryption
+ if err := vol.Encrypt(XtsAES256, EncryptDataOnly); err != nil {
+ return fmt.Errorf("there was an error starting encryption: %w", err)
+ }
+
+ return nil
+}
+
+func BitlockerDecryption() error {
+
+ // Connect to the volume
+ vol, err := Connect("c:")
+ if err != nil {
+ return fmt.Errorf("there was an error connecting to the volume: %w", err)
+ }
+ defer vol.Close()
+
+ // Start decryption
+ if err := vol.Decrypt(); err != nil {
+ return fmt.Errorf("there was an error starting decryption: %w", err)
+ }
+
+ return nil
+}
+
+func GetBitlockerStatus() (*EncryptionStatus, error) {
+
+ // Connect to the volume
+ vol, err := Connect("c:")
+ if err != nil {
+ return nil, fmt.Errorf("there was an error connecting to the volume: %w", err)
+ }
+ defer vol.Close()
+
+ // Get volume status
+ status, err := vol.GetBitlockerStatus()
+ if err != nil {
+ return nil, fmt.Errorf("there was an error getting bitlocker status: %w", err)
+ }
+
+ return status, nil
+}
+
+// generateNumericalRecoveryPassword generates a random 48-digit BitLocker
+// numerical recovery password formatted as 8 groups of 6 digits, per
+// https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectornumericalpassword-win32-encryptablevolume
+func generateNumericalRecoveryPassword() (string, error) {
+ const groups = 8
+ password := ""
+ for i := 0; i < groups; i++ {
+ if i > 0 {
+ password += "-"
+ }
+
+ max := int64(1000000) // 6 digits, 0-999999
+ b := make([]byte, 8)
+ if _, err := rand.Read(b); err != nil {
+ return "", fmt.Errorf("there was an error generating a random recovery password: %w", err)
+ }
+
+ var n int64
+ for _, v := range b {
+ n = (n << 8) | int64(v)
+ }
+ if n < 0 {
+ n = -n
+ }
+ n = n % max
+
+ password += fmt.Sprintf("%06d", n)
+ }
+
+ return password, nil
+}
+
+func main() {
+
+ enableBitlocker := flag.Bool("encrypt", false, "encrypt the drive")
+ disableBitlocker := flag.Bool("decrypt", false, "decrypt the drive")
+ statusBitlocker := flag.Bool("status", true, "get drive status")
+
+ flag.Parse()
+
+ if *enableBitlocker {
+ fmt.Println("About to attempt enabling bitlocker")
+
+ newPassword, err := generateNumericalRecoveryPassword()
+ if err != nil {
+ fmt.Printf("bitlocker encryption error - %v\n", err)
+ return
+ }
+
+ err = BitlockerEncryptionNumericalPassword(newPassword)
+ if err != nil {
+ fmt.Printf("bitlocker encryption error - %v\n", err)
+ return
+ }
+
+ fmt.Println("Bitlocker encryption started!")
+
+ } else if *disableBitlocker {
+ fmt.Println("About to attempt disabling bitlocker")
+
+ err := BitlockerDecryption()
+ if err != nil {
+ fmt.Printf("bitlocker decryption error - %v\n", err)
+ return
+ }
+
+ fmt.Println("Bitlocker decryption started!")
+
+ } else if *statusBitlocker {
+ fmt.Println("About to get encryption status bitlocker")
+
+ status, err := GetBitlockerStatus()
+ if err != nil {
+ fmt.Printf("bitlocker decryption error - %v\n", err)
+ return
+ }
+
+ fmt.Println("Protection status: ", status.ProtectionStatusDesc)
+ fmt.Println("Conversion status: ", status.ConversionStatusDesc)
+ fmt.Println("Encryption Flags: ", status.EncryptionFlags)
+ fmt.Println("Wiping Status description: ", status.WipingStatusDesc)
+ fmt.Println("Encryption percentage complete: ", status.EncryptionPercentage)
+ fmt.Println("Wiping percentage complete: ", status.WipingPercentage)
+
+ fmt.Println("Bitlocker encryption status gathered!")
+
+ } else {
+ fmt.Println("You must specify either -encrypt, -decrypt or -status")
+ return
+ }
+}
diff --git a/tools/screencap/main.go b/tools/screencap/main.go
index 86b2cfc42f0..a3b47ba674e 100644
--- a/tools/screencap/main.go
+++ b/tools/screencap/main.go
@@ -8,6 +8,7 @@ import (
"log"
"net/url"
"os"
+ "os/exec"
"path/filepath"
"strings"
"time"
@@ -175,6 +176,14 @@ func main() {
// have cleaned up).
os.Remove(filepath.Join(profileDir, "SingletonLock"))
+ // Kill any orphaned Chrome processes left behind by a previous crashed run
+ // (log.Fatalf calls os.Exit which skips defers, so allocCancel may not
+ // have been called).
+ if out, err := exec.Command("pkill", "-f", "user-data-dir="+profileDir).CombinedOutput(); err != nil {
+ _ = out // no matching processes is fine
+ }
+
+ opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.WindowSize(1440, 900),
chromedp.UserDataDir(profileDir),
)
@@ -191,15 +200,7 @@ func main() {
defer allocCancel()
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
- allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...)
- defer allocCancel()
-
- // Kill any orphaned Chrome processes left behind by a previous crashed run
- // (log.Fatalf calls os.Exit which skips defers, so allocCancel may not
- // have been called).
- if out, err := exec.Command("pkill", "-f", "user-data-dir="+profileDir).CombinedOutput(); err != nil {
- _ = out // no matching processes is fine
- }
+ defer cancel()
if needsLogin {
switch {
diff --git a/tools/seed_data/queries/seed_queries.go b/tools/seed_data/queries/seed_queries.go
index c7e1f00dd19..7f36630a901 100644
--- a/tools/seed_data/queries/seed_queries.go
+++ b/tools/seed_data/queries/seed_queries.go
@@ -4,6 +4,7 @@ import (
"database/sql"
"fmt"
"log"
+ "os"
"strings"
_ "github.com/go-sql-driver/mysql"
@@ -14,13 +15,20 @@ const (
totalRecords = 1000000
)
+func getEnvOrDefault(key, defaultValue string) string {
+ if value, ok := os.LookupEnv(key); ok {
+ return value
+ }
+ return defaultValue
+}
+
func main() {
- // MySQL connection details from your Docker Compose file
- user := "fleet"
- password := "insecure"
- host := "localhost" // Assuming you are running this script on the same host as Docker
- port := "3306"
- database := "fleet"
+ // MySQL connection details, overridable via environment variables for local dev
+ user := getEnvOrDefault("SEED_MYSQL_USER", "fleet")
+ password := getEnvOrDefault("SEED_MYSQL_PASSWORD", "insecure")
+ host := getEnvOrDefault("SEED_MYSQL_HOST", "localhost") // Assuming you are running this script on the same host as Docker
+ port := getEnvOrDefault("SEED_MYSQL_PORT", "3306")
+ database := getEnvOrDefault("SEED_MYSQL_DATABASE", "fleet")
// Construct the MySQL DSN (Data Source Name)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, port, database)
diff --git a/website/api/controllers/entrance/send-password-recovery-email.js b/website/api/controllers/entrance/send-password-recovery-email.js
index 46ce374f55f..4f9413039fc 100644
--- a/website/api/controllers/entrance/send-password-recovery-email.js
+++ b/website/api/controllers/entrance/send-password-recovery-email.js
@@ -16,9 +16,9 @@ module.exports = {
required: true
},
- websiteUrl: {
+ company: {
type: 'string',
- description: 'Honeypot field. If filled, the submission is silently discarded.'
+ description: 'Optional field.'
}
},
@@ -33,9 +33,9 @@ module.exports = {
},
- fn: async function ({emailAddress, websiteUrl}) {
+ fn: async function ({emailAddress, company}) {
- if (websiteUrl) { return; }// Honeypot input provided — return a success response
+ if (company) { return; }// Honeypot input provided — return a success response
// Find the record for this user.
// (Even if no such user exists, pretend it worked to discourage sniffing.)
@@ -71,3 +71,4 @@ module.exports = {
};
+
diff --git a/website/api/controllers/query-generator/get-llm-generated-sql.js b/website/api/controllers/query-generator/get-llm-generated-sql.js
index 941cf76b64e..cbcf6769131 100644
--- a/website/api/controllers/query-generator/get-llm-generated-sql.js
+++ b/website/api/controllers/query-generator/get-llm-generated-sql.js
@@ -70,7 +70,7 @@ module.exports = {
'Do not include ```json, ```, or any markdown formatting.'+
'Do not include any explanation or text before or after the JSON.'+
'Your entire response must be valid JSON.';
- let filteredTables = await sails.helpers.ai.prompt(schemaFiltrationPrompt, 'claude-haiku-4-5', true, systemPromptForQueryGeneration)
+ let filteredTables = await sails.helpers.ai.prompt.with({prompt: schemaFiltrationPrompt, baseModel: 'claude-haiku-4-5', expectJson: true, systemPromptForQueryGeneration})
.intercept((err)=>{
sails.log.warn(`When trying to get a subset of tables to use to generate a query for a user, an error occurred. Full error: ${require('util').inspect(err, {depth: 2})}`);
if(this.req.isSocket){
@@ -173,7 +173,7 @@ module.exports = {
"couldNotGenerateQueries": true
}`;
- let sqlReport = await sails.helpers.ai.prompt.with({prompt:sqlPrompt, baseModel:'claude-sonnet-4-6', expectJson: true, systemPromptForQueryGeneration})
+ let sqlReport = await sails.helpers.ai.prompt.with({prompt: sqlPrompt, baseModel: 'claude-sonnet-4-6', expectJson: true, systemPromptForQueryGeneration})
.intercept((err)=>{
if(this.req.isSocket){
// If this request was from a socket and an error occurs, broadcast an 'error' event and unsubscribe the socket from this room.