diff --git a/changes/27042-microsoft-compliance-partner-backend b/changes/27042-microsoft-compliance-partner-backend
new file mode 100644
index 0000000000..1f55309374
--- /dev/null
+++ b/changes/27042-microsoft-compliance-partner-backend
@@ -0,0 +1 @@
+* Added support for Microsoft "Conditional acccess" as a compliance partner.
diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go
index 69cdd6e707..8f23fdaa7a 100644
--- a/cmd/fleet/serve.go
+++ b/cmd/fleet/serve.go
@@ -55,6 +55,7 @@ import (
"github.com/fleetdm/fleet/v4/server/pubsub"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/fleetdm/fleet/v4/server/service/async"
+ "github.com/fleetdm/fleet/v4/server/service/conditional_access_microsoft_proxy"
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
@@ -712,6 +713,25 @@ the way that the Fleet server works.
ctx, cancelFunc := context.WithCancel(baseCtx)
defer cancelFunc()
+ var conditionalAccessMicrosoftProxy *conditional_access_microsoft_proxy.Proxy
+ if config.MicrosoftCompliancePartner.IsSet() {
+ var err error
+ conditionalAccessMicrosoftProxy, err = conditional_access_microsoft_proxy.New(
+ config.MicrosoftCompliancePartner.ProxyURI,
+ config.MicrosoftCompliancePartner.ProxyAPIKey,
+ func() (string, error) {
+ appCfg, err := ds.AppConfig(ctx)
+ if err != nil {
+ return "", fmt.Errorf("failed to load appconfig: %w", err)
+ }
+ return appCfg.ServerSettings.ServerURL, nil
+ },
+ )
+ if err != nil {
+ initFatal(err, "new microsoft compliance proxy")
+ }
+ }
+
eh := errorstore.NewHandler(ctx, redisPool, logger, config.Logging.ErrorRetentionPeriod)
ctx = ctxerr.NewContext(ctx, eh)
svc, err := service.NewService(
@@ -740,6 +760,7 @@ the way that the Fleet server works.
wstepCertManager,
eeservice.NewSCEPConfigService(logger, nil),
digicert.NewService(digicert.WithLogger(logger)),
+ conditionalAccessMicrosoftProxy,
)
if err != nil {
initFatal(err, "initializing service")
diff --git a/cmd/fleetctl/fleetctl/apply_test.go b/cmd/fleetctl/fleetctl/apply_test.go
index edcac4aad9..5b2170b754 100644
--- a/cmd/fleetctl/fleetctl/apply_test.go
+++ b/cmd/fleetctl/fleetctl/apply_test.go
@@ -212,6 +212,9 @@ func TestApplyTeamSpecs(t *testing.T) {
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return nil, ¬FoundError{}
+ }
filename := writeTmpYml(t, `
---
@@ -809,6 +812,9 @@ func TestApplyAppConfigDryRunIssue(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return nil, ¬FoundError{}
+ }
// first, set the default app config's agent options as set after fleetctl setup
name := writeTmpYml(t, `---
@@ -1366,6 +1372,9 @@ func TestApplyAsGitOps(t *testing.T) {
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return nil, ¬FoundError{}
+ }
// Apply global config.
name := writeTmpYml(t, `---
@@ -2173,6 +2182,9 @@ func TestApplyMacosSetup(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return nil, ¬FoundError{}
+ }
return ds
}
@@ -2815,6 +2827,10 @@ func TestApplySpecs(t *testing.T) {
return nil
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
+ }
+
// teams - team ID 1 already exists
teamsByName := map[string]*fleet.Team{
"team1": {
@@ -4095,3 +4111,15 @@ func TestApplyFileExtensionValidation(t *testing.T) {
})
}
}
+
+type notFoundError struct{}
+
+var _ fleet.NotFoundError = (*notFoundError)(nil)
+
+func (e *notFoundError) IsNotFound() bool {
+ return true
+}
+
+func (e *notFoundError) Error() string {
+ return ""
+}
diff --git a/cmd/fleetctl/fleetctl/generate_gitops.go b/cmd/fleetctl/fleetctl/generate_gitops.go
index 2436467d65..303cace2e5 100644
--- a/cmd/fleetctl/fleetctl/generate_gitops.go
+++ b/cmd/fleetctl/fleetctl/generate_gitops.go
@@ -681,13 +681,11 @@ func (cmd *GenerateGitopsCommand) generateIntegrations(filePath string, integrat
result = result["global_integrations"].(map[string]interface{})
} else {
result = result["team_integrations"].(map[string]interface{})
- if result["google_calendar"] != nil {
- result = map[string]interface{}{
- "google_calendar": result["google_calendar"],
- }
- } else {
- result = nil
- }
+
+ // We currently don't support configuring Jira and Zendesk integrations on the team.
+ delete(result, "jira")
+ delete(result, "zendesk")
+
// Team integrations don't have secrets right now, so just return as-is.
return result, nil
}
@@ -1018,13 +1016,14 @@ func (cmd *GenerateGitopsCommand) generatePolicies(teamId *uint, filePath string
result := make([]map[string]interface{}, len(policies))
for i, policy := range policies {
policySpec := map[string]interface{}{
- jsonFieldName(t, "Name"): policy.Name,
- jsonFieldName(t, "Description"): policy.Description,
- jsonFieldName(t, "Resolution"): policy.Resolution,
- jsonFieldName(t, "Query"): policy.Query,
- jsonFieldName(t, "Platform"): policy.Platform,
- jsonFieldName(t, "Critical"): policy.Critical,
- jsonFieldName(t, "CalendarEventsEnabled"): policy.CalendarEventsEnabled,
+ jsonFieldName(t, "Name"): policy.Name,
+ jsonFieldName(t, "Description"): policy.Description,
+ jsonFieldName(t, "Resolution"): policy.Resolution,
+ jsonFieldName(t, "Query"): policy.Query,
+ jsonFieldName(t, "Platform"): policy.Platform,
+ jsonFieldName(t, "Critical"): policy.Critical,
+ jsonFieldName(t, "CalendarEventsEnabled"): policy.CalendarEventsEnabled,
+ jsonFieldName(t, "ConditionalAccessEnabled"): policy.ConditionalAccessEnabled,
}
// Handle software automation.
if policy.InstallSoftware != nil {
diff --git a/cmd/fleetctl/fleetctl/generate_gitops_test.go b/cmd/fleetctl/fleetctl/generate_gitops_test.go
index 7c75232a62..d5124c1a22 100644
--- a/cmd/fleetctl/fleetctl/generate_gitops_test.go
+++ b/cmd/fleetctl/fleetctl/generate_gitops_test.go
@@ -255,6 +255,7 @@ func (MockClient) GetPolicies(teamID *uint) ([]*fleet.Policy, error) {
}, {
LabelName: "Label B",
}},
+ ConditionalAccessEnabled: true,
},
InstallSoftware: &fleet.PolicySoftwareTitle{
SoftwareTitleID: 1,
@@ -265,12 +266,13 @@ func (MockClient) GetPolicies(teamID *uint) ([]*fleet.Policy, error) {
return []*fleet.Policy{
{
PolicyData: fleet.PolicyData{
- ID: 1,
- Name: "Team Policy",
- Query: "SELECT * FROM team_policy WHERE id = 1",
- Resolution: ptr.String("Do a team thing"),
- Description: "This is a team policy",
- Platform: "linux,windows",
+ ID: 1,
+ Name: "Team Policy",
+ Query: "SELECT * FROM team_policy WHERE id = 1",
+ Resolution: ptr.String("Do a team thing"),
+ Description: "This is a team policy",
+ Platform: "linux,windows",
+ ConditionalAccessEnabled: true,
},
RunScript: &fleet.PolicyScript{
ID: 1,
diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go
index e2585d652c..5986c8aa7b 100644
--- a/cmd/fleetctl/fleetctl/gitops_test.go
+++ b/cmd/fleetctl/fleetctl/gitops_test.go
@@ -15,6 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
"github.com/fleetdm/fleet/v4/pkg/file"
+ "github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
@@ -3018,3 +3019,63 @@ func TestGitOpsMDMAuthSettings(t *testing.T) {
require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.MetadataURL)
require.Empty(t, appConfig.MDM.EndUserAuthentication.SSOProviderSettings.IDPName)
}
+
+func TestGitOpsTeamConditionalAccess(t *testing.T) {
+ teamName := "TestTeamConditionalAccess"
+
+ ds, _, savedTeams := testing_utils.SetupFullGitOpsPremiumServer(t)
+
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
+ }
+
+ // Create integration with conditional access enabled.
+ _, err := ds.NewTeam(context.Background(), &fleet.Team{Name: teamName, Config: fleet.TeamConfig{
+ Integrations: fleet.TeamIntegrations{
+ ConditionalAccessEnabled: optjson.SetBool(true),
+ },
+ }})
+ require.NoError(t, err)
+ require.NotNil(t, *savedTeams[teamName])
+
+ // Do a GitOps run with conditional access not set.
+ t.Setenv("TEST_TEAM_NAME", teamName)
+ _, err = RunAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_config_webhook.yml"})
+ require.NoError(t, err)
+
+ team, err := ds.TeamByName(context.Background(), teamName)
+ require.NoError(t, err)
+ require.NotNil(t, team)
+ require.True(t, team.Config.Integrations.ConditionalAccessEnabled.Set)
+ require.False(t, team.Config.Integrations.ConditionalAccessEnabled.Value)
+}
+
+func TestGitOpsNoTeamConditionalAccess(t *testing.T) {
+ globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
+ ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
+
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return &fleet.ConditionalAccessMicrosoftIntegration{}, nil
+ }
+
+ appConfig := fleet.AppConfig{
+ Integrations: fleet.Integrations{
+ ConditionalAccessEnabled: optjson.SetBool(true),
+ },
+ }
+
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
+ return &appConfig, nil
+ }
+
+ ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
+ appConfig = *config
+ return nil
+ }
+
+ // Do a GitOps run with conditional access not set.
+ _, err := RunAppNoChecks([]string{"gitops", "-f", globalFileBasic.Name()})
+ require.NoError(t, err)
+ require.True(t, appConfig.Integrations.ConditionalAccessEnabled.Set)
+ require.False(t, appConfig.Integrations.ConditionalAccessEnabled.Value)
+}
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json
index 53ee2a2106..3c9caee7fc 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigJson.json
@@ -93,7 +93,8 @@
"google_calendar": null,
"ndes_scep_proxy": null,
"custom_scep_proxy": null,
- "digicert": null
+ "digicert": null,
+ "conditional_access_enabled": null
},
"mdm": {
"android_enabled_and_configured": false,
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json
index 312791297d..5ac1c43739 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerJson.json
@@ -66,7 +66,8 @@
"google_calendar": null,
"ndes_scep_proxy": null,
"custom_scep_proxy": null,
- "digicert": null
+ "digicert": null,
+ "conditional_access_enabled": null
},
"mdm": {
"android_enabled_and_configured": false,
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml
index d6cd1ab04a..0876d9aba2 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigTeamMaintainerYaml.yml
@@ -14,6 +14,7 @@ spec:
enable_host_users: true
enable_software_inventory: false
integrations:
+ conditional_access_enabled: null
custom_scep_proxy: null
digicert: null
google_calendar: null
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
index c1b5123fc4..7e5e41c0d9 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
@@ -14,6 +14,7 @@ spec:
enable_host_users: true
enable_software_inventory: false
integrations:
+ conditional_access_enabled: null
custom_scep_proxy: null
digicert: null
google_calendar: null
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json
index 617763acd3..2a8f309cfa 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json
@@ -149,7 +149,8 @@
"google_calendar": null,
"ndes_scep_proxy": null,
"custom_scep_proxy": null,
- "digicert": null
+ "digicert": null,
+ "conditional_access_enabled": null
},
"update_interval": {
"osquery_detail": "1h0m0s",
@@ -168,7 +169,8 @@
},
"license": {
"tier": "free",
- "expiration": "0001-01-01T00:00:00Z"
+ "expiration": "0001-01-01T00:00:00Z",
+ "managed_cloud": false
},
"logging": {
"debug": true,
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml
index 62d6216dfc..76b17918da 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml
@@ -14,6 +14,7 @@ spec:
enable_host_users: true
enable_software_inventory: false
integrations:
+ conditional_access_enabled: null
custom_scep_proxy: null
digicert: null
google_calendar: null
@@ -69,6 +70,7 @@ spec:
license:
expiration: "0001-01-01T00:00:00Z"
tier: free
+ managed_cloud: false
logging:
debug: true
json: false
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsJson.json b/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsJson.json
index 9993409eab..f94cc8b618 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsJson.json
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsJson.json
@@ -23,7 +23,8 @@
"integrations": {
"jira": null,
"zendesk": null,
- "google_calendar": null
+ "google_calendar": null,
+ "conditional_access_enabled": null
},
"features": {
"enable_host_users": true,
@@ -106,7 +107,8 @@
"integrations": {
"jira": null,
"zendesk": null,
- "google_calendar": null
+ "google_calendar": null,
+ "conditional_access_enabled": null
},
"features": {
"enable_host_users": false,
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsYaml.yml
index 2793acaab8..573e8520eb 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsYaml.yml
+++ b/cmd/fleetctl/fleetctl/testdata/expectedGetTeamsYaml.yml
@@ -11,6 +11,7 @@ spec:
host_expiry_window: 0
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_updates:
@@ -65,6 +66,7 @@ spec:
host_expiry_window: 15
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_updates:
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseJson.json b/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseJson.json
index 7afa367ad1..240dd5d1bd 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseJson.json
+++ b/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseJson.json
@@ -83,7 +83,8 @@
"updated_at": "0001-01-01T00:00:00Z",
"created_at": "0001-01-01T00:00:00Z",
"critical": false,
- "calendar_events_enabled": true
+ "calendar_events_enabled": true,
+ "conditional_access_enabled": false
},
{
"id": 2,
@@ -99,7 +100,8 @@
"updated_at": "0001-01-01T00:00:00Z",
"created_at": "0001-01-01T00:00:00Z",
"critical": false,
- "calendar_events_enabled": false
+ "calendar_events_enabled": false,
+ "conditional_access_enabled": false
}
],
"status": "offline",
diff --git a/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseYaml.yml b/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseYaml.yml
index 71ddab9d40..51d9456d83 100644
--- a/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseYaml.yml
+++ b/cmd/fleetctl/fleetctl/testdata/expectedHostDetailResponseYaml.yml
@@ -68,6 +68,7 @@ spec:
updated_at: "0001-01-01T00:00:00Z"
critical: false
calendar_events_enabled: true
+ conditional_access_enabled: false
- author_email: "alice@example.com"
author_id: 1
author_name: Alice
@@ -82,6 +83,7 @@ spec:
updated_at: "0001-01-01T00:00:00Z"
critical: false
calendar_events_enabled: false
+ conditional_access_enabled: false
policy_updated_at: "0001-01-01T00:00:00Z"
public_ip: ""
primary_ip: ""
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/appConfig.json b/cmd/fleetctl/fleetctl/testdata/generateGitops/appConfig.json
index 5974075695..382c102dc9 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/appConfig.json
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/appConfig.json
@@ -193,6 +193,7 @@
"group_id": 123456789
}
],
+ "conditional_access_enabled": true,
"google_calendar": [
{
"domain": "fleetdm.com",
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedGlobalPolicies.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedGlobalPolicies.yaml
index 72c2a343c7..cc36db7126 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedGlobalPolicies.yaml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedGlobalPolicies.yaml
@@ -1,4 +1,5 @@
- calendar_events_enabled: false
+ conditional_access_enabled: true
critical: false
description: This is a global policy
install_software:
@@ -9,4 +10,4 @@
name: Global Policy
platform: darwin
query: SELECT * FROM global_policy WHERE id = 1
- resolution: Do a global thing
\ No newline at end of file
+ resolution: Do a global thing
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml
index 517cc37800..cdae94e7c4 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings-insecure.yaml
@@ -13,6 +13,7 @@ host_expiry_settings:
host_expiry_enabled: false
host_expiry_window: 59995
integrations:
+ conditional_access_enabled: true
custom_scep_proxy:
- challenge: some-custom-scep-proxy-challenge
name: some-custom-scep-proxy-name
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml
index 1c7dcf9042..b01c611951 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedOrgSettings.yaml
@@ -13,6 +13,7 @@ host_expiry_settings:
host_expiry_enabled: false
host_expiry_window: 59995
integrations:
+ conditional_access_enabled: true
custom_scep_proxy:
- challenge: ___GITOPS_COMMENT_5___
name: some-custom-scep-proxy-name
@@ -118,4 +119,4 @@ webhook_settings:
destination_url: https://some-vulerabilities-webhook-url.com
enable_vulnerabilities_webhook: true
host_batch_size: 3
-yara_rules: {}
\ No newline at end of file
+yara_rules: {}
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings-insecure.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings-insecure.yaml
index a1616be62b..37e6a59d0a 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings-insecure.yaml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings-insecure.yaml
@@ -5,6 +5,7 @@ host_expiry_settings:
host_expiry_enabled: false
host_expiry_window: 1
integrations:
+ conditional_access_enabled: true
google_calendar:
enable_calendar_events: true
webhook_url: https://some-team-google-calendar-webhook.com
@@ -23,4 +24,4 @@ webhook_settings:
days_count: 3
destination_url: https://some-team-host-status-webhook.com
enable_host_status_webhook: false
- host_percentage: 2
\ No newline at end of file
+ host_percentage: 2
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings.yaml b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings.yaml
index 9410c9cf68..9006aa49d6 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings.yaml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/expectedTeamSettings.yaml
@@ -5,6 +5,7 @@ host_expiry_settings:
host_expiry_enabled: false
host_expiry_window: 1
integrations:
+ conditional_access_enabled: true
google_calendar:
enable_calendar_events: true
webhook_url: https://some-team-google-calendar-webhook.com
@@ -23,4 +24,4 @@ webhook_settings:
days_count: 3
destination_url: https://some-team-host-status-webhook.com
enable_host_status_webhook: false
- host_percentage: 2
\ No newline at end of file
+ host_percentage: 2
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json b/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json
index 3028c3c8d1..4fc34d9884 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/teamConfig.json
@@ -66,6 +66,7 @@
"group_id": 123456789
}
],
+ "conditional_access_enabled": true,
"google_calendar": {
"enable_calendar_events": true,
"webhook_url": "https://some-team-google-calendar-webhook.com"
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml
index d6628556bf..d06b393b34 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_free/default.yml
@@ -56,6 +56,7 @@ org_settings:
host_expiry_enabled: false
host_expiry_window: 59995
integrations:
+ conditional_access_enabled: true
custom_scep_proxy:
- challenge: # TODO: Add your custom SCEP proxy challenge here
name: some-custom-scep-proxy-name
@@ -151,6 +152,7 @@ org_settings:
yara_rules:
policies:
- calendar_events_enabled: false
+ conditional_access_enabled: true
critical: false
description: This is a global policy
install_software:
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml
index f885a337c5..61a3ae589d 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/default.yml
@@ -41,6 +41,7 @@ org_settings:
host_expiry_enabled: false
host_expiry_window: 59995
integrations:
+ conditional_access_enabled: true
custom_scep_proxy:
- challenge: # TODO: Add your custom SCEP proxy challenge here
name: some-custom-scep-proxy-name
@@ -149,6 +150,7 @@ org_settings:
yara_rules:
policies:
- calendar_events_enabled: false
+ conditional_access_enabled: true
critical: false
description: This is a global policy
install_software:
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/no-team.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/no-team.yml
index 4879c5fba1..e375ad6b51 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/no-team.yml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/no-team.yml
@@ -24,6 +24,7 @@ controls:
name: No team
policies:
- calendar_events_enabled: false
+ conditional_access_enabled: true
critical: false
description: This is a team policy
name: Team Policy
diff --git a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a.yml b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a.yml
index 242c83d3f8..53f8053ab2 100644
--- a/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a.yml
+++ b/cmd/fleetctl/fleetctl/testdata/generateGitops/test_dir_premium/teams/team-a.yml
@@ -39,6 +39,7 @@ controls:
name: Team A
policies:
- calendar_events_enabled: false
+ conditional_access_enabled: true
critical: false
description: This is a team policy
name: Team Policy
@@ -84,6 +85,7 @@ team_settings:
host_expiry_enabled: false
host_expiry_window: 1
integrations:
+ conditional_access_enabled: true
google_calendar:
enable_calendar_events: true
webhook_url: https://some-team-google-calendar-webhook.com
diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml
index b4b9464a8f..84ab99de9b 100644
--- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml
+++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml
@@ -14,6 +14,7 @@ spec:
activity_expiry_enabled: false
activity_expiry_window: 0
integrations:
+ conditional_access_enabled: null
custom_scep_proxy: null
digicert: null
google_calendar: null
diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml
index f2919fad67..435e8c7786 100644
--- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml
+++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml
@@ -14,6 +14,7 @@ spec:
activity_expiry_enabled: false
activity_expiry_window: 0
integrations:
+ conditional_access_enabled: null
custom_scep_proxy: null
digicert: null
google_calendar: null
diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml
index adb6ac2107..cb3930cb4b 100644
--- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml
+++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml
@@ -11,6 +11,7 @@ spec:
host_expiry_window: 0
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_settings:
@@ -56,6 +57,7 @@ spec:
host_expiry_window: 0
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_settings:
diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml
index 964c064235..ac4f55344a 100644
--- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml
+++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml
@@ -11,6 +11,7 @@ spec:
host_expiry_window: 0
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_settings:
@@ -56,6 +57,7 @@ spec:
host_expiry_window: 0
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_settings:
diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml
index 01def9ac0e..28665e26ed 100644
--- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml
+++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml
@@ -11,6 +11,7 @@ spec:
enable_software_inventory: false
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_settings:
diff --git a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Set.yml b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Set.yml
index 55650c4a60..1418c61803 100644
--- a/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Set.yml
+++ b/cmd/fleetctl/fleetctl/testdata/macosSetupExpectedTeam1Set.yml
@@ -10,6 +10,7 @@ spec:
host_expiry_window: 0
integrations:
google_calendar: null
+ conditional_access_enabled: null
mdm:
enable_disk_encryption: false
macos_settings:
diff --git a/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go b/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go
index 42cef53b3b..784d804e91 100644
--- a/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go
+++ b/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go
@@ -19,7 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/cached_mysql"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
- "github.com/fleetdm/fleet/v4/server/mdm/apple"
+ apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
@@ -89,7 +89,8 @@ func RunServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*http
}, nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
- _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
+ _ sqlx.QueryerContext,
+ ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM},
fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM},
@@ -388,7 +389,8 @@ func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}}, nil
}
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions,
- tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
+ tmFilter fleet.TeamFilter,
+ ) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
diff --git a/cmd/fleetctl/fleetctl/users_test.go b/cmd/fleetctl/fleetctl/users_test.go
index f9dd954e5b..9237d95831 100644
--- a/cmd/fleetctl/fleetctl/users_test.go
+++ b/cmd/fleetctl/fleetctl/users_test.go
@@ -15,7 +15,6 @@ import (
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/test"
-
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -48,18 +47,6 @@ func TestUserDelete(t *testing.T) {
assert.Equal(t, uint(42), deletedUser)
}
-type notFoundError struct{}
-
-var _ fleet.NotFoundError = (*notFoundError)(nil)
-
-func (e *notFoundError) IsNotFound() bool {
- return true
-}
-
-func (e *notFoundError) Error() string {
- return ""
-}
-
// TestUserCreateForcePasswordReset tests that the `fleetctl user create` command
// creates a user with the proper "AdminForcePasswordReset" value depending on
// the passed flags (e.g. SSO users shouldn't be required to do password reset on first login).
diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md
index d49e43b027..1a341e1252 100644
--- a/docs/Configuration/yaml-files.md
+++ b/docs/Configuration/yaml-files.md
@@ -703,7 +703,7 @@ org_settings:
### integrations
-The `integrations` section lets you configure your Google Calendar, Jira, and Zendesk. After configuration, you can enable [automations](https://fleetdm.com/docs/using-fleet/automations) like calendar event and ticket creation for failing policies. Currently, enabling ticket creation is only available using Fleet's UI or [API](https://fleetdm.com/docs/rest-api/rest-api) (YAML files coming soon).
+The `integrations` section lets you configure your Google Calendar, Conditional Access (for hosts in "No team"), Jira, and Zendesk. After configuration, you can enable [automations](https://fleetdm.com/docs/using-fleet/automations) like calendar event and ticket creation for failing policies. Currently, enabling ticket creation is only available using Fleet's UI or [API](https://fleetdm.com/docs/rest-api/rest-api) (YAML files coming soon).
In addition, you can configure your certificate authorities (CA) to help your end users connect to Wi-Fi. Learn more about certificate authorities in Fleet [here](https://fleetdm.com/guides/certificate-authorities).
@@ -714,6 +714,7 @@ In addition, you can configure your certificate authorities (CA) to help your en
```yaml
org_settings:
integrations:
+ conditional_access_enabled: true
google_calendar:
- api_key_json: $GOOGLE_CALENDAR_API_KEY_JSON
domain: fleetdm.com
diff --git a/docs/Contributing/reference/api-for-contributors.md b/docs/Contributing/reference/api-for-contributors.md
index fe8521a3a6..c534e75fdc 100644
--- a/docs/Contributing/reference/api-for-contributors.md
+++ b/docs/Contributing/reference/api-for-contributors.md
@@ -4726,7 +4726,7 @@ None.
```json
{
- "admin_consented": false
+ "configuration_completed": false
}
```
diff --git a/docs/Contributing/reference/audit-logs.md b/docs/Contributing/reference/audit-logs.md
index b2c417dd51..ac030ac1fc 100644
--- a/docs/Contributing/reference/audit-logs.md
+++ b/docs/Contributing/reference/audit-logs.md
@@ -1803,6 +1803,52 @@ This activity contains the following fields:
}
```
+## added_conditional_access_integration_microsoft
+
+Generated when Microsoft Entra is connected for conditonal access.
+
+This activity does not contain any detail fields.
+
+## deleted_conditional_access_integration_microsoft
+
+Generated when Microsoft Entra is integration is disconnected.
+
+This activity does not contain any detail fields.
+
+## enabled_conditional_access_automations
+
+Generated when conditional access automations are enabled for a team.
+
+This activity contains the following field:
+- "team_id": The ID of the team ("null" for "No team").
+- "team_name": The name of the team (empty for "No team").
+
+#### Example
+
+```json
+{
+ "team_id": 5,
+ "team_name": "Workstations"
+}
+```
+
+## disabled_conditional_access_automations
+
+Generated when conditional access automations are disabled for a team.
+
+This activity contains the following field:
+- "team_id": The ID of the team (`null` for "No team").
+- "team_name": The name of the team (empty for "No team").
+
+#### Example
+
+```json
+{
+ "team_id": 5,
+ "team_name": "Workstations"
+}
+```
+
diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go
index 678ce2eb0d..c7d4f590d3 100644
--- a/ee/server/service/mdm_external_test.go
+++ b/ee/server/service/mdm_external_test.go
@@ -97,6 +97,7 @@ func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Ser
nil,
nil,
nil,
+ nil,
)
if err != nil {
panic(err)
@@ -266,6 +267,9 @@ func TestGetOrCreatePreassignTeam(t *testing.T) {
ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) {
return 0, nil
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return nil, &eeservice.NotFoundError{}
+ }
}
authzCtx := &authz_ctx.AuthorizationContext{}
diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go
index f2b1d2b9ff..533fa7dbce 100644
--- a/ee/server/service/teams.go
+++ b/ee/server/service/teams.go
@@ -166,6 +166,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
windowsUpdatesUpdated bool
macOSDiskEncryptionUpdated bool
macOSEnableEndUserAuthUpdated bool
+ conditionalAccessUpdated bool
)
if payload.MDM != nil {
if payload.MDM.MacOSUpdates != nil {
@@ -254,7 +255,8 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
team.Config.Integrations.Jira = payload.Integrations.Jira
team.Config.Integrations.Zendesk = payload.Integrations.Zendesk
}
- // Only update the calendar integration if it's not nil
+
+ // Only update the calendar integration if it's not nil.
if payload.Integrations.GoogleCalendar != nil {
invalid := &fleet.InvalidArgumentError{}
_ = svc.validateTeamCalendarIntegrations(payload.Integrations.GoogleCalendar, appCfg, false, invalid)
@@ -263,6 +265,19 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
}
team.Config.Integrations.GoogleCalendar = payload.Integrations.GoogleCalendar
}
+
+ // Only update conditional_access_enabled if it's not nil.
+ if payload.Integrations.ConditionalAccessEnabled.Set {
+ if err := fleet.ValidateConditionalAccessIntegration(ctx,
+ svc,
+ team.Config.Integrations.ConditionalAccessEnabled.Value,
+ payload.Integrations.ConditionalAccessEnabled.Value,
+ ); err != nil {
+ return nil, ctxerr.Wrap(ctx, err)
+ }
+ conditionalAccessUpdated = team.Config.Integrations.ConditionalAccessEnabled.Value != payload.Integrations.ConditionalAccessEnabled.Value
+ team.Config.Integrations.ConditionalAccessEnabled = payload.Integrations.ConditionalAccessEnabled
+ }
}
if payload.WebhookSettings != nil || payload.Integrations != nil {
@@ -399,6 +414,32 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
return nil, ctxerr.Wrap(ctx, err, "update macos setup enable end user auth")
}
}
+ // Create activity if conditional access was enabled or disabled for the team.
+ if conditionalAccessUpdated {
+ if team.Config.Integrations.ConditionalAccessEnabled.Value {
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeEnabledConditionalAccessAutomations{
+ TeamID: &team.ID,
+ TeamName: team.Name,
+ },
+ ); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "create activity for enabling conditional access")
+ }
+ } else {
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeDisabledConditionalAccessAutomations{
+ TeamID: &team.ID,
+ TeamName: team.Name,
+ },
+ ); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "create activity for disabling conditional access")
+ }
+ }
+ }
return team, err
}
@@ -1078,6 +1119,18 @@ func (svc *Service) createTeamFromSpec(
}
}
+ var conditionalAccessEnabled optjson.Bool
+ if spec.Integrations.ConditionalAccessEnabled != nil {
+ if err := fleet.ValidateConditionalAccessIntegration(ctx,
+ svc,
+ false,
+ *spec.Integrations.ConditionalAccessEnabled,
+ ); err != nil {
+ return nil, ctxerr.Wrap(ctx, err)
+ }
+ conditionalAccessEnabled = optjson.SetBool(*spec.Integrations.ConditionalAccessEnabled)
+ }
+
if dryRun {
for _, secret := range secrets {
available, err := svc.ds.IsEnrollSecretAvailable(ctx, secret.Secret, true, nil)
@@ -1118,7 +1171,8 @@ func (svc *Service) createTeamFromSpec(
HostStatusWebhook: hostStatusWebhook,
},
Integrations: fleet.TeamIntegrations{
- GoogleCalendar: spec.Integrations.GoogleCalendar,
+ GoogleCalendar: spec.Integrations.GoogleCalendar,
+ ConditionalAccessEnabled: conditionalAccessEnabled,
},
Software: spec.Software,
},
@@ -1128,6 +1182,19 @@ func (svc *Service) createTeamFromSpec(
return nil, err
}
+ if conditionalAccessEnabled.Set && conditionalAccessEnabled.Value {
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeEnabledConditionalAccessAutomations{
+ TeamID: &tm.ID,
+ TeamName: tm.Name,
+ },
+ ); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "create activity for conditional access")
+ }
+ }
+
if enableDiskEncryption && appCfg.MDM.EnabledAndConfigured {
// TODO: Are we missing an activity or anything else for BitLocker here?
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil {
@@ -1336,6 +1403,18 @@ func (svc *Service) editTeamFromSpec(
team.Config.Integrations.GoogleCalendar = spec.Integrations.GoogleCalendar
}
+ oldConditionalAccessEnabled := team.Config.Integrations.ConditionalAccessEnabled.Value
+ if spec.Integrations.ConditionalAccessEnabled != nil {
+ if err := fleet.ValidateConditionalAccessIntegration(ctx,
+ svc,
+ team.Config.Integrations.ConditionalAccessEnabled.Value,
+ *spec.Integrations.ConditionalAccessEnabled,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err)
+ }
+ team.Config.Integrations.ConditionalAccessEnabled = optjson.SetBool(*spec.Integrations.ConditionalAccessEnabled)
+ }
+
if opts.DryRun {
for _, secret := range secrets {
available, err := svc.ds.IsEnrollSecretAvailable(ctx, secret.Secret, false, &team.ID)
@@ -1437,6 +1516,37 @@ func (svc *Service) editTeamFromSpec(
}
}
+ // Create activity if conditional access was enabled or disabled for the team.
+ if spec.Integrations.ConditionalAccessEnabled != nil {
+ if *spec.Integrations.ConditionalAccessEnabled {
+ if !oldConditionalAccessEnabled {
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeEnabledConditionalAccessAutomations{
+ TeamID: &team.ID,
+ TeamName: team.Name,
+ },
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "create activity for enabling conditional access")
+ }
+ }
+ } else {
+ if oldConditionalAccessEnabled {
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeDisabledConditionalAccessAutomations{
+ TeamID: &team.ID,
+ TeamName: team.Name,
+ },
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "create activity for disabling conditional access")
+ }
+ }
+ }
+ }
+
return nil
}
diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts
index a4630ef203..5e1c4751a7 100644
--- a/frontend/interfaces/activity.ts
+++ b/frontend/interfaces/activity.ts
@@ -116,8 +116,8 @@ export enum ActivityType {
CanceledUninstallSoftware = "canceled_uninstall_software",
EnabledAndroidMdm = "enabled_android_mdm",
DisabledAndroidMdm = "disabled_android_mdm",
- ConfiguredMSEntraConditionalAccess = "added_conditional_access_microsoft",
- DeletedMSEntraConditionalAccess = "deleted_conditional_access_microsoft",
+ ConfiguredMSEntraConditionalAccess = "added_conditional_access_integration_microsoft",
+ DeletedMSEntraConditionalAccess = "deleted_conditional_access_integration_microsoft",
// enable/disable above feature for a team
EnabledConditionalAccessAutomations = "enabled_conditional_access_automations",
DisabledConditionalAccessAutomations = "disabled_conditional_access_automations",
diff --git a/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx b/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx
index e604c2f011..e3d0d1c3b1 100644
--- a/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx
+++ b/frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx
@@ -51,7 +51,7 @@ const getIntegrationSettingsNavItems = (
},
];
- if (isManagedCloud && featureFlags.allowConditionalAccess === "true") {
+ if (isManagedCloud) {
items.push({
title: "Conditional access",
urlSection: "conditional-access",
diff --git a/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx b/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx
index fa9cf3255c..4a633c7228 100644
--- a/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx
+++ b/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx
@@ -27,10 +27,7 @@ const IntegrationsPage = ({
const { section } = params;
- if (
- section?.includes("conditional-access") &&
- (!isManagedCloud || featureFlags.allowConditionalAccess !== "true")
- ) {
+ if (section?.includes("conditional-access") && !isManagedCloud) {
router.push(paths.ADMIN_SETTINGS);
}
const navItems = getIntegrationSettingsNavItems(isManagedCloud);
diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
index 62252688b5..292f14bf14 100644
--- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
@@ -1047,7 +1047,9 @@ const ManagePolicyPage = ({
false;
const isConditionalAccessEnabled =
- teamConfig?.integrations.conditional_access_enabled ?? false;
+ (teamIdForApi === API_NO_TEAM_ID
+ ? globalConfig?.integrations.conditional_access_enabled
+ : teamConfig?.integrations.conditional_access_enabled) ?? false;
const getAutomationsDropdownOptions = (configPresent: boolean) => {
let disabledInstallTooltipContent: TooltipContent;
@@ -1125,10 +1127,7 @@ const ManagePolicyPage = ({
},
];
- if (
- globalConfigFromContext?.license.managed_cloud &&
- featureFlags.allowConditionalAccess === "true"
- ) {
+ if (globalConfigFromContext?.license.managed_cloud) {
options.push({
label: "Conditional access",
value: "conditional_access",
diff --git a/frontend/test/jest.config.ts b/frontend/test/jest.config.ts
index 2407e81b10..c587a302cd 100644
--- a/frontend/test/jest.config.ts
+++ b/frontend/test/jest.config.ts
@@ -45,7 +45,7 @@ const config: Config = {
transformIgnorePatterns: [`/node_modules/(?!(${esModules})/)`],
globals: {
TransformStream,
- featureFlags: { allowConditionalAccess: "true" },
+ featureFlags: {},
},
};
diff --git a/it-and-security/lib/macos/configuration-profiles/company-portal-single-signon-extension.mobileconfig b/it-and-security/lib/macos/configuration-profiles/company-portal-single-signon-extension.mobileconfig
new file mode 100644
index 0000000000..005468e65c
--- /dev/null
+++ b/it-and-security/lib/macos/configuration-profiles/company-portal-single-signon-extension.mobileconfig
@@ -0,0 +1,58 @@
+
+
+
+
+ PayloadContent
+
+
+ ExtensionData
+
+ Enable_SSO_On_All_ManagedApps
+ 1
+
+ ExtensionIdentifier
+ com.microsoft.CompanyPortalMac.ssoextension
+ Hosts
+
+ PayloadDisplayName
+ Company Portal Single Sign-On Extension
+ PayloadIdentifier
+ com.apple.extensiblesso.F82C8673-439F-4751-B562-42517A5FD990
+ PayloadType
+ com.apple.extensiblesso
+ PayloadUUID
+ F82C8673-439F-4751-B562-42517A5FD990
+ PayloadVersion
+ 1
+ TeamIdentifier
+ UBF8T346G9
+ Type
+ Redirect
+ URLs
+
+ https://login.microsoftonline.com
+ https://login.microsoft.com
+ https://sts.windows.net
+ https://login.partner.microsoftonline.cn
+ https://login.chinacloudapi.cn
+ https://login.microsoftonline.us
+ https://login-us.microsoftonline.com
+
+
+
+ PayloadDisplayName
+ Company Portal Single Sign-On Extension
+ PayloadIdentifier
+ com.fleetdm.D4DB9649-BA4A-4FA1-AA4F-D1CF606308B1
+ PayloadOrganization
+
+ PayloadRemovalDisallowed
+
+ PayloadType
+ Configuration
+ PayloadUUID
+ com.fleetdm.D4DB9649-BA4A-4FA1-AA4F-D1CF606308B1
+ PayloadVersion
+ 1
+
+
diff --git a/it-and-security/lib/macos/scripts/user-enroll-entra-company-portal.sh b/it-and-security/lib/macos/scripts/user-enroll-entra-company-portal.sh
new file mode 100644
index 0000000000..e40ace11d3
--- /dev/null
+++ b/it-and-security/lib/macos/scripts/user-enroll-entra-company-portal.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+open "/Applications/Company Portal.app" --args -r
diff --git a/security/status.md b/security/status.md
index d5579dca8d..be656784c6 100644
--- a/security/status.md
+++ b/security/status.md
@@ -89,7 +89,7 @@ Following is the vulnerability report of Fleet and its dependencies.
- **Author:** @lucasmrod
- **Status:** `not_affected`
- **Status notes:** The fleetctl tool is used by IT admins to generate packages so the vulnerable code cannot be controlled by attackers.
-- **Products:**: `fleetctl`,`pkg:golang/github.com/goreleaser/nfpm/v2`
+- **Products:**: `fleetctl`,`pkg:maven/commons-beanutils/commons-beanutils`
- **Justification:** `vulnerable_code_cannot_be_controlled_by_adversary`
- **Timestamp:** 2025-06-02 07:33:44
diff --git a/server/authz/policy.rego b/server/authz/policy.rego
index 81cdf229c1..48cf70b1c9 100644
--- a/server/authz/policy.rego
+++ b/server/authz/policy.rego
@@ -1071,3 +1071,13 @@ allow {
subject.global_role == [admin, maintainer][_]
action == [read, write][_]
}
+
+##
+# Microsoft Compliance Partner
+##
+# Global admins can configure Microsoft conditional access.
+allow {
+ object.type == "conditional_access_microsoft"
+ subject.global_role == admin
+ action == write
+}
diff --git a/server/config/config.go b/server/config/config.go
index d0e5911e18..8eaaa4cc32 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -589,36 +589,37 @@ type PackagingConfig struct {
// structs, Manager.addConfigs and Manager.LoadConfig should be
// updated to set and retrieve the configurations as appropriate.
type FleetConfig struct {
- Mysql MysqlConfig
- MysqlReadReplica MysqlConfig `yaml:"mysql_read_replica"`
- Redis RedisConfig
- Server ServerConfig
- Auth AuthConfig
- App AppConfig
- Session SessionConfig
- Osquery OsqueryConfig
- Activity ActivityConfig
- Logging LoggingConfig
- Firehose FirehoseConfig
- Kinesis KinesisConfig
- Lambda LambdaConfig
- S3 S3Config
- Email EmailConfig
- SES SESConfig
- PubSub PubSubConfig
- Filesystem FilesystemConfig
- Webhook WebhookConfig
- KafkaREST KafkaRESTConfig
- License LicenseConfig
- Vulnerabilities VulnerabilitiesConfig
- Upgrades UpgradesConfig
- Sentry SentryConfig
- GeoIP GeoIPConfig
- Prometheus PrometheusConfig
- Packaging PackagingConfig
- MDM MDMConfig
- Calendar CalendarConfig
- Partnerships PartnershipsConfig
+ Mysql MysqlConfig
+ MysqlReadReplica MysqlConfig `yaml:"mysql_read_replica"`
+ Redis RedisConfig
+ Server ServerConfig
+ Auth AuthConfig
+ App AppConfig
+ Session SessionConfig
+ Osquery OsqueryConfig
+ Activity ActivityConfig
+ Logging LoggingConfig
+ Firehose FirehoseConfig
+ Kinesis KinesisConfig
+ Lambda LambdaConfig
+ S3 S3Config
+ Email EmailConfig
+ SES SESConfig
+ PubSub PubSubConfig
+ Filesystem FilesystemConfig
+ Webhook WebhookConfig
+ KafkaREST KafkaRESTConfig
+ License LicenseConfig
+ Vulnerabilities VulnerabilitiesConfig
+ Upgrades UpgradesConfig
+ Sentry SentryConfig
+ GeoIP GeoIPConfig
+ Prometheus PrometheusConfig
+ Packaging PackagingConfig
+ MDM MDMConfig
+ Calendar CalendarConfig
+ Partnerships PartnershipsConfig
+ MicrosoftCompliancePartner MicrosoftCompliancePartnerConfig `yaml:"microsoft_compliance_partner"`
}
type PartnershipsConfig struct {
@@ -626,6 +627,21 @@ type PartnershipsConfig struct {
EnablePrimo bool `yaml:"enable_primo"`
}
+// MicrosoftCompliancePartnerConfig holds the server configuration for the "Conditional access" feature.
+// Currently only set on Cloud environments.
+type MicrosoftCompliancePartnerConfig struct {
+ // ProxyAPIKey is a shared key required to use the Microsoft Compliance Partner proxy API (fleetdm.com).
+ ProxyAPIKey string `yaml:"proxy_api_key"`
+ // ProxyURI is the URI of the Microsoft Compliance Partner proxy (for development/testing).
+ ProxyURI string `yaml:"proxy_uri"`
+}
+
+// IsSet returns if the compliance partner configuration is set.
+// Currently only set on Cloud environments.
+func (m MicrosoftCompliancePartnerConfig) IsSet() bool {
+ return m.ProxyAPIKey != ""
+}
+
type MDMConfig struct {
AppleAPNsCert string `yaml:"apple_apns_cert"`
AppleAPNsCertBytes string `yaml:"apple_apns_cert_bytes"`
@@ -1420,6 +1436,11 @@ func (man Manager) addConfigs() {
// Partnerships
man.addConfigBool("partnerships.enable_secureframe", false, "Point transparency URL at Secureframe landing page")
+
+ // Microsoft Compliance Partner
+ man.addConfigString("microsoft_compliance_partner.proxy_api_key", "", "Shared key required to use the Microsoft Compliance Partner proxy API")
+ man.addConfigString("microsoft_compliance_partner.proxy_uri", "https://fleetdm.com", "URI of the Microsoft Compliance Partner proxy (for development/testing)")
+
man.addConfigBool("partnerships.enable_primo", false, "Cosmetically disables team capabilities in the UI")
}
@@ -1705,6 +1726,10 @@ func (man Manager) LoadConfig() FleetConfig {
EnableSecureframe: man.getConfigBool("partnerships.enable_secureframe"),
EnablePrimo: man.getConfigBool("partnerships.enable_primo"),
},
+ MicrosoftCompliancePartner: MicrosoftCompliancePartnerConfig{
+ ProxyAPIKey: man.getConfigString("microsoft_compliance_partner.proxy_api_key"),
+ ProxyURI: man.getConfigString("microsoft_compliance_partner.proxy_uri"),
+ },
}
// ensure immediately that the async config is valid for all known tasks
diff --git a/server/datastore/mysql/conditional_access_microsoft.go b/server/datastore/mysql/conditional_access_microsoft.go
new file mode 100644
index 0000000000..642f076e7b
--- /dev/null
+++ b/server/datastore/mysql/conditional_access_microsoft.go
@@ -0,0 +1,148 @@
+package mysql
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "strings"
+
+ "github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/jmoiron/sqlx"
+)
+
+func (ds *Datastore) ConditionalAccessMicrosoftCreateIntegration(
+ ctx context.Context, tenantID string, proxyServerSecret string,
+) error {
+ return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
+ // Currently only one global integration is supported, thus we need to delete the existing
+ // one before creating a new one.
+ if _, err := tx.ExecContext(ctx,
+ `DELETE FROM microsoft_compliance_partner_integrations;`,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting microsoft_compliance_partner_integrations")
+ }
+ if _, err := tx.ExecContext(ctx,
+ `INSERT INTO microsoft_compliance_partner_integrations (tenant_id, proxy_server_secret) VALUES (?, ?);`,
+ tenantID, proxyServerSecret,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "inserting new microsoft_compliance_partner_integrations")
+ }
+ return nil
+ })
+}
+
+func (ds *Datastore) ConditionalAccessMicrosoftMarkSetupDone(ctx context.Context) error {
+ // Currently only one global integration is supported.
+ if _, err := ds.writer(ctx).ExecContext(ctx,
+ `UPDATE microsoft_compliance_partner_integrations SET setup_done = true;`,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting microsoft_compliance_partner_integrations")
+ }
+ return nil
+}
+
+func (ds *Datastore) ConditionalAccessMicrosoftGet(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return getConditionalAccessMicrosoft(ctx, ds.reader(ctx))
+}
+
+func getConditionalAccessMicrosoft(ctx context.Context, q sqlx.QueryerContext) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ var integration fleet.ConditionalAccessMicrosoftIntegration
+ err := sqlx.GetContext(
+ ctx, q, &integration,
+ // Currently only one global integration is supported.
+ `SELECT tenant_id, proxy_server_secret, setup_done FROM microsoft_compliance_partner_integrations;`,
+ )
+ if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ctxerr.Wrap(ctx, notFound("MicrosoftCompliancePartnerIntegration"))
+ }
+ return nil, ctxerr.Wrap(ctx, err, "getting microsoft_compliance_partner_integrations")
+ }
+ return &integration, nil
+}
+
+func (ds *Datastore) ConditionalAccessMicrosoftDelete(ctx context.Context) error {
+ return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
+ // Currently only one global integration is supported.
+ if _, err := tx.ExecContext(ctx, `DELETE FROM microsoft_compliance_partner_integrations;`); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting microsoft_compliance_partner_integrations")
+ }
+ // Remove all last reported statuses.
+ if _, err := tx.ExecContext(ctx, `DELETE FROM microsoft_compliance_partner_host_statuses;`); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting microsoft_compliance_partner_host_statuses")
+ }
+ return nil
+ })
+}
+
+func (ds *Datastore) LoadHostConditionalAccessStatus(ctx context.Context, hostID uint) (*fleet.HostConditionalAccessStatus, error) {
+ var hostConditionalAccessStatus fleet.HostConditionalAccessStatus
+ if err := sqlx.GetContext(ctx,
+ ds.reader(ctx),
+ &hostConditionalAccessStatus,
+ `SELECT
+ mcphs.host_id, mcphs.device_id, mcphs.user_principal_name, mcphs.compliant, mcphs.created_at, mcphs.updated_at, mcphs.managed,
+ h.os_version, hdn.display_name
+ FROM microsoft_compliance_partner_host_statuses mcphs
+ JOIN host_display_names hdn ON hdn.host_id=mcphs.host_id
+ JOIN hosts h ON h.id=mcphs.host_id
+ WHERE mcphs.host_id = ?`,
+ hostID,
+ ); err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ctxerr.Wrap(ctx, notFound("HostConditionalAccessStatus").WithID(hostID))
+ }
+ }
+ hostConditionalAccessStatus.OSVersion = strings.TrimPrefix(hostConditionalAccessStatus.OSVersion, "macOS ")
+ return &hostConditionalAccessStatus, nil
+}
+
+func (ds *Datastore) CreateHostConditionalAccessStatus(ctx context.Context, hostID uint, deviceID string, userPrincipalName string) error {
+ // Most of the time this information won't change, so use the reader first.
+ var hostConditionalAccessStatus fleet.HostConditionalAccessStatus
+ err := sqlx.GetContext(ctx,
+ ds.reader(ctx),
+ &hostConditionalAccessStatus,
+ `SELECT device_id, user_principal_name FROM microsoft_compliance_partner_host_statuses WHERE host_id = ?`,
+ hostID,
+ )
+ switch {
+ case err == nil:
+ if deviceID == hostConditionalAccessStatus.DeviceID && userPrincipalName == hostConditionalAccessStatus.UserPrincipalName {
+ // Nothing to do, the Entra account on the device is still the same.
+ return nil
+ }
+ // If we got here it means the host's Entra data has changed, so we will override the host's status row.
+ case errors.Is(err, sql.ErrNoRows):
+ // OK, let's create one.
+ default:
+ return ctxerr.Wrap(ctx, err, "failed to get microsoft_compliance_partner_host_statuses")
+ }
+
+ // Create or override existing row for the host.
+ if _, err := ds.writer(ctx).ExecContext(ctx,
+ `INSERT INTO microsoft_compliance_partner_host_statuses
+ (host_id, device_id, user_principal_name)
+ VALUES (?, ?, ?)
+ ON DUPLICATE KEY UPDATE
+ device_id = VALUES(device_id),
+ user_principal_name = VALUES(user_principal_name),
+ managed = NULL,
+ compliant = NULL`,
+ hostID, deviceID, userPrincipalName,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "create host conditional access status")
+ }
+ return nil
+}
+
+func (ds *Datastore) SetHostConditionalAccessStatus(ctx context.Context, hostID uint, managed, compliant bool) error {
+ if _, err := ds.writer(ctx).ExecContext(ctx,
+ `UPDATE microsoft_compliance_partner_host_statuses SET managed = ?, compliant = ? WHERE host_id = ?;`,
+ managed, compliant, hostID,
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "update host conditional access status")
+ }
+ return nil
+}
diff --git a/server/datastore/mysql/conditional_access_microsoft_test.go b/server/datastore/mysql/conditional_access_microsoft_test.go
new file mode 100644
index 0000000000..23dd090ff9
--- /dev/null
+++ b/server/datastore/mysql/conditional_access_microsoft_test.go
@@ -0,0 +1,187 @@
+package mysql
+
+import (
+ "context"
+ "testing"
+
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/stretchr/testify/require"
+)
+
+func TestConditionalAccess(t *testing.T) {
+ ds := CreateMySQLDS(t)
+
+ cases := []struct {
+ name string
+ fn func(t *testing.T, ds *Datastore)
+ }{
+ {"Setup", testConditionalAccessSetup},
+ {"Hosts", testConditionalAccessHosts},
+ }
+
+ for _, c := range cases {
+ t.Helper()
+ t.Run(c.name, func(t *testing.T) {
+ defer TruncateTables(t, ds)
+
+ c.fn(t, ds)
+ })
+ }
+}
+
+func testConditionalAccessSetup(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ _, err := ds.ConditionalAccessMicrosoftGet(ctx)
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+
+ err = ds.ConditionalAccessMicrosoftCreateIntegration(ctx, "foobar", "insecure")
+ require.NoError(t, err)
+ ca, err := ds.ConditionalAccessMicrosoftGet(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, ca)
+ require.False(t, ca.SetupDone)
+ require.Equal(t, "insecure", ca.ProxyServerSecret)
+ require.Equal(t, "foobar", ca.TenantID)
+
+ // ConditionalAccessMicrosoftCreateIntegration replaces the existing one.
+ err = ds.ConditionalAccessMicrosoftCreateIntegration(ctx, "foobar2", "insecure2")
+ require.NoError(t, err)
+ ca, err = ds.ConditionalAccessMicrosoftGet(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, ca)
+ require.False(t, ca.SetupDone)
+ require.Equal(t, "insecure2", ca.ProxyServerSecret)
+ require.Equal(t, "foobar2", ca.TenantID)
+
+ err = ds.ConditionalAccessMicrosoftMarkSetupDone(ctx)
+ require.NoError(t, err)
+
+ ca, err = ds.ConditionalAccessMicrosoftGet(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, ca)
+ require.True(t, ca.SetupDone)
+ require.Equal(t, "insecure2", ca.ProxyServerSecret)
+ require.Equal(t, "foobar2", ca.TenantID)
+
+ err = ds.ConditionalAccessMicrosoftDelete(ctx)
+ require.NoError(t, err)
+
+ _, err = ds.ConditionalAccessMicrosoftGet(ctx)
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+
+ // Create a new one after deleting the existing.
+ err = ds.ConditionalAccessMicrosoftCreateIntegration(ctx, "foobar3", "insecure3")
+ require.NoError(t, err)
+ ca, err = ds.ConditionalAccessMicrosoftGet(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, ca)
+ require.False(t, ca.SetupDone)
+ require.Equal(t, "insecure3", ca.ProxyServerSecret)
+ require.Equal(t, "foobar3", ca.TenantID)
+}
+
+func testConditionalAccessHosts(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ err := ds.ConditionalAccessMicrosoftCreateIntegration(ctx, "foobar", "insecure")
+ require.NoError(t, err)
+
+ // Test with non-existent host.
+ _, err = ds.LoadHostConditionalAccessStatus(ctx, 999_999)
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+
+ noTeamHost := newTestHostWithPlatform(t, ds, "host1", "darwin", nil)
+
+ // Test with an existent host but no status yet.
+ _, err = ds.LoadHostConditionalAccessStatus(ctx, noTeamHost.ID)
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+
+ // Nothing happens if the host doesn't have an entry yet.
+ err = ds.SetHostConditionalAccessStatus(ctx, noTeamHost.ID, false, false)
+ require.NoError(t, err)
+ _, err = ds.LoadHostConditionalAccessStatus(ctx, noTeamHost.ID)
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+
+ err = ds.CreateHostConditionalAccessStatus(ctx, noTeamHost.ID, "entraDeviceID", "foobar@example.onmicrosoft.com")
+ require.NoError(t, err)
+
+ s, err := ds.LoadHostConditionalAccessStatus(ctx, noTeamHost.ID)
+ require.NoError(t, err)
+ require.Equal(t, noTeamHost.ID, s.HostID)
+ require.Equal(t, "entraDeviceID", s.DeviceID)
+ require.Equal(t, "foobar@example.onmicrosoft.com", s.UserPrincipalName)
+ require.Equal(t, "host1", s.DisplayName)
+ require.Equal(t, "15.4.1", s.OSVersion)
+ require.NotZero(t, s.CreatedAt)
+ require.NotZero(t, s.UpdatedAt)
+ // When the status entry is created, these are not set yet.
+ // These values are updated during detail query ingestion.
+ require.Nil(t, s.Managed)
+ require.Nil(t, s.Compliant)
+
+ // Execute with same values should do nothing.
+ err = ds.CreateHostConditionalAccessStatus(ctx, noTeamHost.ID, "entraDeviceID", "foobar@example.onmicrosoft.com")
+ require.NoError(t, err)
+ s, err = ds.LoadHostConditionalAccessStatus(ctx, noTeamHost.ID)
+ require.NoError(t, err)
+ require.Equal(t, noTeamHost.ID, s.HostID)
+ require.Equal(t, "entraDeviceID", s.DeviceID)
+ require.Equal(t, "foobar@example.onmicrosoft.com", s.UserPrincipalName)
+ require.Equal(t, "host1", s.DisplayName)
+ require.Equal(t, "15.4.1", s.OSVersion)
+ require.NotZero(t, s.CreatedAt)
+ require.NotZero(t, s.UpdatedAt)
+ // These values are updated during detail query ingestion.
+ require.Nil(t, s.Managed)
+ require.Nil(t, s.Compliant)
+
+ err = ds.SetHostConditionalAccessStatus(ctx, noTeamHost.ID, true, false)
+ require.NoError(t, err)
+
+ s, err = ds.LoadHostConditionalAccessStatus(ctx, noTeamHost.ID)
+ require.NoError(t, err)
+ require.Equal(t, noTeamHost.ID, s.HostID)
+ require.Equal(t, "entraDeviceID", s.DeviceID)
+ require.Equal(t, "foobar@example.onmicrosoft.com", s.UserPrincipalName)
+ require.Equal(t, "host1", s.DisplayName)
+ require.Equal(t, "15.4.1", s.OSVersion)
+ require.NotZero(t, s.CreatedAt)
+ require.NotZero(t, s.UpdatedAt)
+ require.NotNil(t, s.Managed)
+ require.True(t, *s.Managed)
+ require.NotNil(t, s.Compliant)
+ require.False(t, *s.Compliant)
+
+ err = ds.SetHostConditionalAccessStatus(ctx, noTeamHost.ID, false, true)
+ require.NoError(t, err)
+
+ s, err = ds.LoadHostConditionalAccessStatus(ctx, noTeamHost.ID)
+ require.NoError(t, err)
+ require.NotNil(t, s.Managed)
+ require.False(t, *s.Managed)
+ require.NotNil(t, s.Compliant)
+ require.True(t, *s.Compliant)
+
+ // Simulate a device changing its device ID and user principal name
+ // (e.g. log out from Entra on device and log in again).
+ // We should update its data and clear its statuses.
+ err = ds.CreateHostConditionalAccessStatus(ctx, noTeamHost.ID, "entraDeviceID2", "foobar2@example.onmicrosoft.com")
+ require.NoError(t, err)
+ s, err = ds.LoadHostConditionalAccessStatus(ctx, noTeamHost.ID)
+ require.NoError(t, err)
+ require.Equal(t, noTeamHost.ID, s.HostID)
+ require.Equal(t, "entraDeviceID2", s.DeviceID)
+ require.Equal(t, "foobar2@example.onmicrosoft.com", s.UserPrincipalName)
+ require.Equal(t, "host1", s.DisplayName)
+ require.Equal(t, "15.4.1", s.OSVersion)
+ require.NotZero(t, s.CreatedAt)
+ require.NotZero(t, s.UpdatedAt)
+ require.Nil(t, s.Managed)
+ require.Nil(t, s.Compliant)
+}
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 7d7f0f618b..7a29ab51ff 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -547,6 +547,7 @@ var hostRefs = []string{
"host_scim_user",
"batch_script_execution_host_results",
"host_mdm_commands",
+ "microsoft_compliance_partner_host_statuses",
}
// NOTE: The following tables are explicity excluded from hostRefs list and accordingly are not
@@ -3000,6 +3001,9 @@ func (ds *Datastore) AddHostsToTeam(ctx context.Context, teamID *uint, hostIDs [
if err := cleanupQueryResultsOnTeamChange(ctx, tx, hostIDsBatch); err != nil {
return ctxerr.Wrap(ctx, err, "AddHostsToTeam delete query results")
}
+ if err := cleanupConditionalAccessOnTeamChange(ctx, tx, hostIDsBatch); err != nil {
+ return ctxerr.Wrap(ctx, err, "AddHostsToTeam delete conditional access")
+ }
query, args, err := sqlx.In(`UPDATE hosts SET team_id = ? WHERE id IN (?)`, teamID, hostIDsBatch)
if err != nil {
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index 3e1f8d69ef..32f672ce74 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -7469,6 +7469,9 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
_, err = ds.BatchExecuteScript(ctx, nil, script.ID, []uint{host.ID})
require.NoError(t, err)
+ err = ds.CreateHostConditionalAccessStatus(ctx, host.ID, "entraDeviceID", "userPrincipalName")
+ require.NoError(t, err)
+
// Check there's an entry for the host in all the associated tables.
for _, hostRef := range hostRefs {
var ok bool
diff --git a/server/datastore/mysql/migrations/tables/20250609120000_MicrosoftCompliancePartner.go b/server/datastore/mysql/migrations/tables/20250609120000_MicrosoftCompliancePartner.go
new file mode 100644
index 0000000000..2da008a515
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20250609120000_MicrosoftCompliancePartner.go
@@ -0,0 +1,58 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20250609120000, Down_20250609120000)
+}
+
+func Up_20250609120000(tx *sql.Tx) error {
+ // microsoft_compliance_partner_integrations stores the Microsoft Compliance Partner integrations.
+ // On the first version this table will only contain one row (one tenant supported for all devices in Fleet).
+ if _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS microsoft_compliance_partner_integrations (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+
+ tenant_id VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ proxy_server_secret VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ setup_done BOOLEAN NOT NULL DEFAULT FALSE,
+
+ created_at DATETIME(6) NULL DEFAULT NOW(6),
+ updated_at DATETIME(6) NULL DEFAULT NOW(6) ON UPDATE NOW(6),
+
+ UNIQUE KEY idx_microsoft_compliance_partner_tenant_id (tenant_id)
+ )`); err != nil {
+ return fmt.Errorf("failed to create microsoft_compliance_partner table: %w", err)
+ }
+
+ // microsoft_compliance_partner_host_statuses is used to track the "Device ID" and "User Principal Name"
+ // of the host in Entra and the last "managed" and "compliant" statuses reported to Microsoft Intune servers.
+ if _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS microsoft_compliance_partner_host_statuses (
+ host_id INT UNSIGNED NOT NULL PRIMARY KEY,
+
+ device_id VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ user_principal_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+
+ managed BOOLEAN NULL,
+ compliant BOOLEAN NULL,
+
+ created_at DATETIME(6) NULL DEFAULT NOW(6),
+ updated_at DATETIME(6) NULL DEFAULT NOW(6) ON UPDATE NOW(6)
+ )`); err != nil {
+ return fmt.Errorf("failed to create microsoft_compliance_partner_host_statuses table: %w", err)
+ }
+
+ // Adding a new field to policies to enable/disable them for conditional access.
+ _, err := tx.Exec(`ALTER TABLE policies ADD COLUMN conditional_access_enabled TINYINT(1) UNSIGNED NOT NULL DEFAULT '0'`)
+ if err != nil {
+ return fmt.Errorf("failed to add conditional_access_enabled to policies: %w", err)
+ }
+
+ return nil
+}
+
+func Down_20250609120000(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go
index 368caf89dc..6baa194031 100644
--- a/server/datastore/mysql/policies.go
+++ b/server/datastore/mysql/policies.go
@@ -25,7 +25,7 @@ const policyCols = `
p.id, p.team_id, p.resolution, p.name, p.query, p.description,
p.author_id, p.platforms, p.created_at, p.updated_at, p.critical,
p.calendar_events_enabled, p.software_installer_id, p.script_id,
- p.vpp_apps_teams_id
+ p.vpp_apps_teams_id, p.conditional_access_enabled
`
var (
@@ -325,11 +325,14 @@ func savePolicy(ctx context.Context, db sqlx.ExtContext, logger kitlog.Logger, p
p.Name = norm.NFC.String(p.Name)
updateStmt := `
UPDATE policies
- SET name = ?, query = ?, description = ?, resolution = ?, platforms = ?, critical = ?, calendar_events_enabled = ?, software_installer_id = ?, script_id = ?, vpp_apps_teams_id = ?, checksum = ` + policiesChecksumComputedColumn() + `
+ SET name = ?, query = ?, description = ?, resolution = ?,
+ platforms = ?, critical = ?, calendar_events_enabled = ?,
+ software_installer_id = ?, script_id = ?, vpp_apps_teams_id = ?,
+ conditional_access_enabled = ?, checksum = ` + policiesChecksumComputedColumn() + `
WHERE id = ?
`
result, err := db.ExecContext(
- ctx, updateStmt, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.Critical, p.CalendarEventsEnabled, p.SoftwareInstallerID, p.ScriptID, p.VPPAppsTeamsID, p.ID,
+ ctx, updateStmt, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.Critical, p.CalendarEventsEnabled, p.SoftwareInstallerID, p.ScriptID, p.VPPAppsTeamsID, p.ConditionalAccessEnabled, p.ID,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "updating policy")
@@ -965,11 +968,15 @@ func newTeamPolicy(ctx context.Context, db sqlx.ExtContext, teamID uint, authorI
res, err := db.ExecContext(ctx,
fmt.Sprintf(
- `INSERT INTO policies (name, query, description, team_id, resolution, author_id, platforms, critical, calendar_events_enabled, software_installer_id, script_id, vpp_apps_teams_id, checksum) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)`,
+ `INSERT INTO policies (
+ name, query, description, team_id, resolution, author_id,
+ platforms, critical, calendar_events_enabled, software_installer_id,
+ script_id, vpp_apps_teams_id, conditional_access_enabled, checksum
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)`,
policiesChecksumComputedColumn(),
),
nameUnicode, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platform, args.Critical,
- args.CalendarEventsEnabled, args.SoftwareInstallerID, args.ScriptID, args.VPPAppsTeamsID,
+ args.CalendarEventsEnabled, args.SoftwareInstallerID, args.ScriptID, args.VPPAppsTeamsID, args.ConditionalAccessEnabled,
)
switch {
case err == nil:
@@ -1216,8 +1223,9 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
software_installer_id,
vpp_apps_teams_id,
script_id,
+ conditional_access_enabled,
checksum
- ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)
ON DUPLICATE KEY UPDATE
query = VALUES(query),
description = VALUES(description),
@@ -1228,7 +1236,8 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
calendar_events_enabled = VALUES(calendar_events_enabled),
software_installer_id = VALUES(software_installer_id),
vpp_apps_teams_id = VALUES(vpp_apps_teams_id),
- script_id = VALUES(script_id)
+ script_id = VALUES(script_id),
+ conditional_access_enabled = VALUES(conditional_access_enabled)
`, policiesChecksumComputedColumn(),
)
for teamID, teamPolicySpecs := range teamIDToPolicies {
@@ -1252,7 +1261,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
ctx,
query,
spec.Name, spec.Query, spec.Description, authorID, spec.Resolution, teamID, spec.Platform, spec.Critical,
- spec.CalendarEventsEnabled, softwareInstallerID, vppAppsTeamsID, scriptID,
+ spec.CalendarEventsEnabled, softwareInstallerID, vppAppsTeamsID, scriptID, spec.ConditionalAccessEnabled,
)
if err != nil {
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
@@ -1462,6 +1471,18 @@ func cleanupQueryResultsOnTeamChange(ctx context.Context, tx sqlx.ExtContext, ho
return nil
}
+func cleanupConditionalAccessOnTeamChange(ctx context.Context, tx sqlx.ExtContext, hostIDs []uint) error {
+ const cleanupQuery = `DELETE FROM microsoft_compliance_partner_host_statuses WHERE host_id IN (?)`
+ query, args, err := sqlx.In(cleanupQuery, hostIDs)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "build cleanup conditional access")
+ }
+ if _, err := tx.ExecContext(ctx, query, args...); err != nil {
+ return ctxerr.Wrap(ctx, err, "exec cleanup query conditional access")
+ }
+ return nil
+}
+
func cleanupPolicyMembershipOnPolicyUpdate(
ctx context.Context, queryerContext sqlx.QueryerContext, db sqlx.ExecerContext, policyID uint, platforms string,
) error {
@@ -1986,6 +2007,17 @@ func (ds *Datastore) GetCalendarPolicies(ctx context.Context, teamID uint) ([]fl
return policies, nil
}
+func (ds *Datastore) GetPoliciesForConditionalAccess(ctx context.Context, teamID uint) ([]uint, error) {
+ // Currently, the "Conditional access" feature is for macOS hosts only.
+ query := `SELECT id FROM policies WHERE team_id = ? AND conditional_access_enabled AND (platforms LIKE '%darwin%' OR platforms = '');`
+ var policyIDs []uint
+ err := sqlx.SelectContext(ctx, ds.reader(ctx), &policyIDs, query, teamID)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "get policies for conditional access")
+ }
+ return policyIDs, nil
+}
+
func (ds *Datastore) GetPoliciesWithAssociatedInstaller(ctx context.Context, teamID uint, policyIDs []uint) ([]fleet.PolicySoftwareInstallerData, error) {
if len(policyIDs) == 0 {
return nil, nil
diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go
index fa908e422a..b41c309eb3 100644
--- a/server/datastore/mysql/policies_test.go
+++ b/server/datastore/mysql/policies_test.go
@@ -1120,6 +1120,8 @@ func newTestHostWithPlatform(t *testing.T, ds *Datastore, hostname, platform str
UUID: uuid.NewString(),
Hostname: hostname,
Platform: platform,
+ OSVersion: "15.4.1",
+ ComputerName: hostname,
})
require.NoError(t, err)
if teamID != nil {
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index da7d73d14b..3dd35de031 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -96,7 +96,7 @@ CREATE TABLE `app_config_json` (
PRIMARY KEY (`id`)
) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"manual_agent_install\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"android_enabled_and_configured\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"gitops\": {\"repository_url\": \"\", \"gitops_mode_enabled\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null, \"custom_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
+INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"manual_agent_install\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"android_enabled_and_configured\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"gitops\": {\"repository_url\": \"\", \"gitops_mode_enabled\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null, \"custom_scep_proxy\": null, \"conditional_access_enabled\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `batch_script_execution_host_results` (
@@ -1329,15 +1329,41 @@ CREATE TABLE `mdm_windows_enrollments` (
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `microsoft_compliance_partner_host_statuses` (
+ `host_id` int unsigned NOT NULL,
+ `device_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_principal_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ `managed` tinyint(1) DEFAULT NULL,
+ `compliant` tinyint(1) DEFAULT NULL,
+ `created_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6),
+ `updated_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+ PRIMARY KEY (`host_id`)
+) 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 `microsoft_compliance_partner_integrations` (
+ `id` int unsigned NOT NULL AUTO_INCREMENT,
+ `tenant_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ `proxy_server_secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
+ `setup_done` tinyint(1) NOT NULL DEFAULT '0',
+ `created_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6),
+ `updated_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_microsoft_compliance_partner_tenant_id` (`tenant_id`)
+) 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 `migration_status_tables` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`version_id` bigint NOT NULL,
`is_applied` tinyint(1) NOT NULL,
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
-) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=392 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=393 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,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250326161930,1,'2020-01-01 01:01:01'),(370,20250326161931,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'),(372,20250331154206,1,'2020-01-01 01:01:01'),(373,20250401155831,1,'2020-01-01 01:01:01'),(374,20250408133233,1,'2020-01-01 01:01:01'),(375,20250410104321,1,'2020-01-01 01:01:01'),(376,20250421085116,1,'2020-01-01 01:01:01'),(377,20250422095806,1,'2020-01-01 01:01:01'),(378,20250424153059,1,'2020-01-01 01:01:01'),(379,20250430103833,1,'2020-01-01 01:01:01'),(380,20250430112622,1,'2020-01-01 01:01:01'),(381,20250501162727,1,'2020-01-01 01:01:01'),(382,20250502154517,1,'2020-01-01 01:01:01'),(383,20250502222222,1,'2020-01-01 01:01:01'),(384,20250507170845,1,'2020-01-01 01:01:01'),(385,20250513162912,1,'2020-01-01 01:01:01'),(386,20250519161614,1,'2020-01-01 01:01:01'),(387,20250520153848,1,'2020-01-01 01:01:01'),(388,20250528115932,1,'2020-01-01 01:01:01'),(389,20250529102706,1,'2020-01-01 01:01:01'),(390,20250603105558,1,'2020-01-01 01:01:01'),(391,20250609102714,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,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01'),(361,20250226153445,1,'2020-01-01 01:01:01'),(362,20250304162702,1,'2020-01-01 01:01:01'),(363,20250306144233,1,'2020-01-01 01:01:01'),(364,20250313163430,1,'2020-01-01 01:01:01'),(365,20250317130944,1,'2020-01-01 01:01:01'),(366,20250318165922,1,'2020-01-01 01:01:01'),(367,20250320132525,1,'2020-01-01 01:01:01'),(368,20250320200000,1,'2020-01-01 01:01:01'),(369,20250326161930,1,'2020-01-01 01:01:01'),(370,20250326161931,1,'2020-01-01 01:01:01'),(371,20250331042354,1,'2020-01-01 01:01:01'),(372,20250331154206,1,'2020-01-01 01:01:01'),(373,20250401155831,1,'2020-01-01 01:01:01'),(374,20250408133233,1,'2020-01-01 01:01:01'),(375,20250410104321,1,'2020-01-01 01:01:01'),(376,20250421085116,1,'2020-01-01 01:01:01'),(377,20250422095806,1,'2020-01-01 01:01:01'),(378,20250424153059,1,'2020-01-01 01:01:01'),(379,20250430103833,1,'2020-01-01 01:01:01'),(380,20250430112622,1,'2020-01-01 01:01:01'),(381,20250501162727,1,'2020-01-01 01:01:01'),(382,20250502154517,1,'2020-01-01 01:01:01'),(383,20250502222222,1,'2020-01-01 01:01:01'),(384,20250507170845,1,'2020-01-01 01:01:01'),(385,20250513162912,1,'2020-01-01 01:01:01'),(386,20250519161614,1,'2020-01-01 01:01:01'),(387,20250520153848,1,'2020-01-01 01:01:01'),(388,20250528115932,1,'2020-01-01 01:01:01'),(389,20250529102706,1,'2020-01-01 01:01:01'),(390,20250603105558,1,'2020-01-01 01:01:01'),(391,20250609102714,1,'2020-01-01 01:01:01'),(392,20250609120000,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` (
@@ -1693,6 +1719,7 @@ CREATE TABLE `policies` (
`software_installer_id` int unsigned DEFAULT NULL,
`script_id` int unsigned DEFAULT NULL,
`vpp_apps_teams_id` int unsigned DEFAULT NULL,
+ `conditional_access_enabled` tinyint unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_policies_checksum` (`checksum`),
KEY `idx_policies_author_id` (`author_id`),
diff --git a/server/fleet/activities.go b/server/fleet/activities.go
index 7585a7e749..c6a723ae70 100644
--- a/server/fleet/activities.go
+++ b/server/fleet/activities.go
@@ -206,6 +206,11 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeCanceledInstallSoftware{},
ActivityTypeCanceledUninstallSoftware{},
ActivityTypeCanceledInstallAppStoreApp{},
+
+ ActivityTypeAddedConditionalAccessIntegrationMicrosoft{},
+ ActivityTypeDeletedConditionalAccessIntegrationMicrosoft{},
+ ActivityTypeEnabledConditionalAccessAutomations{},
+ ActivityTypeDisabledConditionalAccessAutomations{},
}
type ActivityDetails interface {
@@ -2508,3 +2513,63 @@ func (a ActivityTypeRanScriptBatch) Documentation() (string, string, string) {
"host_count": 12
}`
}
+
+type ActivityTypeAddedConditionalAccessIntegrationMicrosoft struct{}
+
+func (a ActivityTypeAddedConditionalAccessIntegrationMicrosoft) ActivityName() string {
+ return "added_conditional_access_integration_microsoft"
+}
+
+func (a ActivityTypeAddedConditionalAccessIntegrationMicrosoft) Documentation() (string, string, string) {
+ return "Generated when Microsoft Entra is connected for conditonal access.",
+ "This activity does not contain any detail fields.", ""
+}
+
+type ActivityTypeDeletedConditionalAccessIntegrationMicrosoft struct{}
+
+func (a ActivityTypeDeletedConditionalAccessIntegrationMicrosoft) ActivityName() string {
+ return "deleted_conditional_access_integration_microsoft"
+}
+
+func (a ActivityTypeDeletedConditionalAccessIntegrationMicrosoft) Documentation() (string, string, string) {
+ return "Generated when Microsoft Entra is integration is disconnected.",
+ "This activity does not contain any detail fields.", ""
+}
+
+type ActivityTypeEnabledConditionalAccessAutomations struct {
+ TeamID *uint `json:"team_id"`
+ TeamName string `json:"team_name"`
+}
+
+func (a ActivityTypeEnabledConditionalAccessAutomations) ActivityName() string {
+ return "enabled_conditional_access_automations"
+}
+
+func (a ActivityTypeEnabledConditionalAccessAutomations) Documentation() (string, string, string) {
+ return "Generated when conditional access automations are enabled for a team.",
+ `This activity contains the following field:
+- "team_id": The ID of the team ("null" for "No team").
+- "team_name": The name of the team (empty for "No team").`, `{
+ "team_id": 5,
+ "team_name": "Workstations"
+}`
+}
+
+type ActivityTypeDisabledConditionalAccessAutomations struct {
+ TeamID *uint `json:"team_id"`
+ TeamName string `json:"team_name"`
+}
+
+func (a ActivityTypeDisabledConditionalAccessAutomations) ActivityName() string {
+ return "disabled_conditional_access_automations"
+}
+
+func (a ActivityTypeDisabledConditionalAccessAutomations) Documentation() (string, string, string) {
+ return "Generated when conditional access automations are disabled for a team.",
+ `This activity contains the following field:
+- "team_id": The ID of the team (` + "`null`" + ` for "No team").
+- "team_name": The name of the team (empty for "No team").`, `{
+ "team_id": 5,
+ "team_name": "Workstations"
+}`
+}
diff --git a/server/fleet/app.go b/server/fleet/app.go
index 1810f0cf78..8b65d770a7 100644
--- a/server/fleet/app.go
+++ b/server/fleet/app.go
@@ -77,6 +77,15 @@ type SSOSettings struct {
EnableJITRoleSync bool `json:"enable_jit_role_sync"`
}
+// ConditionalAccessSettings holds the global settings for the "Conditional access" feature.
+type ConditionalAccessSettings struct {
+ // MicrosoftEntraTenantID is the Entra's tenant ID.
+ MicrosoftEntraTenantID string `json:"microsoft_entra_tenant_id"`
+ // MicrosoftEntraConnectionConfigured is true when the tenant has been configured
+ // for "Conditional access" on Entra and Fleet.
+ MicrosoftEntraConnectionConfigured bool `json:"microsoft_entra_connection_configured"`
+}
+
// SMTPSettings is part of the AppConfig which defines the wire representation
// of the app config endpoints
type SMTPSettings struct {
@@ -553,6 +562,7 @@ type AppConfig struct {
//
// This field is a pointer to avoid returning this information to non-global-admins.
SSOSettings *SSOSettings `json:"sso_settings,omitempty"`
+
// FleetDesktop holds settings for Fleet Desktop that can be changed via the API.
FleetDesktop FleetDesktopSettings `json:"fleet_desktop"`
@@ -1345,6 +1355,9 @@ type LicenseInfo struct {
Note string `json:"note,omitempty"`
// AllowDisableTelemetry allows specific customers to not send analytics
AllowDisableTelemetry bool `json:"allow_disable_telemetry,omitempty"`
+ // ManagedCloud indicates whether this Fleet instance is a cloud instance.
+ // Currently only used to display UI features only present on cloud instances.
+ ManagedCloud bool `json:"managed_cloud"`
}
func (l *LicenseInfo) IsPremium() bool {
diff --git a/server/fleet/conditional_access_microsoft.go b/server/fleet/conditional_access_microsoft.go
new file mode 100644
index 0000000000..01d6ad304b
--- /dev/null
+++ b/server/fleet/conditional_access_microsoft.go
@@ -0,0 +1,46 @@
+package fleet
+
+// ConditionalAccessMicrosoftIntegrations holds settings for a "Conditional access" integration.
+type ConditionalAccessMicrosoftIntegration struct {
+ // TenantID is the Entra's tenant ID.
+ TenantID string `db:"tenant_id"`
+ // ProxyServerSecret is the secret used to authenticate a Cloud instance.
+ ProxyServerSecret string `db:"proxy_server_secret"`
+ // SetupDone is true when the Entra admin has consented and the tenant has been provisioned.
+ SetupDone bool `db:"setup_done"`
+}
+
+// AuthzType implements authz.AuthzTyper.
+func (c *ConditionalAccessMicrosoftIntegration) AuthzType() string {
+ return "conditional_access_microsoft"
+}
+
+// HostConditionalAccessStatus holds "Conditional access" status for a host.
+type HostConditionalAccessStatus struct {
+ // HostID is the host's ID.
+ HostID uint `db:"host_id"`
+
+ // DeviceID is Entra's Device ID assigned when the device first logs in to Entra (obtained using a detail query).
+ DeviceID string `db:"device_id"`
+ // DeviceID is Entra's User Principal Name that logged in the device (obtained using a detail query).
+ UserPrincipalName string `db:"user_principal_name"`
+
+ // Managed holds the last "DeviceManagementState" reported to Entra.
+ // It is true if the host is MDM enrolled, false otherwise.
+ //
+ // This field is used to know if Fleet needs to update the status on Entra.
+ Managed *bool `db:"managed"`
+ // Compliant holds the last "complianceStatus" reported to Entra.
+ // It is true if all configured policies are passing.
+ //
+ // This field is used to know if Fleet needs to update the status on Entra.
+ Compliant *bool `db:"compliant"`
+
+ // DisplayName is the host's display name to reported to Entra.
+ DisplayName string `db:"display_name"`
+ // OSVersion is the host's OS version reported to Entra
+ OSVersion string `db:"os_version"`
+
+ // UpdateCreateTimestamps holds the timestamps of the entry.
+ UpdateCreateTimestamps
+}
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index 909d3dcf53..673dd56def 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -14,11 +14,10 @@ import (
"github.com/fleetdm/fleet/v4/server/health"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
+ "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage"
"github.com/jmoiron/sqlx"
-
- "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
)
type CarveStore interface {
@@ -743,6 +742,8 @@ type Datastore interface {
GetPoliciesWithAssociatedVPP(ctx context.Context, teamID uint, policyIDs []uint) ([]PolicyVPPData, error)
GetPoliciesWithAssociatedScript(ctx context.Context, teamID uint, policyIDs []uint) ([]PolicyScriptData, error)
GetCalendarPolicies(ctx context.Context, teamID uint) ([]PolicyCalendarData, error)
+ // GetPoliciesForConditionalAccess returns the team policies that are configured for "Conditional access".
+ GetPoliciesForConditionalAccess(ctx context.Context, teamID uint) ([]uint, error)
// Methods used for async processing of host policy query results.
AsyncBatchInsertPolicyMembership(ctx context.Context, batch []PolicyMembershipResult) error
@@ -2122,6 +2123,33 @@ type Datastore interface {
ScimLastRequest(ctx context.Context) (*ScimLastRequest, error)
// UpdateScimLastRequest updates the last SCIM request info
UpdateScimLastRequest(ctx context.Context, lastRequest *ScimLastRequest) error
+
+ // /////////////////////////////////////////////////////////////////////////////
+ // Microsoft Compliance Partner
+
+ // ConditionalAccessMicrosoftCreateIntegration creates the Conditional Access integration on the datastore.
+ // The integration is created as "not done".
+ // Currently only one integration can be configured, so this method replaces any existing integration.
+ ConditionalAccessMicrosoftCreateIntegration(ctx context.Context, tenantID, proxyServerSecret string) error
+ // ConditionalAccessMicrosoftGet returns the current Conditional Access integration.
+ // Returns a NotFoundError error if there's none.
+ ConditionalAccessMicrosoftGet(ctx context.Context) (*ConditionalAccessMicrosoftIntegration, error)
+ // ConditionalAccessMicrosoftMarkSetupDone marks the configuration as done on the datastore.
+ ConditionalAccessMicrosoftMarkSetupDone(ctx context.Context) error
+ // ConditionalAccessMicrosoftDelete deletes the integration from the datastore.
+ // It will also cleanup all recorded compliance status of all hosts from the datastore.
+ ConditionalAccessMicrosoftDelete(ctx context.Context) error
+ // LoadHostConditionalAccessStatus will load the current "Conditional Access" status of a host.
+ // The status holds Entra's "Device ID", "User Principal Name", and last reported "managed" and "compliant" status.
+ // Returns a NotFoundError error if there's no entry for the host.
+ LoadHostConditionalAccessStatus(ctx context.Context, hostID uint) (*HostConditionalAccessStatus, error)
+ // CreateHostConditionalAccessStatus creates the entry for the host on the datastore.
+ // This does not set the "managed" or "compliant" status yet, this just creates the entry needed with Entra information.
+ // If the host already has a different deviceID/userPrincipalName it will override them.
+ CreateHostConditionalAccessStatus(ctx context.Context, hostID uint, deviceID string, userPrincipalName string) error
+ // SetHostConditionalAccessStatus sets the "managed" and "compliant" statuses last set on Entra.
+ // It does nothing if the host doesn't have a status entry created with CreateHostConditionalAccessStatus yet.
+ SetHostConditionalAccessStatus(ctx context.Context, hostID uint, managed, compliant bool) error
}
type AndroidDatastore interface {
diff --git a/server/fleet/integrations.go b/server/fleet/integrations.go
index 8f107cbf0b..40ed449f51 100644
--- a/server/fleet/integrations.go
+++ b/server/fleet/integrations.go
@@ -19,6 +19,8 @@ type TeamIntegrations struct {
Jira []*TeamJiraIntegration `json:"jira"`
Zendesk []*TeamZendeskIntegration `json:"zendesk"`
GoogleCalendar *TeamGoogleCalendarIntegration `json:"google_calendar"`
+ // ConditionalAccessEnabled indicates whether the conditional access feature is enabled on this team.
+ ConditionalAccessEnabled optjson.Bool `json:"conditional_access_enabled,omitempty"`
}
// MatchWithIntegrations matches the team integrations to their corresponding
@@ -416,6 +418,46 @@ type Integrations struct {
// NDESSCEPProxy settings. In JSON, not specifying this field means keep current setting, null means clear settings.
NDESSCEPProxy optjson.Any[NDESSCEPProxyIntegration] `json:"ndes_scep_proxy"`
CustomSCEPProxy optjson.Slice[CustomSCEPProxyIntegration] `json:"custom_scep_proxy"`
+ // ConditionalAccessEnabled indicates whether conditional access is enabled/disabled for "No team".
+ ConditionalAccessEnabled optjson.Bool `json:"conditional_access_enabled"`
+}
+
+// ValidateConditionalAccessIntegration validates "Conditional access" can be enabled on a team/"No team".
+// It checks the global setup of the feature has been made.
+func ValidateConditionalAccessIntegration(
+ ctx context.Context,
+ g interface {
+ ConditionalAccessMicrosoftGet(context.Context) (*ConditionalAccessMicrosoftIntegration, error)
+ },
+ currentConditionalAccessEnabled bool,
+ newConditionalAccessEnabled bool,
+) error {
+ switch {
+ case currentConditionalAccessEnabled == newConditionalAccessEnabled:
+ // No change, mothing to do.
+ case currentConditionalAccessEnabled && !newConditionalAccessEnabled:
+ // Disabling feature on team/no-team, nothing to do.
+ case !currentConditionalAccessEnabled && newConditionalAccessEnabled:
+ // Enabling feature on team/no-team.
+ var settings *ConditionalAccessSettings
+ conditionalAccessIntegration, err := g.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ return fmt.Errorf("load conditional access microsoft: %w", err)
+ }
+ if conditionalAccessIntegration != nil {
+ settings = &ConditionalAccessSettings{
+ MicrosoftEntraTenantID: conditionalAccessIntegration.TenantID,
+ MicrosoftEntraConnectionConfigured: conditionalAccessIntegration.SetupDone,
+ }
+ }
+ if settings == nil || !settings.MicrosoftEntraConnectionConfigured {
+ return NewInvalidArgumentError(
+ "integrations.conditional_access_enabled",
+ "Couldn't enable because the integration isn't configured",
+ )
+ }
+ }
+ return nil
}
func ValidateEnabledActivitiesWebhook(webhook ActivitiesWebhookSettings, invalid *InvalidArgumentError) {
diff --git a/server/fleet/policies.go b/server/fleet/policies.go
index b662366863..8b71426f93 100644
--- a/server/fleet/policies.go
+++ b/server/fleet/policies.go
@@ -52,6 +52,10 @@ type PolicyPayload struct {
LabelsIncludeAny []string
// LabelsExcludeAny is a list of labels excluded from being targeted by this policy
LabelsExcludeAny []string
+ // ConditionalAccessEnabled indicates whether this is a policy used for Microsoft conditional access.
+ //
+ // Only applies to team policies.
+ ConditionalAccessEnabled bool
}
// NewTeamPolicyPayload holds data for team policy creation.
@@ -88,6 +92,8 @@ type NewTeamPolicyPayload struct {
LabelsIncludeAny []string
// LabelsExcludeAny is a list of labels excluded from being targeted by this policy
LabelsExcludeAny []string
+ // ConditionalAccessEnabled indicates whether this is a policy used for Microsoft conditional access.
+ ConditionalAccessEnabled bool
}
var (
@@ -190,6 +196,10 @@ type ModifyPolicyPayload struct {
LabelsIncludeAny []string `json:"labels_include_any"`
// LabelsExcludeAny is a list of labels excluded from being targeted by this policy
LabelsExcludeAny []string `json:"labels_exclude_any"`
+ // ConditionalAccessEnabled indicates whether this is a policy used for Microsoft conditional access.
+ //
+ // Only applies to team policies.
+ ConditionalAccessEnabled *bool `json:"conditional_access_enabled" premium:"true"`
}
// Verify verifies the policy payload is valid.
@@ -247,11 +257,19 @@ type PolicyData struct {
// LabelsExcludeAny is a list of labels excluded from being targeted by this policy
LabelsExcludeAny []LabelIdent `json:"labels_exclude_any,omitempty"`
+ // CalendarEventsEnabled indicates whether calendar events are enabled for the policy.
+ //
+ // Only applies to team policies.
CalendarEventsEnabled bool `json:"calendar_events_enabled" db:"calendar_events_enabled"`
SoftwareInstallerID *uint `json:"-" db:"software_installer_id"`
VPPAppsTeamsID *uint `json:"-" db:"vpp_apps_teams_id"`
ScriptID *uint `json:"-" db:"script_id"`
+ // ConditionalAccessEnabled indicates whether this is a policy used for Microsoft conditional access.
+ //
+ // Only applies to team policies.
+ ConditionalAccessEnabled bool `json:"conditional_access_enabled" db:"conditional_access_enabled"`
+
UpdateCreateTimestamps
}
@@ -364,6 +382,10 @@ type PolicySpec struct {
ScriptID *uint `json:"script_id"`
LabelsIncludeAny []string `json:"labels_include_any,omitempty"`
LabelsExcludeAny []string `json:"labels_exclude_any,omitempty"`
+ // ConditionalAccessEnabled indicates whether this is a policy used for Microsoft conditional access.
+ //
+ // Only applies to team policies.
+ ConditionalAccessEnabled bool `json:"conditional_access_enabled"`
}
// PolicySoftwareTitle contains software title data for policies.
diff --git a/server/fleet/service.go b/server/fleet/service.go
index 2862c59415..084192949a 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -1247,6 +1247,19 @@ type Service interface {
// ScimDetails returns the details of last access to Fleet's SCIM endpoints
ScimDetails(ctx context.Context) (ScimDetails, error)
+
+ // /////////////////////////////////////////////////////////////////////////////
+ // Microsoft Conditional Access
+
+ // ConditionalAccessMicrosoftCreateIntegration kicks-off the integration with Entra
+ // and returns the consent URL to redirect the admin to.
+ ConditionalAccessMicrosoftCreateIntegration(ctx context.Context, tenantID string) (adminConsentURL string, err error)
+ // ConditionalAccessMicrosoftGet returns the current (currently unique) integration.
+ ConditionalAccessMicrosoftGet(ctx context.Context) (*ConditionalAccessMicrosoftIntegration, error)
+ // ConditionalAccessMicrosoftConfirm finalizes the integration (marks integration as done).
+ ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, err error)
+ // ConditionalAccessMicrosoftDelete deletes the integration and deprovisions the tenant on Entra.
+ ConditionalAccessMicrosoftDelete(ctx context.Context) error
}
type KeyValueStore interface {
diff --git a/server/fleet/teams.go b/server/fleet/teams.go
index c4be41d77e..57c40d7e04 100644
--- a/server/fleet/teams.go
+++ b/server/fleet/teams.go
@@ -476,6 +476,8 @@ type TeamSpecWebhookSettings struct {
type TeamSpecIntegrations struct {
// If value is nil, we don't want to change the existing value.
GoogleCalendar *TeamGoogleCalendarIntegration `json:"google_calendar"`
+ // ConditionalAccessEnabled indicates whether "Conditional access" is enabled/disabled for the team.
+ ConditionalAccessEnabled *bool `json:"conditional_access_enabled"`
}
// TeamSpecsDryRunAssumptions holds the assumptions that are made when applying team specs in dry-run mode.
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index fbbf642c54..7eced15251 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -556,6 +556,8 @@ type GetPoliciesWithAssociatedScriptFunc func(ctx context.Context, teamID uint,
type GetCalendarPoliciesFunc func(ctx context.Context, teamID uint) ([]fleet.PolicyCalendarData, error)
+type GetPoliciesForConditionalAccessFunc func(ctx context.Context, teamID uint) ([]uint, error)
+
type AsyncBatchInsertPolicyMembershipFunc func(ctx context.Context, batch []fleet.PolicyMembershipResult) error
type AsyncBatchUpdatePolicyTimestampFunc func(ctx context.Context, ids []uint, ts time.Time) error
@@ -1364,6 +1366,20 @@ type ScimLastRequestFunc func(ctx context.Context) (*fleet.ScimLastRequest, erro
type UpdateScimLastRequestFunc func(ctx context.Context, lastRequest *fleet.ScimLastRequest) error
+type ConditionalAccessMicrosoftCreateIntegrationFunc func(ctx context.Context, tenantID string, proxyServerSecret string) error
+
+type ConditionalAccessMicrosoftGetFunc func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error)
+
+type ConditionalAccessMicrosoftMarkSetupDoneFunc func(ctx context.Context) error
+
+type ConditionalAccessMicrosoftDeleteFunc func(ctx context.Context) error
+
+type LoadHostConditionalAccessStatusFunc func(ctx context.Context, hostID uint) (*fleet.HostConditionalAccessStatus, error)
+
+type CreateHostConditionalAccessStatusFunc func(ctx context.Context, hostID uint, deviceID string, userPrincipalName string) error
+
+type SetHostConditionalAccessStatusFunc func(ctx context.Context, hostID uint, managed bool, compliant bool) error
+
type DataStore struct {
HealthCheckFunc HealthCheckFunc
HealthCheckFuncInvoked bool
@@ -2166,6 +2182,9 @@ type DataStore struct {
GetCalendarPoliciesFunc GetCalendarPoliciesFunc
GetCalendarPoliciesFuncInvoked bool
+ GetPoliciesForConditionalAccessFunc GetPoliciesForConditionalAccessFunc
+ GetPoliciesForConditionalAccessFuncInvoked bool
+
AsyncBatchInsertPolicyMembershipFunc AsyncBatchInsertPolicyMembershipFunc
AsyncBatchInsertPolicyMembershipFuncInvoked bool
@@ -3378,6 +3397,27 @@ type DataStore struct {
UpdateScimLastRequestFunc UpdateScimLastRequestFunc
UpdateScimLastRequestFuncInvoked bool
+ ConditionalAccessMicrosoftCreateIntegrationFunc ConditionalAccessMicrosoftCreateIntegrationFunc
+ ConditionalAccessMicrosoftCreateIntegrationFuncInvoked bool
+
+ ConditionalAccessMicrosoftGetFunc ConditionalAccessMicrosoftGetFunc
+ ConditionalAccessMicrosoftGetFuncInvoked bool
+
+ ConditionalAccessMicrosoftMarkSetupDoneFunc ConditionalAccessMicrosoftMarkSetupDoneFunc
+ ConditionalAccessMicrosoftMarkSetupDoneFuncInvoked bool
+
+ ConditionalAccessMicrosoftDeleteFunc ConditionalAccessMicrosoftDeleteFunc
+ ConditionalAccessMicrosoftDeleteFuncInvoked bool
+
+ LoadHostConditionalAccessStatusFunc LoadHostConditionalAccessStatusFunc
+ LoadHostConditionalAccessStatusFuncInvoked bool
+
+ CreateHostConditionalAccessStatusFunc CreateHostConditionalAccessStatusFunc
+ CreateHostConditionalAccessStatusFuncInvoked bool
+
+ SetHostConditionalAccessStatusFunc SetHostConditionalAccessStatusFunc
+ SetHostConditionalAccessStatusFuncInvoked bool
+
mu sync.Mutex
}
@@ -5250,6 +5290,13 @@ func (s *DataStore) GetCalendarPolicies(ctx context.Context, teamID uint) ([]fle
return s.GetCalendarPoliciesFunc(ctx, teamID)
}
+func (s *DataStore) GetPoliciesForConditionalAccess(ctx context.Context, teamID uint) ([]uint, error) {
+ s.mu.Lock()
+ s.GetPoliciesForConditionalAccessFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetPoliciesForConditionalAccessFunc(ctx, teamID)
+}
+
func (s *DataStore) AsyncBatchInsertPolicyMembership(ctx context.Context, batch []fleet.PolicyMembershipResult) error {
s.mu.Lock()
s.AsyncBatchInsertPolicyMembershipFuncInvoked = true
@@ -8077,3 +8124,52 @@ func (s *DataStore) UpdateScimLastRequest(ctx context.Context, lastRequest *flee
s.mu.Unlock()
return s.UpdateScimLastRequestFunc(ctx, lastRequest)
}
+
+func (s *DataStore) ConditionalAccessMicrosoftCreateIntegration(ctx context.Context, tenantID string, proxyServerSecret string) error {
+ s.mu.Lock()
+ s.ConditionalAccessMicrosoftCreateIntegrationFuncInvoked = true
+ s.mu.Unlock()
+ return s.ConditionalAccessMicrosoftCreateIntegrationFunc(ctx, tenantID, proxyServerSecret)
+}
+
+func (s *DataStore) ConditionalAccessMicrosoftGet(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ s.mu.Lock()
+ s.ConditionalAccessMicrosoftGetFuncInvoked = true
+ s.mu.Unlock()
+ return s.ConditionalAccessMicrosoftGetFunc(ctx)
+}
+
+func (s *DataStore) ConditionalAccessMicrosoftMarkSetupDone(ctx context.Context) error {
+ s.mu.Lock()
+ s.ConditionalAccessMicrosoftMarkSetupDoneFuncInvoked = true
+ s.mu.Unlock()
+ return s.ConditionalAccessMicrosoftMarkSetupDoneFunc(ctx)
+}
+
+func (s *DataStore) ConditionalAccessMicrosoftDelete(ctx context.Context) error {
+ s.mu.Lock()
+ s.ConditionalAccessMicrosoftDeleteFuncInvoked = true
+ s.mu.Unlock()
+ return s.ConditionalAccessMicrosoftDeleteFunc(ctx)
+}
+
+func (s *DataStore) LoadHostConditionalAccessStatus(ctx context.Context, hostID uint) (*fleet.HostConditionalAccessStatus, error) {
+ s.mu.Lock()
+ s.LoadHostConditionalAccessStatusFuncInvoked = true
+ s.mu.Unlock()
+ return s.LoadHostConditionalAccessStatusFunc(ctx, hostID)
+}
+
+func (s *DataStore) CreateHostConditionalAccessStatus(ctx context.Context, hostID uint, deviceID string, userPrincipalName string) error {
+ s.mu.Lock()
+ s.CreateHostConditionalAccessStatusFuncInvoked = true
+ s.mu.Unlock()
+ return s.CreateHostConditionalAccessStatusFunc(ctx, hostID, deviceID, userPrincipalName)
+}
+
+func (s *DataStore) SetHostConditionalAccessStatus(ctx context.Context, hostID uint, managed bool, compliant bool) error {
+ s.mu.Lock()
+ s.SetHostConditionalAccessStatusFuncInvoked = true
+ s.mu.Unlock()
+ return s.SetHostConditionalAccessStatusFunc(ctx, hostID, managed, compliant)
+}
diff --git a/server/service/appconfig.go b/server/service/appconfig.go
index fadf44c865..00373829c7 100644
--- a/server/service/appconfig.go
+++ b/server/service/appconfig.go
@@ -56,6 +56,8 @@ type appConfigResponseFields struct {
Err error `json:"error,omitempty"`
AndroidEnabled bool `json:"android_enabled,omitempty"`
Partnerships *fleet.Partnerships `json:"partnerships,omitempty"`
+ // ConditionalAccess holds the Microsoft conditional access configuration.
+ ConditionalAccess *fleet.ConditionalAccessSettings `json:"conditional_access,omitempty"`
}
// UnmarshalJSON implements the json.Unmarshaler interface to make sure we serialize
@@ -136,6 +138,18 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se
return nil, err
}
+ var conditionalAccessSettings *fleet.ConditionalAccessSettings
+ conditionalAccessIntegration, err := svc.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if conditionalAccessIntegration != nil {
+ conditionalAccessSettings = &fleet.ConditionalAccessSettings{
+ MicrosoftEntraTenantID: conditionalAccessIntegration.TenantID,
+ MicrosoftEntraConnectionConfigured: conditionalAccessIntegration.SetupDone,
+ }
+ }
+
isGlobalAdmin := vc.User.GlobalRole != nil && *vc.User.GlobalRole == fleet.RoleAdmin
isAnyTeamAdmin := false
if vc.User.Teams != nil {
@@ -196,14 +210,15 @@ func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Se
UIGitOpsMode: appConfig.UIGitOpsMode,
},
appConfigResponseFields: appConfigResponseFields{
- UpdateInterval: updateIntervalConfig,
- Vulnerabilities: vulnConfig,
- License: license,
- Logging: loggingConfig,
- Email: emailConfig,
- SandboxEnabled: svc.SandboxEnabled(),
- AndroidEnabled: os.Getenv("FLEET_DEV_ANDROID_ENABLED") == "1", // Temporary feature flag that will be removed.
- Partnerships: partnerships,
+ UpdateInterval: updateIntervalConfig,
+ Vulnerabilities: vulnConfig,
+ License: license,
+ Logging: loggingConfig,
+ Email: emailConfig,
+ SandboxEnabled: svc.SandboxEnabled(),
+ AndroidEnabled: os.Getenv("FLEET_DEV_ANDROID_ENABLED") == "1", // Temporary feature flag that will be removed.
+ Partnerships: partnerships,
+ ConditionalAccess: conditionalAccessSettings,
},
}
return response, nil
@@ -315,6 +330,8 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
oldAgentOptions = string(*appConfig.AgentOptions)
}
+ oldConditionalAccessEnabled := appConfig.Integrations.ConditionalAccessEnabled
+
storedJiraByProjectKey, err := fleet.IndexJiraIntegrations(appConfig.Integrations.Jira)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "modify AppConfig")
@@ -478,6 +495,15 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
fleet.ValidateEnabledHostStatusIntegrations(appConfig.WebhookSettings.HostStatusWebhook, invalid)
fleet.ValidateEnabledActivitiesWebhook(appConfig.WebhookSettings.ActivitiesWebhook, invalid)
+ var conditionalAccessNoTeamUpdated bool
+ if newAppConfig.Integrations.ConditionalAccessEnabled.Set {
+ if err := fleet.ValidateConditionalAccessIntegration(ctx, svc, oldConditionalAccessEnabled.Value, newAppConfig.Integrations.ConditionalAccessEnabled.Value); err != nil {
+ return nil, err
+ }
+ conditionalAccessNoTeamUpdated = oldConditionalAccessEnabled.Value != newAppConfig.Integrations.ConditionalAccessEnabled.Value
+ appConfig.Integrations.ConditionalAccessEnabled = newAppConfig.Integrations.ConditionalAccessEnabled
+ }
+
if err := svc.validateMDM(ctx, license, &oldAppConfig.MDM, &appConfig.MDM, invalid); err != nil {
return nil, ctxerr.Wrap(ctx, err, "validating MDM config")
}
@@ -944,6 +970,33 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
}
}
+ // Create activity if conditional access was enabled or disabled for "No team".
+ if conditionalAccessNoTeamUpdated {
+ if appConfig.Integrations.ConditionalAccessEnabled.Value {
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeEnabledConditionalAccessAutomations{
+ TeamID: nil,
+ TeamName: "",
+ },
+ ); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "create activity for enabling conditional access")
+ }
+ } else {
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeDisabledConditionalAccessAutomations{
+ TeamID: nil,
+ TeamName: "",
+ },
+ ); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "create activity for disabling conditional access")
+ }
+ }
+ }
+
return obfuscatedAppConfig, nil
}
diff --git a/server/service/client.go b/server/service/client.go
index 35583dd463..218ac804fd 100644
--- a/server/service/client.go
+++ b/server/service/client.go
@@ -1661,6 +1661,9 @@ func (c *Client) DoGitOps(
if googleCal, ok := integrations.(map[string]interface{})["google_calendar"]; !ok || googleCal == nil {
integrations.(map[string]interface{})["google_calendar"] = []interface{}{}
}
+ if conditionalAccessEnabled, ok := integrations.(map[string]interface{})["conditional_access_enabled"]; !ok || conditionalAccessEnabled == nil {
+ integrations.(map[string]interface{})["conditional_access_enabled"] = false
+ }
if ndesSCEPProxy, ok := integrations.(map[string]interface{})["ndes_scep_proxy"]; !ok || ndesSCEPProxy == nil {
// Per backend patterns.md, best practice is to clear a JSON config field with `null`
integrations.(map[string]interface{})["ndes_scep_proxy"] = nil
@@ -1847,6 +1850,7 @@ func (c *Client) DoGitOps(
if !ok {
return nil, nil, errors.New("team_settings.integrations config is not a map")
}
+
if googleCal, ok := integrations.(map[string]interface{})["google_calendar"]; !ok || googleCal == nil {
integrations.(map[string]interface{})["google_calendar"] = map[string]interface{}{}
} else {
@@ -1856,6 +1860,15 @@ func (c *Client) DoGitOps(
}
}
+ if conditionalAccessEnabled, ok := integrations.(map[string]interface{})["conditional_access_enabled"]; !ok || conditionalAccessEnabled == nil {
+ integrations.(map[string]interface{})["conditional_access_enabled"] = false
+ } else {
+ _, ok = conditionalAccessEnabled.(bool)
+ if !ok {
+ return nil, nil, errors.New("team_settings.integrations.conditional_access_enabled config is not a bool")
+ }
+ }
+
team["mdm"] = map[string]interface{}{}
mdmAppConfig = team["mdm"].(map[string]interface{})
}
diff --git a/server/service/conditional_access_microsoft.go b/server/service/conditional_access_microsoft.go
new file mode 100644
index 0000000000..554a8748b5
--- /dev/null
+++ b/server/service/conditional_access_microsoft.go
@@ -0,0 +1,241 @@
+package service
+
+import (
+ "context"
+ "errors"
+
+ "github.com/fleetdm/fleet/v4/server/authz"
+ "github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/go-kit/log/level"
+)
+
+type conditionalAccessMicrosoftCreateRequest struct {
+ // MicrosoftTenantID holds the Entra tenant ID.
+ MicrosoftTenantID string `json:"microsoft_tenant_id"`
+}
+
+type conditionalAccessMicrosoftCreateResponse struct {
+ // MicrosoftAuthenticationURL holds the URL to redirect the admin to consent access
+ // to the tenant to Fleet's multi-tenant application.
+ MicrosoftAuthenticationURL string `json:"microsoft_authentication_url"`
+ Err error `json:"error,omitempty"`
+}
+
+func (r conditionalAccessMicrosoftCreateResponse) Error() error { return r.Err }
+
+func conditionalAccessMicrosoftCreateEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
+ req := request.(*conditionalAccessMicrosoftCreateRequest)
+ adminConsentURL, err := svc.ConditionalAccessMicrosoftCreateIntegration(ctx, req.MicrosoftTenantID)
+ if err != nil {
+ return conditionalAccessMicrosoftCreateResponse{Err: err}, nil
+ }
+ return conditionalAccessMicrosoftCreateResponse{
+ MicrosoftAuthenticationURL: adminConsentURL,
+ }, nil
+}
+
+func (svc *Service) ConditionalAccessMicrosoftCreateIntegration(ctx context.Context, tenantID string) (adminConsentURL string, err error) {
+ // 0. Check user is authorized to create an integration.
+ if err := svc.authz.Authorize(ctx, &fleet.ConditionalAccessMicrosoftIntegration{}, fleet.ActionWrite); err != nil {
+ return "", ctxerr.Wrap(ctx, err, "failed to authorize")
+ }
+
+ if !svc.config.MicrosoftCompliancePartner.IsSet() {
+ return "", &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
+ }
+
+ // Load current integration, if any.
+ existingIntegration, err := svc.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ return "", ctxerr.Wrap(ctx, err, "failed to load the integration")
+ }
+ switch {
+ case existingIntegration != nil && existingIntegration.TenantID == tenantID:
+ // Nothing to do, integration with same tenant ID has already been created.
+ // Retrieve settings of the integration to get the admin consent URL.
+ getResponse, err := svc.conditionalAccessMicrosoftProxy.Get(ctx, existingIntegration.TenantID, existingIntegration.ProxyServerSecret)
+ if err != nil {
+ return "", ctxerr.Wrap(ctx, err, "failed to get the integration settings")
+ }
+ return getResponse.AdminConsentURL, nil
+ case existingIntegration != nil && existingIntegration.SetupDone:
+ return "", &fleet.BadRequestError{Message: "integration already setup"}
+ }
+
+ //
+ // At this point we have two scenarios:
+ // - There's no integration yet, so we need to create a new one.
+ // - There's an integration already with a different TenantID and has not been setup.
+ //
+
+ // Create integration on the proxy.
+ proxyCreateResponse, err := svc.conditionalAccessMicrosoftProxy.Create(ctx, tenantID)
+ if err != nil {
+ return "", ctxerr.Wrap(ctx, err, "failed to create integration in proxy")
+ }
+
+ // Create integration in datastore.
+ if err := svc.ds.ConditionalAccessMicrosoftCreateIntegration(ctx, proxyCreateResponse.TenantID, proxyCreateResponse.Secret); err != nil {
+ return "", ctxerr.Wrap(ctx, err, "failed to create integration in datastore")
+ }
+
+ // Retrieve settings of the integration to get the admin consent URL.
+ getResponse, err := svc.conditionalAccessMicrosoftProxy.Get(ctx, proxyCreateResponse.TenantID, proxyCreateResponse.Secret)
+ if err != nil {
+ return "", ctxerr.Wrap(ctx, err, "failed to get the integration settings")
+ }
+ return getResponse.AdminConsentURL, nil
+}
+
+type conditionalAccessMicrosoftConfirmRequest struct{}
+
+type conditionalAccessMicrosoftConfirmResponse struct {
+ ConfigurationCompleted bool `json:"configuration_completed"`
+ Err error `json:"error,omitempty"`
+}
+
+func (r conditionalAccessMicrosoftConfirmResponse) Error() error { return r.Err }
+
+func conditionalAccessMicrosoftConfirmEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
+ _ = request.(*conditionalAccessMicrosoftConfirmRequest)
+ configurationCompleted, err := svc.ConditionalAccessMicrosoftConfirm(ctx)
+ if err != nil {
+ return conditionalAccessMicrosoftConfirmResponse{Err: err}, nil
+ }
+ return conditionalAccessMicrosoftConfirmResponse{
+ ConfigurationCompleted: configurationCompleted,
+ }, nil
+}
+
+func (svc *Service) ConditionalAccessMicrosoftConfirm(ctx context.Context) (configurationCompleted bool, err error) {
+ // Check user is authorized to write integrations.
+ if err := svc.authz.Authorize(ctx, &fleet.ConditionalAccessMicrosoftIntegration{}, fleet.ActionWrite); err != nil {
+ return false, ctxerr.Wrap(ctx, err, "failed to authorize")
+ }
+
+ if !svc.config.MicrosoftCompliancePartner.IsSet() {
+ return false, &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
+ }
+
+ // Load current integration.
+ integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ return false, ctxerr.Wrap(ctx, err, "failed to load the integration")
+ }
+
+ if integration.SetupDone {
+ return true, nil
+ }
+
+ getResponse, err := svc.conditionalAccessMicrosoftProxy.Get(ctx, integration.TenantID, integration.ProxyServerSecret)
+ if err != nil {
+ level.Error(svc.logger).Log("msg", "failed to get integration settings from proxy", "err", err)
+ return false, nil
+ }
+
+ if !getResponse.SetupDone {
+ return false, nil
+ }
+
+ if err := svc.ds.ConditionalAccessMicrosoftMarkSetupDone(ctx); err != nil {
+ return false, ctxerr.Wrap(ctx, err, "failed to mark setup_done=true")
+ }
+
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeAddedConditionalAccessIntegrationMicrosoft{},
+ ); err != nil {
+ return false, ctxerr.Wrap(ctx, err, "create activity for conditional access integration microsoft")
+ }
+
+ return true, nil
+}
+
+type conditionalAccessMicrosoftDeleteRequest struct{}
+
+type conditionalAccessMicrosoftDeleteResponse struct {
+ Err error `json:"error,omitempty"`
+}
+
+func (r conditionalAccessMicrosoftDeleteResponse) Error() error { return r.Err }
+
+func conditionalAccessMicrosoftDeleteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
+ _ = request.(*conditionalAccessMicrosoftDeleteRequest)
+ if err := svc.ConditionalAccessMicrosoftDelete(ctx); err != nil {
+ return conditionalAccessMicrosoftDeleteResponse{Err: err}, nil
+ }
+ return conditionalAccessMicrosoftDeleteResponse{}, nil
+}
+
+func (svc *Service) ConditionalAccessMicrosoftDelete(ctx context.Context) error {
+ // Check user is authorized to delete an integration.
+ if err := svc.authz.Authorize(ctx, &fleet.ConditionalAccessMicrosoftIntegration{}, fleet.ActionWrite); err != nil {
+ return ctxerr.Wrap(ctx, err, "failed to authorize")
+ }
+
+ if !svc.config.MicrosoftCompliancePartner.IsSet() {
+ return &fleet.BadRequestError{Message: "microsoft conditional access configuration not set"}
+ }
+
+ // Load current integration.
+ integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ if fleet.IsNotFound(err) {
+ return &fleet.BadRequestError{Message: "integration not found"}
+ }
+ return ctxerr.Wrap(ctx, err, "failed to load the integration")
+ }
+
+ // Delete integration on the proxy.
+ deleteResponse, err := svc.conditionalAccessMicrosoftProxy.Delete(ctx, integration.TenantID, integration.ProxyServerSecret)
+ if err != nil {
+ if fleet.IsNotFound(err) {
+ // In case there's an issue on the Proxy database we want to make sure to
+ // allow deleting the integration in Fleet, so we continue.
+ svc.logger.Log("msg", "delete returned not found, continuing...")
+ } else {
+ return ctxerr.Wrap(ctx, err, "failed to delete the integration on the proxy")
+ }
+ } else if deleteResponse.Error != "" {
+ return ctxerr.Wrap(ctx, errors.New(deleteResponse.Error), "delete on the proxy failed")
+ }
+
+ // Delete integration in datastore.
+ if err := svc.ds.ConditionalAccessMicrosoftDelete(ctx); err != nil {
+ return ctxerr.Wrap(ctx, err, "failed to delete integration in datastore")
+ }
+
+ if err := svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeDeletedConditionalAccessIntegrationMicrosoft{},
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "create activity for deletion of conditional access integration microsoft")
+ }
+
+ return nil
+}
+
+func (svc *Service) ConditionalAccessMicrosoftGet(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ // Check user is authorized to read app config (which is where expose integration information)
+ if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "failed to authorize")
+ }
+
+ if !svc.config.MicrosoftCompliancePartner.IsSet() {
+ return nil, nil
+ }
+
+ // Load current integration.
+ integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ if fleet.IsNotFound(err) {
+ return nil, nil
+ }
+ return nil, ctxerr.Wrap(ctx, err, "failed to load the integration")
+ }
+
+ return integration, nil
+}
diff --git a/server/service/conditional_access_microsoft_proxy/conditional_access_microsoft_proxy.go b/server/service/conditional_access_microsoft_proxy/conditional_access_microsoft_proxy.go
new file mode 100644
index 0000000000..4a17a69d62
--- /dev/null
+++ b/server/service/conditional_access_microsoft_proxy/conditional_access_microsoft_proxy.go
@@ -0,0 +1,310 @@
+// Package conditional_access_microsoft_proxy is the client HTTP package to operate on Entra through Fleet's MS proxy.
+package conditional_access_microsoft_proxy
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/fleetdm/fleet/v4/pkg/fleethttp"
+)
+
+// Proxy holds functionality to send requests to Entra via Fleet's MS proxy.
+type Proxy struct {
+ uri string
+ apiKey string
+ originGetter func() (string, error)
+
+ c *http.Client
+}
+
+// New creates a Proxy that will use the given URI and API key.
+func New(uri string, apiKey string, originGetter func() (string, error)) (*Proxy, error) {
+ if _, err := url.Parse(uri); err != nil {
+ return nil, fmt.Errorf("parse uri: %w", err)
+ }
+ return &Proxy{
+ uri: uri,
+ apiKey: apiKey,
+
+ originGetter: originGetter,
+
+ c: fleethttp.NewClient(),
+ }, nil
+}
+
+type createRequest struct {
+ TenantID string `json:"entraTenantId"`
+}
+
+// CreateResponse returns the tenant ID and the secret of the created integration
+// Such credentials are used to authenticate all requests.
+type CreateResponse struct {
+ TenantID string `json:"entra_tenant_id"`
+ Secret string `json:"fleet_server_secret"`
+}
+
+// Create creates the integration on the MS proxy and returns the consent URL.
+func (p *Proxy) Create(ctx context.Context, tenantID string) (*CreateResponse, error) {
+ var createResponse CreateResponse
+ if err := p.post(
+ "/api/v1/microsoft-compliance-partner",
+ createRequest{TenantID: tenantID},
+ &createResponse,
+ ); err != nil {
+ return nil, fmt.Errorf("create integration failed: %w", err)
+ }
+ return &createResponse, nil
+}
+
+// GetResponse holds the settings of the current integration.
+type GetResponse struct {
+ TenantID string `json:"entra_tenant_id"`
+ SetupDone bool `json:"setup_done"`
+ AdminConsentURL string `json:"admin_consent_url"`
+ SetupError *string `json:"setup_error"`
+}
+
+// Get returns the integration settings.
+func (p *Proxy) Get(ctx context.Context, tenantID string, secret string) (*GetResponse, error) {
+ var getResponse GetResponse
+ if err := p.get(
+ "/api/v1/microsoft-compliance-partner/settings",
+ fmt.Sprintf("entraTenantId=%s&fleetServerSecret=%s", tenantID, secret),
+ &getResponse,
+ ); err != nil {
+ return nil, fmt.Errorf("get integration settings failed: %w", err)
+ }
+ return &getResponse, nil
+}
+
+// DeleteResponse contains an error detail if any.
+type DeleteResponse struct {
+ Error string `json:"error"`
+}
+
+// Delete deprovisions the tenant on Microsoft and deletes the integration in the proxy service.
+// Returns a fleet.IsNotFound error if the integration doesn't exist.
+func (p *Proxy) Delete(ctx context.Context, tenantID string, secret string) (*DeleteResponse, error) {
+ var deleteResponse DeleteResponse
+ if err := p.delete(
+ "/api/v1/microsoft-compliance-partner",
+ fmt.Sprintf("entraTenantId=%s&fleetServerSecret=%s", tenantID, secret),
+ &deleteResponse,
+ ); err != nil {
+ return nil, fmt.Errorf("delete integration failed: %w", err)
+ }
+ return &deleteResponse, nil
+}
+
+type setComplianceStatusRequest struct {
+ TenantID string `json:"entraTenantId"`
+ Secret string `json:"fleetServerSecret"`
+
+ DeviceID string `json:"deviceId"`
+ UserPrincipalName string `json:"userPrincipalName"`
+
+ DeviceManagementState bool `json:"deviceManagementState"`
+ DeviceName string `json:"deviceName"`
+ OS string `json:"os"`
+ OSVersion string `json:"osVersion"`
+ Compliant bool `json:"compliant"`
+ LastCheckInTime int `json:"lastCheckInTime"`
+}
+
+// SetComplianceStatusResponse holds the MessageID to query the status of the "compliance set" operation.
+type SetComplianceStatusResponse struct {
+ // MessageID holds the ID to use when querying the status of the "compliance set" operation.
+ MessageID string `json:"message_id"`
+}
+
+// SetComplianceStatus sets the inventory and compliance status of a host.
+// Returns the message ID to query the status of the operation (MS has an asynchronous API).
+func (p *Proxy) SetComplianceStatus(
+ ctx context.Context,
+ tenantID string, secret string,
+ deviceID string,
+ userPrincipalName string,
+ mdmEnrolled bool,
+ deviceName, osName, osVersion string,
+ compliant bool,
+ lastCheckInTime time.Time,
+) (*SetComplianceStatusResponse, error) {
+ var setComplianceStatusResponse SetComplianceStatusResponse
+ if err := p.post(
+ "/api/v1/microsoft-compliance-partner/device",
+ setComplianceStatusRequest{
+ TenantID: tenantID,
+ Secret: secret,
+
+ DeviceID: deviceID,
+ UserPrincipalName: userPrincipalName,
+
+ DeviceManagementState: mdmEnrolled,
+ DeviceName: deviceName,
+ OS: osName,
+ OSVersion: osVersion,
+ Compliant: compliant,
+ LastCheckInTime: int(lastCheckInTime.Unix()),
+ },
+ &setComplianceStatusResponse,
+ ); err != nil {
+ return nil, fmt.Errorf("set compliance status response failed: %w", err)
+ }
+ return &setComplianceStatusResponse, nil
+}
+
+// MessageStatusCompleted is the value returned when a "compliance set" operation has been successfully applied.
+const MessageStatusCompleted = "Completed"
+
+// GetMessageStatusResponse returns the status of a "compliance set" operation.
+type GetMessageStatusResponse struct {
+ // MessageID is the ID of the operation.
+ MessageID string `json:"message_id"`
+ // Status of the operation.
+ Status string `json:"status"`
+ // Detail has some error description when Status is not "Completed".
+ Detail *string `json:"detail"`
+}
+
+// GetMessageStatus returns the status of the operation (MS has an asynchronous API).
+func (p *Proxy) GetMessageStatus(
+ ctx context.Context,
+ tenantID string, secret string,
+ messageID string,
+) (*GetMessageStatusResponse, error) {
+ var getMessageStatusResponse GetMessageStatusResponse
+ if err := p.get(
+ "/api/v1/microsoft-compliance-partner/device/message",
+ fmt.Sprintf("entraTenantId=%s&fleetServerSecret=%s&messageId=%s", tenantID, secret, messageID),
+ &getMessageStatusResponse,
+ ); err != nil {
+ return nil, fmt.Errorf("get message status response failed: %w", err)
+ }
+ return &getMessageStatusResponse, nil
+}
+
+func (p *Proxy) post(path string, request interface{}, response interface{}) error {
+ b, err := json.Marshal(request)
+ if err != nil {
+ return fmt.Errorf("marshal request: %w", err)
+ }
+ postRequest, err := http.NewRequest("POST", p.uri+path, nil)
+ if err != nil {
+ return fmt.Errorf("post create request: %w", err)
+ }
+ if err := p.setHeaders(postRequest); err != nil {
+ return fmt.Errorf("post set headers: %w", err)
+ }
+ postRequest.Header.Add("Content-Type", "application/json")
+ postRequest.Body = io.NopCloser(bytes.NewBuffer(b))
+ resp, err := p.c.Do(postRequest)
+ if err != nil {
+ return fmt.Errorf("post request: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("post request failed: %s", resp.Status)
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("post read response body: %w", err)
+ }
+ if err := json.Unmarshal(body, response); err != nil {
+ return fmt.Errorf("post unmarshal response: %w", err)
+ }
+ return nil
+}
+
+func (p *Proxy) get(path string, query string, response interface{}) error {
+ getURL := p.uri + path
+ if query != "" {
+ getURL += "?" + url.PathEscape(query)
+ }
+ getRequest, err := http.NewRequest("GET", getURL, nil)
+ if err != nil {
+ return fmt.Errorf("get create request: %w", err)
+ }
+ if err := p.setHeaders(getRequest); err != nil {
+ return fmt.Errorf("get set headers: %w", err)
+ }
+ resp, err := p.c.Do(getRequest)
+ if err != nil {
+ return fmt.Errorf("get request: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("get request failed: %s", resp.Status)
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("get read response body: %w", err)
+ }
+ if err := json.Unmarshal(body, response); err != nil {
+ return fmt.Errorf("get unmarshal response: %w", err)
+ }
+ return nil
+}
+
+func (p *Proxy) delete(path string, query string, response interface{}) error {
+ deleteURL := p.uri + path
+ if query != "" {
+ deleteURL += "?" + url.PathEscape(query)
+ }
+ deleteRequest, err := http.NewRequest("DELETE", deleteURL, nil)
+ if err != nil {
+ return fmt.Errorf("delete create request: %w", err)
+ }
+ if err := p.setHeaders(deleteRequest); err != nil {
+ return fmt.Errorf("delete set headers: %w", err)
+ }
+ resp, err := p.c.Do(deleteRequest)
+ if err != nil {
+ return fmt.Errorf("delete request: %w", err)
+ }
+ defer resp.Body.Close()
+ switch resp.StatusCode {
+ case http.StatusOK:
+ // OK
+ case http.StatusNotFound:
+ return ¬FoundError{}
+ default:
+ return fmt.Errorf("delete request failed: %s", resp.Status)
+ }
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("delete read response body: %w", err)
+ }
+ if err := json.Unmarshal(body, response); err != nil {
+ return fmt.Errorf("delete unmarshal response: %w", err)
+ }
+ return nil
+}
+
+type notFoundError struct{}
+
+func (e *notFoundError) Error() string {
+ return "not found"
+}
+
+func (e *notFoundError) IsNotFound() bool {
+ return true
+}
+
+func (p *Proxy) setHeaders(r *http.Request) error {
+ origin, err := p.originGetter()
+ if err != nil {
+ return fmt.Errorf("get origin: %w", err)
+ }
+ if origin == "" {
+ return fmt.Errorf("missing origin: %w", err)
+ }
+ r.Header.Add("MS-API-Key", p.apiKey)
+ r.Header.Add("Origin", origin)
+ return nil
+}
diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go
index fdb1925a92..86761ba939 100644
--- a/server/service/global_schedule.go
+++ b/server/service/global_schedule.go
@@ -166,7 +166,6 @@ func deleteGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc
return deleteGlobalScheduleResponse{}, nil
}
-// TODO(lucas): Document new behavior.
func (svc *Service) DeleteGlobalScheduledQueries(ctx context.Context, id uint) error {
return svc.DeleteQueryByID(ctx, id)
}
diff --git a/server/service/handler.go b/server/service/handler.go
index 01e87b41a2..1fe0f2fbeb 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -509,6 +509,11 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// Scim details
ue.GET("/api/_version_/fleet/scim/details", getScimDetailsEndpoint, nil)
+ // Microsoft Compliance Partner
+ ue.POST("/api/_version_/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateEndpoint, conditionalAccessMicrosoftCreateRequest{})
+ ue.POST("/api/_version_/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmEndpoint, conditionalAccessMicrosoftConfirmRequest{})
+ ue.DELETE("/api/_version_/fleet/conditional-access/microsoft", conditionalAccessMicrosoftDeleteEndpoint, conditionalAccessMicrosoftDeleteRequest{})
+
// Only Fleet MDM specific endpoints should be within the root /mdm/ path.
// NOTE: remember to update
// `service.mdmConfigurationRequiredEndpoints` when you add an
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index dc7c513318..9184d6809b 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -7295,7 +7295,6 @@ func (s *integrationTestSuite) TestAppConfig() {
assert.Contains(t, errMsg, "missing or invalid license")
}
-// TODO(lucas): Add tests here.
func (s *integrationTestSuite) TestQuerySpecs() {
t := s.T()
@@ -10554,7 +10553,7 @@ func (s *integrationTestSuite) TestDirectIngestScheduledQueryStats() {
App: config.AppConfig{
EnableScheduledQueryStats: true,
},
- }, appConfig, &appConfig.Features)
+ }, appConfig, &appConfig.Features, osquery_utils.Integrations{})
task := async.NewTask(s.ds, nil, clock.C, config.OsqueryConfig{})
err = detailQueries["scheduled_query_stats"].DirectTaskIngestFunc(
context.Background(),
@@ -10709,7 +10708,7 @@ func (s *integrationTestSuite) TestDirectIngestSoftwareWithLongFields() {
"installed_path": "C:\\Program Files\\Wireshark",
},
}
- detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features)
+ detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features, osquery_utils.Integrations{})
err = detailQueries["software_windows"].DirectIngestFunc(
context.Background(),
log.NewNopLogger(),
@@ -10845,7 +10844,7 @@ func (s *integrationTestSuite) TestDirectIngestSoftwareWithInvalidFields() {
}
var w1 bytes.Buffer
logger1 := log.NewJSONLogger(&w1)
- detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features)
+ detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features, osquery_utils.Integrations{})
err = detailQueries["software_windows"].DirectIngestFunc(
context.Background(),
logger1,
@@ -10880,7 +10879,7 @@ func (s *integrationTestSuite) TestDirectIngestSoftwareWithInvalidFields() {
"last_opened_at": "foobar",
},
}
- detailQueries = osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features)
+ detailQueries = osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features, osquery_utils.Integrations{})
var w2 bytes.Buffer
logger2 := log.NewJSONLogger(&w2)
err = detailQueries["software_windows"].DirectIngestFunc(
@@ -10920,7 +10919,7 @@ func (s *integrationTestSuite) TestDirectIngestSoftwareWithInvalidFields() {
}
var w3 bytes.Buffer
logger3 := log.NewJSONLogger(&w3)
- detailQueries = osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features)
+ detailQueries = osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, appConfig, &appConfig.Features, osquery_utils.Integrations{})
err = detailQueries["software_windows"].DirectIngestFunc(
context.Background(),
logger3,
@@ -13450,3 +13449,23 @@ func (s *integrationTestSuite) TestHostReenrollWithSameHostRowRefetchOsquery() {
require.Equal(t, oldHosts[i].ID, h.ID)
}
}
+
+func (s *integrationTestSuite) TestConditionalAccessOnlyCloud() {
+ t := s.T()
+
+ var resp appConfigResponse
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &resp)
+ require.False(t, resp.License.ManagedCloud)
+
+ // Microsoft compliance partner APIs should fail if the setting is not set (only set on Cloud).
+ var r conditionalAccessMicrosoftCreateResponse
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{
+ MicrosoftTenantID: "foobar",
+ }, http.StatusBadRequest, &r)
+ var c conditionalAccessMicrosoftConfirmResponse
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{},
+ http.StatusBadRequest, &c)
+ var d conditionalAccessMicrosoftDeleteResponse
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{},
+ http.StatusBadRequest, &d)
+}
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index ad42b331d3..ca3d8bdf6c 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -45,6 +45,7 @@ import (
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/pubsub"
commonCalendar "github.com/fleetdm/fleet/v4/server/service/calendar"
+ "github.com/fleetdm/fleet/v4/server/service/conditional_access_microsoft_proxy"
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
"github.com/fleetdm/fleet/v4/server/service/schedule"
"github.com/fleetdm/fleet/v4/server/test"
@@ -115,7 +116,8 @@ func (s *integrationEnterpriseTestSuite) SetupSuite() {
}
},
},
- SoftwareInstallStore: softwareInstallStore,
+ SoftwareInstallStore: softwareInstallStore,
+ ConditionalAccessMicrosoftProxy: mockedConditionalAccessMicrosoftProxyInstance,
}
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
config.Logger = kitlog.NewNopLogger()
@@ -9732,6 +9734,36 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareAuth() {
s.token = s.getTestAdminToken()
}
+func genDistributedReqWithPolicyResults(host *fleet.Host, policyResults map[uint]*bool) submitDistributedQueryResultsRequestShim {
+ var (
+ results = make(map[string]json.RawMessage)
+ statuses = make(map[string]interface{})
+ messages = make(map[string]string)
+ )
+ for policyID, policyResult := range policyResults {
+ distributedQueryName := hostPolicyQueryPrefix + fmt.Sprint(policyID)
+ switch {
+ case policyResult == nil:
+ results[distributedQueryName] = json.RawMessage(`[]`)
+ statuses[distributedQueryName] = 1
+ messages[distributedQueryName] = "policy failed execution"
+ case *policyResult:
+ results[distributedQueryName] = json.RawMessage(`[{"1": "1"}]`)
+ statuses[distributedQueryName] = 0
+ case !*policyResult:
+ results[distributedQueryName] = json.RawMessage(`[]`)
+ statuses[distributedQueryName] = 0
+ }
+ }
+ return submitDistributedQueryResultsRequestShim{
+ NodeKey: *host.NodeKey,
+ Results: results,
+ Statuses: statuses,
+ Messages: messages,
+ Stats: map[string]*fleet.Stats{},
+ }
+}
+
func (s *integrationEnterpriseTestSuite) TestCalendarEvents() {
ctx := context.Background()
t := s.T()
@@ -9819,36 +9851,6 @@ func (s *integrationEnterpriseTestSuite) TestCalendarEvents() {
)
require.NoError(t, err)
- genDistributedReqWithPolicyResults := func(host *fleet.Host, policyResults map[uint]*bool) submitDistributedQueryResultsRequestShim {
- var (
- results = make(map[string]json.RawMessage)
- statuses = make(map[string]interface{})
- messages = make(map[string]string)
- )
- for policyID, policyResult := range policyResults {
- distributedQueryName := hostPolicyQueryPrefix + fmt.Sprint(policyID)
- switch {
- case policyResult == nil:
- results[distributedQueryName] = json.RawMessage(`[]`)
- statuses[distributedQueryName] = 1
- messages[distributedQueryName] = "policy failed execution"
- case *policyResult:
- results[distributedQueryName] = json.RawMessage(`[{"1": "1"}]`)
- statuses[distributedQueryName] = 0
- case !*policyResult:
- results[distributedQueryName] = json.RawMessage(`[]`)
- statuses[distributedQueryName] = 0
- }
- }
- return submitDistributedQueryResultsRequestShim{
- NodeKey: *host.NodeKey,
- Results: results,
- Statuses: statuses,
- Messages: messages,
- Stats: map[string]*fleet.Stats{},
- }
- }
-
// host1Team1 is failing a calendar policy and not a non-calendar policy (no results for global).
distributedResp := submitDistributedQueryResultsResponse{}
s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
@@ -13353,32 +13355,14 @@ func getSoftwareTitleID(t *testing.T, ds *mysql.Datastore, title, source string)
return id
}
-func genDistributedReqWithPolicyResults(host *fleet.Host, policyResults map[uint]*bool) submitDistributedQueryResultsRequestShim {
- var (
- results = make(map[string]json.RawMessage)
- statuses = make(map[string]interface{})
- messages = make(map[string]string)
- )
- for policyID, policyResult := range policyResults {
- distributedQueryName := hostPolicyQueryPrefix + fmt.Sprint(policyID)
- switch {
- case policyResult == nil:
- results[distributedQueryName] = json.RawMessage(`[]`)
- statuses[distributedQueryName] = 1
- messages[distributedQueryName] = "policy failed execution"
- case *policyResult:
- results[distributedQueryName] = json.RawMessage(`[{"1": "1"}]`)
- statuses[distributedQueryName] = 0
- case !*policyResult:
- results[distributedQueryName] = json.RawMessage(`[]`)
- statuses[distributedQueryName] = 0
- }
- }
+func genDistributedReqWithEntraIDDetails(host *fleet.Host, deviceID, userPrincipalName string) submitDistributedQueryResultsRequestShim {
+ results := make(map[string]json.RawMessage)
+ results["fleet_detail_query_conditional_access_microsoft_device_id"] = json.RawMessage(fmt.Sprintf(`[{"device_id": "%s", "user_principal_name": "%s"}]`, deviceID, userPrincipalName))
return submitDistributedQueryResultsRequestShim{
NodeKey: *host.NodeKey,
Results: results,
- Statuses: statuses,
- Messages: messages,
+ Statuses: make(map[string]interface{}),
+ Messages: make(map[string]string),
Stats: map[string]*fleet.Stats{},
}
}
@@ -17838,3 +17822,571 @@ func (s *integrationEnterpriseTestSuite) TestBatchSoftwareInstallerAndFMACategor
})
}
}
+
+type mockedConditionalAccessMicrosoftProxy struct {
+ createResponse *conditional_access_microsoft_proxy.CreateResponse
+ getResponse *conditional_access_microsoft_proxy.GetResponse
+ deleteErr error
+ deleteResponse *conditional_access_microsoft_proxy.DeleteResponse
+
+ setComplianceStatusFunc func(
+ ctx context.Context,
+ tenantID string, secret string,
+ deviceID string,
+ userPrincipalName string,
+ mdmEnrolled bool,
+ deviceName, osName, osVersion string,
+ compliant bool,
+ lastCheckInTime time.Time,
+ ) (*conditional_access_microsoft_proxy.SetComplianceStatusResponse, error)
+
+ getMessageStatusFunc func(
+ ctx context.Context,
+ tenantID string,
+ secret string,
+ messageID string,
+ ) (*conditional_access_microsoft_proxy.GetMessageStatusResponse, error)
+}
+
+func (m *mockedConditionalAccessMicrosoftProxy) Create(ctx context.Context, tenantID string) (*conditional_access_microsoft_proxy.CreateResponse, error) {
+ return m.createResponse, nil
+}
+
+func (m *mockedConditionalAccessMicrosoftProxy) Get(ctx context.Context, tenantID string, secret string) (*conditional_access_microsoft_proxy.GetResponse, error) {
+ return m.getResponse, nil
+}
+
+func (m *mockedConditionalAccessMicrosoftProxy) Delete(ctx context.Context, tenantID string, secret string) (*conditional_access_microsoft_proxy.DeleteResponse, error) {
+ if m.deleteErr != nil {
+ return nil, m.deleteErr
+ }
+ return m.deleteResponse, nil
+}
+
+func (m *mockedConditionalAccessMicrosoftProxy) SetComplianceStatus(
+ ctx context.Context,
+ tenantID string, secret string,
+ deviceID string,
+ userPrincipalName string,
+ mdmEnrolled bool,
+ deviceName, osName, osVersion string,
+ compliant bool,
+ lastCheckInTime time.Time,
+) (*conditional_access_microsoft_proxy.SetComplianceStatusResponse, error) {
+ return m.setComplianceStatusFunc(ctx, tenantID, secret, deviceID, userPrincipalName, mdmEnrolled, deviceName, osName, osVersion, compliant, lastCheckInTime)
+}
+
+func (m *mockedConditionalAccessMicrosoftProxy) GetMessageStatus(
+ ctx context.Context, tenantID string, secret string, messageID string,
+) (*conditional_access_microsoft_proxy.GetMessageStatusResponse, error) {
+ return m.getMessageStatusFunc(ctx, tenantID, secret, messageID)
+}
+
+var mockedConditionalAccessMicrosoftProxyInstance = &mockedConditionalAccessMicrosoftProxy{}
+
+func (s *integrationEnterpriseTestSuite) TestConditionalAccessBasicSetup() {
+ t := s.T()
+
+ // Test license.managed_cloud is set on Cloud environments.
+ var acResp appConfigResponse
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.True(t, acResp.License.ManagedCloud)
+
+ // Test global maintainer fails to create the integration.
+ u := &fleet.User{
+ Name: "test maintainer",
+ Email: "maintainer@example.com",
+ GlobalRole: ptr.String(fleet.RoleMaintainer),
+ }
+ password := test.GoodPassword
+ require.NoError(t, u.SetPassword(password, 10, 10))
+ _, err := s.ds.NewUser(context.Background(), u)
+ require.NoError(t, err)
+ s.token = s.getTestToken("maintainer@example.com", password)
+ var r conditionalAccessMicrosoftCreateResponse
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{}, http.StatusForbidden, &r)
+ var c conditionalAccessMicrosoftConfirmResponse
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{}, http.StatusForbidden, &c)
+ var d conditionalAccessMicrosoftDeleteResponse
+ s.DoJSON("DELETE", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftDeleteRequest{}, http.StatusForbidden, &d)
+
+ // Restore token for global admin.
+ s.token = s.getTestAdminToken()
+
+ // Setup integration.
+ mockedConditionalAccessMicrosoftProxyInstance.getResponse = &conditional_access_microsoft_proxy.GetResponse{
+ TenantID: "foobar",
+ SetupDone: false,
+ AdminConsentURL: "https://example.com",
+ }
+ mockedConditionalAccessMicrosoftProxyInstance.createResponse = &conditional_access_microsoft_proxy.CreateResponse{
+ TenantID: "foobar",
+ Secret: "secret",
+ }
+ r = conditionalAccessMicrosoftCreateResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{
+ MicrosoftTenantID: "foobar",
+ }, http.StatusOK, &r)
+ require.Equal(t, "https://example.com", r.MicrosoftAuthenticationURL)
+
+ // UI uses the /config endpoint to know the status of the integration.
+ acResp = appConfigResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.NotNil(t, acResp)
+ require.NotNil(t, acResp.ConditionalAccess)
+ require.Equal(t, "foobar", acResp.ConditionalAccess.MicrosoftEntraTenantID)
+ require.False(t, acResp.ConditionalAccess.MicrosoftEntraConnectionConfigured)
+
+ // Confirm should return that the setup is not done.
+ c = conditionalAccessMicrosoftConfirmResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{}, http.StatusOK, &c)
+ require.False(t, c.ConfigurationCompleted)
+
+ // Confirm now should succeed.
+ mockedConditionalAccessMicrosoftProxyInstance.getResponse = &conditional_access_microsoft_proxy.GetResponse{
+ TenantID: "foobar",
+ SetupDone: true,
+ }
+ c = conditionalAccessMicrosoftConfirmResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{}, http.StatusOK, &c)
+ require.True(t, c.ConfigurationCompleted)
+ // Confirm again should succeed because integration is done.
+ c = conditionalAccessMicrosoftConfirmResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{}, http.StatusOK, &c)
+ require.True(t, c.ConfigurationCompleted)
+ // Create will succeed if using the same tenant ID.
+ r = conditionalAccessMicrosoftCreateResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{
+ MicrosoftTenantID: "foobar",
+ }, http.StatusOK, &r)
+ // Create will should fail if using the a different tenant ID (if the setup is done).
+ r = conditionalAccessMicrosoftCreateResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{
+ MicrosoftTenantID: "zoobar",
+ }, http.StatusBadRequest, &r)
+
+ // Test app config returns that the configuration is done.
+ acResp = appConfigResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.NotNil(t, acResp)
+ require.NotNil(t, acResp.ConditionalAccess)
+ require.Equal(t, "foobar", acResp.ConditionalAccess.MicrosoftEntraTenantID)
+ require.True(t, acResp.ConditionalAccess.MicrosoftEntraConnectionConfigured)
+
+ // Delete endpoint.
+ mockedConditionalAccessMicrosoftProxyInstance.deleteResponse = &conditional_access_microsoft_proxy.DeleteResponse{}
+ d = conditionalAccessMicrosoftDeleteResponse{}
+ s.DoJSON("DELETE", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftDeleteRequest{}, http.StatusOK, &d)
+ // Deleting again should fail with bad request.
+ d = conditionalAccessMicrosoftDeleteResponse{}
+ s.DoJSON("DELETE", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftDeleteRequest{}, http.StatusBadRequest, &d)
+
+ // Test app config returns that the integration was deleted.
+ acResp = appConfigResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.NotNil(t, acResp)
+ require.Nil(t, acResp.ConditionalAccess)
+
+ // Create again with a different tenant.
+ mockedConditionalAccessMicrosoftProxyInstance.getResponse = &conditional_access_microsoft_proxy.GetResponse{
+ TenantID: "zoobar",
+ SetupDone: false,
+ AdminConsentURL: "https://example.com",
+ }
+ mockedConditionalAccessMicrosoftProxyInstance.createResponse = &conditional_access_microsoft_proxy.CreateResponse{
+ TenantID: "zoobar",
+ Secret: "secret",
+ }
+ r = conditionalAccessMicrosoftCreateResponse{}
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{
+ MicrosoftTenantID: "zoobar",
+ }, http.StatusOK, &r)
+
+ // Test app config returns that the new integration was created.
+ acResp = appConfigResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.NotNil(t, acResp)
+ require.NotNil(t, acResp.ConditionalAccess)
+ require.Equal(t, "zoobar", acResp.ConditionalAccess.MicrosoftEntraTenantID)
+ require.False(t, acResp.ConditionalAccess.MicrosoftEntraConnectionConfigured)
+
+ // Simulate a not found error on the proxy (should allow deletion to start over).
+ mockedConditionalAccessMicrosoftProxyInstance.deleteErr = ¬FoundError{}
+ d = conditionalAccessMicrosoftDeleteResponse{}
+ s.DoJSON("DELETE", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftDeleteRequest{}, http.StatusOK, &d)
+
+ // Test app config returns that the configuration is gone.
+ acResp = appConfigResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.NotNil(t, acResp)
+ require.Nil(t, acResp.ConditionalAccess)
+}
+
+func (s *integrationEnterpriseTestSuite) TestConditionalAccessPolicies() {
+ t := s.T()
+
+ // Setup integration.
+ mockedConditionalAccessMicrosoftProxyInstance.getResponse = &conditional_access_microsoft_proxy.GetResponse{
+ TenantID: "foobar",
+ SetupDone: false,
+ AdminConsentURL: "https://example.com",
+ }
+ mockedConditionalAccessMicrosoftProxyInstance.createResponse = &conditional_access_microsoft_proxy.CreateResponse{
+ TenantID: "foobar",
+ Secret: "secret",
+ }
+ var r conditionalAccessMicrosoftCreateResponse
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft", conditionalAccessMicrosoftCreateRequest{
+ MicrosoftTenantID: "foobar",
+ }, http.StatusOK, &r)
+ mockedConditionalAccessMicrosoftProxyInstance.getResponse = &conditional_access_microsoft_proxy.GetResponse{
+ TenantID: "foobar",
+ SetupDone: true,
+ }
+ var c conditionalAccessMicrosoftConfirmResponse
+ s.DoJSON("POST", "/api/latest/fleet/conditional-access/microsoft/confirm", conditionalAccessMicrosoftConfirmRequest{}, http.StatusOK, &c)
+ require.True(t, c.ConfigurationCompleted)
+
+ t1, err := s.ds.NewTeam(context.Background(), &fleet.Team{
+ Name: "team1",
+ Description: "desc team1",
+ })
+ require.NoError(t, err)
+
+ var pr teamPolicyResponse
+ s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", t1.ID), teamPolicyRequest{
+ Query: "SELECT 1;",
+ Name: "Compliance check 1",
+ ConditionalAccessEnabled: true,
+ }, http.StatusOK, &pr)
+ cp1 := pr.Policy
+ pr = teamPolicyResponse{}
+ s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", t1.ID), teamPolicyRequest{
+ Query: "SELECT 2;",
+ Name: "Compliance check 2",
+ ConditionalAccessEnabled: true,
+ }, http.StatusOK, &pr)
+ cp2 := pr.Policy
+ pr = teamPolicyResponse{}
+ s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", t1.ID), teamPolicyRequest{
+ Query: "SELECT 3;",
+ Name: "Other policy",
+ ConditionalAccessEnabled: false,
+ }, http.StatusOK, &pr)
+ p3 := pr.Policy
+
+ ctx := context.Background()
+ newHost := func(name string, teamID *uint) *fleet.Host {
+ h, err := s.ds.NewHost(ctx, &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now().Add(-1 * time.Minute),
+ OsqueryHostID: ptr.String(t.Name() + name),
+ NodeKey: ptr.String(t.Name() + name),
+ UUID: uuid.New().String(),
+ Hostname: fmt.Sprintf("%s.%s.local", name, t.Name()),
+ Platform: "darwin",
+ TeamID: teamID,
+ })
+ require.NoError(t, err)
+ return h
+ }
+
+ //
+ // Test A: host h1 in team t1.
+ //
+
+ h1 := newHost("h1", &t1.ID)
+ orbitKey := setOrbitEnrollment(t, h1, s.ds)
+ h1.OrbitNodeKey = &orbitKey
+
+ // Feature is disabled on the host's team.
+ distributedResp := submitDistributedQueryResultsResponse{}
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithEntraIDDetails(h1, "entraDeviceID", "entraUserPrincipalName"), http.StatusOK, &distributedResp)
+ _, err = s.ds.LoadHostConditionalAccessStatus(ctx, h1.ID)
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+
+ // Enable feature on team.
+ var tmResp teamResponse
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", t1.ID), map[string]any{
+ "integrations": map[string]any{
+ "conditional_access_enabled": true,
+ },
+ }, http.StatusOK, &tmResp)
+
+ distributedResp = submitDistributedQueryResultsResponse{}
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithEntraIDDetails(h1, "entraDeviceID", "entraUserPrincipalName"), http.StatusOK, &distributedResp)
+ h1s, err := s.ds.LoadHostConditionalAccessStatus(ctx, h1.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID", h1s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName", h1s.UserPrincipalName)
+ require.Nil(t, h1s.Managed)
+ require.Nil(t, h1s.Compliant)
+
+ // override value to reduce test time.
+ conditionalAccessSetWaitTime = 250 * time.Millisecond
+ mockedConditionalAccessMicrosoftProxyInstance.setComplianceStatusFunc = func(
+ ctx context.Context,
+ tenantID string, secret string,
+ deviceID string,
+ userPrincipalName string,
+ mdmEnrolled bool,
+ deviceName, osName, osVersion string,
+ compliant bool,
+ lastCheckInTime time.Time,
+ ) (*conditional_access_microsoft_proxy.SetComplianceStatusResponse, error) {
+ return &conditional_access_microsoft_proxy.SetComplianceStatusResponse{
+ MessageID: "messageID",
+ }, nil
+ }
+
+ setDone := make(chan struct{})
+
+ mockedConditionalAccessMicrosoftProxyInstance.getMessageStatusFunc = func(
+ ctx context.Context,
+ tenantID string,
+ secret string,
+ messageID string,
+ ) (*conditional_access_microsoft_proxy.GetMessageStatusResponse, error) {
+ close(setDone)
+ return &conditional_access_microsoft_proxy.GetMessageStatusResponse{
+ MessageID: "messageID",
+ Status: "Completed",
+ }, nil
+ }
+
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ h1,
+ map[uint]*bool{
+ cp1.ID: ptr.Bool(true),
+ cp2.ID: ptr.Bool(true),
+ p3.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+
+ select {
+ case <-setDone:
+ time.Sleep(1 * time.Second)
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for compliance to be set")
+ }
+
+ h1s, err = s.ds.LoadHostConditionalAccessStatus(ctx, h1.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID", h1s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName", h1s.UserPrincipalName)
+ require.NotNil(t, h1s.Managed)
+ require.False(t, *h1s.Managed) // not MDM enrolled
+ require.NotNil(t, h1s.Compliant)
+ require.True(t, *h1s.Compliant) // the two configured policies are passing
+
+ // Enroll to MDM to update managed status.
+ err = s.ds.SetOrUpdateMDMData(ctx,
+ h1.ID, false, true /* enrolled */, s.server.URL, false, /* installedFromDEP */
+ "Fleet" /* MDM name */, "", /* fleetEnrollmentRef */
+ )
+ require.NoError(t, err)
+
+ setDone = make(chan struct{})
+
+ // Publish same policy results.
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ h1,
+ map[uint]*bool{
+ cp1.ID: ptr.Bool(true),
+ cp2.ID: ptr.Bool(true),
+ p3.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+
+ select {
+ case <-setDone:
+ time.Sleep(1 * time.Second)
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for compliance to be set")
+ }
+
+ h1s, err = s.ds.LoadHostConditionalAccessStatus(ctx, h1.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID", h1s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName", h1s.UserPrincipalName)
+ require.NotNil(t, h1s.Managed)
+ require.True(t, *h1s.Managed) // now should be MDM enrolled
+ require.NotNil(t, h1s.Compliant)
+ require.True(t, *h1s.Compliant) // the two configured policies are passing
+
+ setDone = make(chan struct{})
+
+ // Now the host is failing a compliance policy.
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ h1,
+ map[uint]*bool{
+ cp1.ID: ptr.Bool(true),
+ cp2.ID: ptr.Bool(false),
+ p3.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+
+ select {
+ case <-setDone:
+ time.Sleep(1 * time.Second)
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for compliance to be set")
+ }
+
+ h1s, err = s.ds.LoadHostConditionalAccessStatus(ctx, h1.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID", h1s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName", h1s.UserPrincipalName)
+ require.NotNil(t, h1s.Managed)
+ require.True(t, *h1s.Managed)
+ require.NotNil(t, h1s.Compliant)
+ require.False(t, *h1s.Compliant) // now the host is non-compliant
+
+ // Now nothing changes so there's no compliance operation on the proxy.
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ h1,
+ map[uint]*bool{
+ cp1.ID: ptr.Bool(true),
+ cp2.ID: ptr.Bool(false),
+ p3.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+
+ time.Sleep(5 * time.Second)
+
+ h1s, err = s.ds.LoadHostConditionalAccessStatus(ctx, h1.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID", h1s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName", h1s.UserPrincipalName)
+ require.NotNil(t, h1s.Managed)
+ require.True(t, *h1s.Managed)
+ require.NotNil(t, h1s.Compliant)
+ require.False(t, *h1s.Compliant)
+
+ //
+ // Test B: host h2 in "No team".
+ //
+
+ h2 := newHost("h2", nil)
+ orbitKey2 := setOrbitEnrollment(t, h2, s.ds)
+ h2.OrbitNodeKey = &orbitKey2
+
+ // "No team" configuration for conditional access is in global config.
+ s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(`{
+ "integrations": {
+ "conditional_access_enabled": true
+ }
+ }`), http.StatusOK)
+ // Test that by not setting it it's not disabled.
+ s.DoRaw("PATCH", "/api/v1/fleet/config", []byte(`{
+ "integrations": {}
+ }`), http.StatusOK)
+ acResp := appConfigResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
+ require.NotNil(t, acResp)
+ require.True(t, acResp.Integrations.ConditionalAccessEnabled.Set)
+ require.True(t, acResp.Integrations.ConditionalAccessEnabled.Value)
+
+ pr = teamPolicyResponse{}
+ s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", fleet.PolicyNoTeamID), teamPolicyRequest{
+ Query: "SELECT 1;",
+ Name: "Compliance check 1",
+ ConditionalAccessEnabled: true,
+ }, http.StatusOK, &pr)
+ cp1 = pr.Policy
+ pr = teamPolicyResponse{}
+ s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", fleet.PolicyNoTeamID), teamPolicyRequest{
+ Query: "SELECT 2;",
+ Name: "Other 1",
+ ConditionalAccessEnabled: false,
+ }, http.StatusOK, &pr)
+ p2 := pr.Policy
+ pr = teamPolicyResponse{}
+
+ // Enroll to MDM to update managed status.
+ err = s.ds.SetOrUpdateMDMData(ctx,
+ h2.ID, false, true /* enrolled */, s.server.URL, false, /* installedFromDEP */
+ "Fleet" /* MDM name */, "", /* fleetEnrollmentRef */
+ )
+ require.NoError(t, err)
+
+ // Ingest device ID and user principal name.
+ distributedResp = submitDistributedQueryResultsResponse{}
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithEntraIDDetails(h2, "entraDeviceID2", "entraUserPrincipalName2"), http.StatusOK, &distributedResp)
+ h2s, err := s.ds.LoadHostConditionalAccessStatus(ctx, h2.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID2", h2s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName2", h2s.UserPrincipalName)
+ require.Nil(t, h2s.Managed)
+ require.Nil(t, h2s.Compliant)
+
+ setDone = make(chan struct{})
+
+ // Now the host is failing a the compliance policy.
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ h2,
+ map[uint]*bool{
+ cp1.ID: ptr.Bool(false),
+ p2.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+
+ select {
+ case <-setDone:
+ time.Sleep(1 * time.Second)
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for compliance to be set")
+ }
+
+ h2s, err = s.ds.LoadHostConditionalAccessStatus(ctx, h2.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID2", h2s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName2", h2s.UserPrincipalName)
+ require.NotNil(t, h2s.Managed)
+ require.True(t, *h2s.Managed)
+ require.NotNil(t, h2s.Compliant)
+ require.False(t, *h2s.Compliant) // host is non-compliant
+
+ // Delete compliance policy, now there should be no compliance policies so host should be compliant.
+ _, err = s.ds.DeleteTeamPolicies(ctx, fleet.PolicyNoTeamID, []uint{cp1.ID})
+ require.NoError(t, err)
+
+ setDone = make(chan struct{})
+
+ // Now the host is failing a compliance policy.
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ h2,
+ map[uint]*bool{
+ p2.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+
+ select {
+ case <-setDone:
+ time.Sleep(1 * time.Second)
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for compliance to be set")
+ }
+
+ h2s, err = s.ds.LoadHostConditionalAccessStatus(ctx, h2.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID2", h2s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName2", h2s.UserPrincipalName)
+ require.NotNil(t, h2s.Managed)
+ require.True(t, *h2s.Managed)
+ require.NotNil(t, h2s.Compliant)
+ require.True(t, *h2s.Compliant) // now the host is compliant
+
+ // A change of device ID and user principal name should update and clear the managed and compliant values.
+ distributedResp = submitDistributedQueryResultsResponse{}
+ s.DoJSON("POST", "/api/osquery/distributed/write", genDistributedReqWithEntraIDDetails(h2, "entraDeviceID3", "entraUserPrincipalName3"), http.StatusOK, &distributedResp)
+ h2s, err = s.ds.LoadHostConditionalAccessStatus(ctx, h2.ID)
+ require.NoError(t, err)
+ require.Equal(t, "entraDeviceID3", h2s.DeviceID)
+ require.Equal(t, "entraUserPrincipalName3", h2s.UserPrincipalName)
+ require.Nil(t, h2s.Managed)
+ require.Nil(t, h2s.Compliant)
+}
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index cdcc5f06d9..389cd21558 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -5438,7 +5438,7 @@ func (s *integrationMDMTestSuite) TestSSO() {
ac, err := s.ds.AppConfig(context.Background())
require.NoError(t, err)
- detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features)
+ detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features, osquery_utils.Integrations{})
// simulate osquery reporting mdm information
rows := []map[string]string{
@@ -5766,7 +5766,7 @@ func (s *integrationMDMTestSuite) TestSSOWithSCIM() {
ac, err := s.ds.AppConfig(context.Background())
require.NoError(t, err)
- detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features)
+ detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features, osquery_utils.Integrations{})
// simulate osquery reporting mdm information, doesn't change anything
rows := []map[string]string{
diff --git a/server/service/osquery.go b/server/service/osquery.go
index b9f659b320..d989db8c18 100644
--- a/server/service/osquery.go
+++ b/server/service/osquery.go
@@ -22,6 +22,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/pubsub"
+ "github.com/fleetdm/fleet/v4/server/service/conditional_access_microsoft_proxy"
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
kithttp "github.com/go-kit/kit/transport/http"
@@ -150,7 +151,9 @@ func (svc *Service) EnrollAgent(ctx context.Context, enrollSecret, hostIdentifie
}
// Save enrollment details if provided
- detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features)
+ detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features, osquery_utils.Integrations{
+ ConditionalAccessMicrosoft: false, // here we are just using a few ingestion functions, so no need to set.
+ })
save := false
if r, ok := hostDetails["os_version"]; ok {
err := detailQueries["os_version"].IngestFunc(ctx, svc.logger, host, []map[string]string{r})
@@ -697,7 +700,9 @@ func (svc *Service) detailQueriesForHost(ctx context.Context, host *fleet.Host)
queries = make(map[string]string)
discovery = make(map[string]string)
- detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features)
+ detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features, osquery_utils.Integrations{
+ ConditionalAccessMicrosoft: svc.hostRequiresConditionalAccessMicrosoftIngestion(ctx, host),
+ })
for name, query := range detailQueries {
if criticalQueriesOnly && !criticalDetailQueries[name] {
continue
@@ -743,6 +748,24 @@ func (svc *Service) detailQueriesForHost(ctx context.Context, host *fleet.Host)
return queries, discovery, nil
}
+func (svc *Service) hostRequiresConditionalAccessMicrosoftIngestion(ctx context.Context, host *fleet.Host) bool {
+ if host.Platform != "darwin" {
+ return false
+ }
+
+ conditionalAccessConfigured, conditionalAccessEnabledForTeam, err := svc.conditionalAccessConfiguredAndEnabledForTeam(ctx, host.TeamID)
+ if err != nil {
+ level.Error(svc.logger).Log(
+ "msg", "load conditional access configured and enabled, skipping ingestion",
+ "host_id", host.ID,
+ "err", err,
+ )
+ return false
+ }
+
+ return conditionalAccessConfigured && conditionalAccessEnabledForTeam
+}
+
func (svc *Service) shouldUpdate(lastUpdated time.Time, interval time.Duration, hostID uint) bool {
svc.jitterMu.Lock()
defer svc.jitterMu.Unlock()
@@ -1006,7 +1029,6 @@ func (svc *Service) SubmitDistributedQueryResults(
}
if len(policyResults) > 0 {
-
if err := processCalendarPolicies(ctx, svc.ds, ac, host, policyResults, svc.logger); err != nil {
logging.WithErr(ctx, err)
}
@@ -1015,6 +1037,12 @@ func (svc *Service) SubmitDistributedQueryResults(
logging.WithErr(ctx, err)
}
+ if host.Platform == "darwin" {
+ if err := svc.processConditionalAccessForNewlyFailingPolicies(ctx, host.ID, host.TeamID, host.OrbitNodeKey, policyResults); err != nil {
+ logging.WithErr(ctx, err)
+ }
+ }
+
if host.Platform == "darwin" && svc.EnterpriseOverrides != nil {
// NOTE: if the installers for the policies here are not scoped to the host via labels, we update the policy status here to stop it from showing up as "failed" in the
// host details.
@@ -1518,7 +1546,9 @@ func (svc *Service) directIngestDetailQuery(ctx context.Context, host *fleet.Hos
return false, newOsqueryError("ingest detail query: " + err.Error())
}
- detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features)
+ detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features, osquery_utils.Integrations{
+ ConditionalAccessMicrosoft: svc.hostRequiresConditionalAccessMicrosoftIngestion(ctx, host),
+ })
query, ok := detailQueries[name]
if !ok {
return false, newOsqueryError("unknown detail query " + name)
@@ -1660,7 +1690,9 @@ func (svc *Service) ingestDetailQuery(ctx context.Context, host *fleet.Host, nam
return newOsqueryError("ingest detail query: " + err.Error())
}
- detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features)
+ detailQueries := osquery_utils.GetDetailQueries(ctx, svc.config, appConfig, features, osquery_utils.Integrations{
+ ConditionalAccessMicrosoft: svc.hostRequiresConditionalAccessMicrosoftIngestion(ctx, host),
+ })
query, ok := detailQueries[name]
if !ok {
return newOsqueryError("unknown detail query " + name)
@@ -2192,6 +2224,238 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
return nil
}
+func (svc *Service) conditionalAccessConfiguredAndEnabledForTeam(ctx context.Context, hostTeamID *uint) (configured bool, enabledForTeam bool, err error) {
+ // Check if the needed server configuration for Conditional Access is set.
+ if !svc.config.MicrosoftCompliancePartner.IsSet() {
+ return false, false, nil
+ }
+
+ // Check if the integration is fully configured.
+ integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ if fleet.IsNotFound(err) {
+ return false, false, nil
+ }
+ return false, false, ctxerr.Wrap(ctx, err, "failed to load the integration")
+ }
+ if !integration.SetupDone {
+ return false, false, nil
+ }
+
+ if hostTeamID == nil {
+ // Configuration for "No team" is stored in the main appconfig.
+ cfg, err := svc.ds.AppConfig(ctx)
+ if err != nil {
+ return false, false, ctxerr.Wrap(ctx, err, "failed to load appconfig")
+ }
+ var conditionalAccessEnabled bool
+ if cfg.Integrations.ConditionalAccessEnabled.Set {
+ conditionalAccessEnabled = cfg.Integrations.ConditionalAccessEnabled.Value
+ }
+ return true, conditionalAccessEnabled, nil
+ }
+
+ // Host belongs to a team, thus we load the team configuration.
+ team, err := svc.ds.Team(ctx, *hostTeamID)
+ if err != nil {
+ return false, false, ctxerr.Wrap(ctx, err, "failed to load team config")
+ }
+ var teamConditionalAccessEnabled bool
+ if team.Config.Integrations.ConditionalAccessEnabled.Set {
+ teamConditionalAccessEnabled = team.Config.Integrations.ConditionalAccessEnabled.Value
+ }
+ return true, teamConditionalAccessEnabled, nil
+}
+
+func (svc *Service) processConditionalAccessForNewlyFailingPolicies(
+ ctx context.Context,
+ hostID uint,
+ hostTeamID *uint,
+ hostOrbitNodeKey *string,
+ incomingPolicyResults map[uint]*bool,
+) error {
+ if hostOrbitNodeKey == nil || *hostOrbitNodeKey == "" {
+ // Vanilla osquery hosts cannot do conditional access.
+ return nil
+ }
+
+ configured, enabledForTeam, err := svc.conditionalAccessConfiguredAndEnabledForTeam(ctx, hostTeamID)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "failed to check for conditional access configuration")
+ }
+
+ if !configured || !enabledForTeam {
+ // Nothing to do, feature not configured or not enabled for this host's team.
+ return nil
+ }
+
+ hostConditionalAccessStatus, err := svc.ds.LoadHostConditionalAccessStatus(ctx, hostID)
+ if err != nil {
+ if fleet.IsNotFound(err) {
+ // Nothing to do because Fleet hasn't ingested the Entra's "Device ID" or
+ // "User Principal Name" from the device yet (we cannot perform any actions
+ // for the host on Entra without it).
+ return nil
+ }
+ return ctxerr.Wrap(ctx, err, "failed to load host conditional access status")
+ }
+
+ var policyTeamID uint
+ if hostTeamID == nil {
+ policyTeamID = fleet.PolicyNoTeamID
+ } else {
+ policyTeamID = *hostTeamID
+ }
+
+ var mdmEnrolled bool
+ hostMDM, err := svc.ds.GetHostMDM(ctx, hostID)
+ if err != nil {
+ // If GetHostMDM returns not found then it means that
+ // the host may not be MDM enrolled yet.
+ if !fleet.IsNotFound(err) {
+ return ctxerr.Wrap(ctx, err, "failed to get host mdm")
+ }
+ } else {
+ mdmEnrolled = hostMDM.Enrolled
+ }
+
+ // Get policies configured for conditional access.
+ conditionalAccessPolicyIDs, err := svc.ds.GetPoliciesForConditionalAccess(ctx, policyTeamID)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "failed to get policies with conditional access")
+ }
+
+ hostIsCompliantInFleet := true
+ conditionalAccessPolicyIDsSet := make(map[uint]struct{}, len(conditionalAccessPolicyIDs))
+ for _, policyID := range conditionalAccessPolicyIDs {
+ conditionalAccessPolicyIDsSet[policyID] = struct{}{}
+ }
+ for incomingPolicyID, incomingPolicyResult := range incomingPolicyResults {
+ if _, ok := conditionalAccessPolicyIDsSet[incomingPolicyID]; !ok {
+ // Ignore results for policies that are not for conditional access.
+ continue
+ }
+ if incomingPolicyResult != nil && !*incomingPolicyResult {
+ hostIsCompliantInFleet = false
+ break
+ }
+ }
+
+ if hostConditionalAccessStatus.Managed != nil && mdmEnrolled == *hostConditionalAccessStatus.Managed &&
+ hostConditionalAccessStatus.Compliant != nil && hostIsCompliantInFleet == *hostConditionalAccessStatus.Compliant {
+ // Nothing to do, nothing has changed.
+ return nil
+ }
+
+ svc.setHostConditionalAccessAsync(hostID, hostConditionalAccessStatus, mdmEnrolled, hostIsCompliantInFleet)
+
+ return nil
+}
+
+func (svc *Service) setHostConditionalAccessAsync(
+ hostID uint,
+ hostConditionalAccessStatus *fleet.HostConditionalAccessStatus,
+ managed bool,
+ compliant bool,
+) {
+ go func() {
+ logger := log.With(svc.logger,
+ "msg", "set host conditional access",
+ "host_id", hostID,
+ "managed", managed,
+ "compliant", compliant,
+ )
+ start := time.Now()
+ if err := svc.setHostConditionalAccess(hostID, hostConditionalAccessStatus, managed, compliant); err != nil {
+ level.Error(logger).Log("took", time.Since(start), "err", err)
+ }
+ level.Debug(logger).Log("took", time.Since(start))
+ }()
+}
+
+// conditionalAccessSetWaitTime is the interval to check for message status.
+// It's a global variable to be set in tests.
+var conditionalAccessSetWaitTime = 10 * time.Second
+
+func (svc *Service) setHostConditionalAccess(
+ hostID uint,
+ hostConditionalAccessStatus *fleet.HostConditionalAccessStatus,
+ managed bool,
+ compliant bool,
+) error {
+ ctx := context.Background()
+
+ integration, err := svc.ds.ConditionalAccessMicrosoftGet(ctx)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "get integration")
+ }
+ logger := log.With(svc.logger,
+ "msg", "set compliance status",
+ "host_id", hostID,
+ "managed", managed,
+ "compliant", compliant,
+ )
+ level.Debug(logger).Log()
+ response, err := svc.conditionalAccessMicrosoftProxy.SetComplianceStatus(ctx,
+ integration.TenantID,
+ integration.ProxyServerSecret,
+
+ hostConditionalAccessStatus.DeviceID,
+ hostConditionalAccessStatus.UserPrincipalName,
+
+ managed,
+ hostConditionalAccessStatus.DisplayName,
+ "macOS",
+ hostConditionalAccessStatus.OSVersion,
+ compliant,
+ time.Now().UTC(),
+ )
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "failed to set compliance status")
+ }
+ const (
+ timeout = 1 * time.Minute
+ )
+ level.Debug(logger).Log("msg", "set compliance status message sent")
+ startTime := time.Now()
+ for range time.Tick(conditionalAccessSetWaitTime) {
+ if time.Since(startTime) > timeout {
+ return ctxerr.Errorf(ctx, "timeout waiting for message after %s", time.Since(startTime))
+ }
+ level.Debug(logger).Log("msg", "get compliance status message wait")
+ messageStatus, err := svc.conditionalAccessMicrosoftProxy.GetMessageStatus(ctx,
+ integration.TenantID, integration.ProxyServerSecret, response.MessageID,
+ )
+ if err != nil {
+ // Retry again in case of network or transient errors.
+ level.Info(logger).Log("msg", "get message status, retrying", "err", err)
+ continue
+ }
+ if messageStatus.Status == conditional_access_microsoft_proxy.MessageStatusCompleted {
+ level.Debug(logger).Log(
+ "msg", "set device compliance status completed",
+ "took", time.Since(startTime),
+ )
+ break
+ }
+ detail := ""
+ if messageStatus.Detail != nil {
+ detail = *messageStatus.Detail
+ }
+ level.Info(logger).Log(
+ "msg", "get message status, retrying",
+ "status", messageStatus.Status,
+ "detail", detail,
+ )
+ }
+
+ if err := svc.ds.SetHostConditionalAccessStatus(ctx, hostID, managed, compliant); err != nil {
+ return ctxerr.Wrap(ctx, err, "set conditional access status on datastore")
+ }
+
+ return nil
+}
+
func (svc *Service) maybeDebugHost(
ctx context.Context,
host *fleet.Host,
diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go
index 77064c08e5..e8e92d8626 100644
--- a/server/service/osquery_test.go
+++ b/server/service/osquery_test.go
@@ -263,6 +263,7 @@ var allDetailQueries = osquery_utils.GetDetailQueries(
EnableHostUsers: true,
EnableSoftwareInventory: true,
},
+ osquery_utils.Integrations{},
)
func expectedDetailQueriesForPlatform(platform string) map[string]osquery_utils.DetailQuery {
diff --git a/server/service/osquery_utils/gen_queries_doc.go b/server/service/osquery_utils/gen_queries_doc.go
index 39ea973a57..6555c2c183 100644
--- a/server/service/osquery_utils/gen_queries_doc.go
+++ b/server/service/osquery_utils/gen_queries_doc.go
@@ -30,6 +30,9 @@ func main() {
EnableSoftwareInventory: true,
EnableHostUsers: true,
},
+ osquery_utils.Integrations{
+ ConditionalAccessMicrosoft: true,
+ },
)
var b strings.Builder
diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go
index 3265382528..506913d521 100644
--- a/server/service/osquery_utils/queries.go
+++ b/server/service/osquery_utils/queries.go
@@ -859,6 +859,15 @@ var windowsUpdateHistory = DetailQuery{
DirectIngestFunc: directIngestWindowsUpdateHistory,
}
+// entraIDDetails holds the query and ingestion function for Microsoft "Conditional access" feature.
+var entraIDDetails = DetailQuery{
+ // The query ingests Entra's Device ID and User Principal Name of the account that logged in to the device (using Company Portal.app).
+ Query: `SELECT * FROM (SELECT common_name AS device_id FROM certificates WHERE issuer LIKE '/DC=net+DC=windows+CN=MS-Organization-Access+OU%' LIMIT 1)
+ CROSS JOIN (SELECT label as user_principal_name FROM keychain_items WHERE account = 'com.microsoft.workplacejoin.registeredUserPrincipalName' LIMIT 1);`,
+ Platforms: []string{"darwin"},
+ DirectIngestFunc: directIngestEntraIDDetails,
+}
+
var softwareMacOS = DetailQuery{
// Note that we create the cached_users CTE (the WITH clause) in order to suggest to SQLite
// that it generates the users once instead of once for each UNIONed query. We use CROSS JOIN to
@@ -1518,6 +1527,34 @@ func directIngestWindowsUpdateHistory(
return ds.InsertWindowsUpdates(ctx, host.ID, updates)
}
+func directIngestEntraIDDetails(
+ ctx context.Context,
+ logger log.Logger,
+ host *fleet.Host,
+ ds fleet.Datastore,
+ rows []map[string]string,
+) error {
+ if len(rows) == 0 {
+ // Device maybe hasn't logged in to Entra ID yet.
+ return nil
+ }
+ row := rows[0]
+
+ deviceID := row["device_id"]
+ if deviceID == "" {
+ return ctxerr.New(ctx, "empty Entra ID device_id")
+ }
+ userPrincipalName := row["user_principal_name"]
+ if userPrincipalName == "" {
+ return ctxerr.New(ctx, "empty Entra ID user_principal_name")
+ }
+
+ if err := ds.CreateHostConditionalAccessStatus(ctx, host.ID, deviceID, userPrincipalName); err != nil {
+ return ctxerr.Wrap(ctx, err, "failed to create host conditional access status")
+ }
+ return nil
+}
+
func directIngestScheduledQueryStats(ctx context.Context, logger log.Logger, host *fleet.Host, task *async.Task, rows []map[string]string) error {
packs := map[string][]fleet.ScheduledQueryStats{}
for _, row := range rows {
@@ -2231,11 +2268,16 @@ var luksVerifyQueryIngester = func(decrypter func(string) (string, error)) func(
//go:generate go run gen_queries_doc.go "../../../docs/Contributing/product-groups/orchestration/understanding-host-vitals.md"
+type Integrations struct {
+ ConditionalAccessMicrosoft bool
+}
+
func GetDetailQueries(
ctx context.Context,
fleetConfig config.FleetConfig,
appConfig *fleet.AppConfig,
features *fleet.Features,
+ integrations Integrations,
) map[string]DetailQuery {
generatedMap := make(map[string]DetailQuery)
for key, query := range hostDetailQueries {
@@ -2281,6 +2323,10 @@ func GetDetailQueries(
}
}
+ if integrations.ConditionalAccessMicrosoft {
+ generatedMap["conditional_access_microsoft_device_id"] = entraIDDetails
+ }
+
if appConfig != nil && appConfig.MDM.EnableDiskEncryption.Value {
luksVerifyQuery.DirectIngestFunc = luksVerifyQueryIngester(func(privateKey string) func(string) (string, error) {
return func(encrypted string) (string, error) {
diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go
index 93c74e0006..cd5c06539b 100644
--- a/server/service/osquery_utils/queries_test.go
+++ b/server/service/osquery_utils/queries_test.go
@@ -38,7 +38,7 @@ func TestDetailQueryNetworkInterfaces(t *testing.T) {
var initialHost fleet.Host
host := initialHost
- ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["network_interface_unix"].IngestFunc
+ ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil, Integrations{})["network_interface_unix"].IngestFunc
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
assert.Equal(t, initialHost, host)
@@ -84,7 +84,7 @@ func TestDetailQueryScheduledQueryStats(t *testing.T) {
return nil
}
- ingest := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, nil)["scheduled_query_stats"].DirectTaskIngestFunc
+ ingest := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, nil, Integrations{})["scheduled_query_stats"].DirectTaskIngestFunc
ctx := context.Background()
assert.NoError(t, ingest(ctx, log.NewNopLogger(), &host, task, nil))
@@ -263,7 +263,7 @@ func sortedKeysCompare(t *testing.T, m map[string]DetailQuery, expectedKeys []st
}
func TestGetDetailQueries(t *testing.T) {
- queriesNoConfig := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)
+ queriesNoConfig := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil, Integrations{})
baseQueries := []string{
"network_interface_unix",
@@ -298,16 +298,16 @@ func TestGetDetailQueries(t *testing.T) {
require.Len(t, queriesNoConfig, len(baseQueries))
sortedKeysCompare(t, queriesNoConfig, baseQueries)
- queriesWithoutWinOSVuln := GetDetailQueries(context.Background(), config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil, nil)
+ queriesWithoutWinOSVuln := GetDetailQueries(context.Background(), config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil, nil, Integrations{})
require.Len(t, queriesWithoutWinOSVuln, 26)
- queriesWithUsers := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true})
+ queriesWithUsers := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true}, Integrations{})
qs := baseQueries
qs = append(qs, "users", "users_chrome", "scheduled_query_stats")
require.Len(t, queriesWithUsers, len(qs))
sortedKeysCompare(t, queriesWithUsers, qs)
- queriesWithUsersAndSoftware := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true})
+ queriesWithUsersAndSoftware := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}, Integrations{})
qs = baseQueries
qs = append(qs, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions",
"software_chrome", "software_python_packages", "software_python_packages_with_users_dir", "scheduled_query_stats", "software_macos_firefox", "software_macos_codesign")
@@ -327,14 +327,14 @@ func TestGetDetailQueries(t *testing.T) {
ac := fleet.AppConfig{}
ac.MDM.EnabledAndConfigured = true
// windows mdm is disabled by default, windows mdm queries should not be present
- gotQueries := GetDetailQueries(context.Background(), config.FleetConfig{}, &ac, nil)
+ gotQueries := GetDetailQueries(context.Background(), config.FleetConfig{}, &ac, nil, Integrations{})
wantQueries := baseQueries
wantQueries = append(wantQueries, mdmQueriesBase...)
require.Len(t, gotQueries, len(wantQueries))
sortedKeysCompare(t, gotQueries, wantQueries)
// enable windows mdm, windows mdm queries should be present
ac.MDM.WindowsEnabledAndConfigured = true
- gotQueries = GetDetailQueries(context.Background(), config.FleetConfig{}, &ac, nil)
+ gotQueries = GetDetailQueries(context.Background(), config.FleetConfig{}, &ac, nil, Integrations{})
wantQueries = append(wantQueries, mdmQueriesWindows...)
require.Len(t, gotQueries, len(wantQueries))
sortedKeysCompare(t, gotQueries, wantQueries)
@@ -344,7 +344,7 @@ func TestDetailQueriesOSVersionUnixLike(t *testing.T) {
var initialHost fleet.Host
host := initialHost
- ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["os_version"].IngestFunc
+ ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil, Integrations{})["os_version"].IngestFunc
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
assert.Equal(t, initialHost, host)
@@ -418,7 +418,7 @@ func TestDetailQueriesOSVersionWindows(t *testing.T) {
var initialHost fleet.Host
host := initialHost
- ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["os_version_windows"].IngestFunc
+ ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil, Integrations{})["os_version_windows"].IngestFunc
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
assert.Equal(t, initialHost, host)
@@ -473,7 +473,7 @@ func TestDetailQueriesOSVersionChrome(t *testing.T) {
var initialHost fleet.Host
host := initialHost
- ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil)["os_version"].IngestFunc
+ ingest := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, nil, Integrations{})["os_version"].IngestFunc
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, nil))
assert.Equal(t, initialHost, host)
@@ -1289,29 +1289,29 @@ func TestDirectIngestOSUnixLike(t *testing.T) {
}
func TestAppConfigReplaceQuery(t *testing.T) {
- queries := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true})
+ queries := GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true}, Integrations{})
originalQuery := queries["users"].Query
replacementMap := make(map[string]*string)
replacementMap["users"] = ptr.String("select 1 from blah")
- queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
+ queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap}, Integrations{})
assert.NotEqual(t, originalQuery, queries["users"].Query)
assert.Equal(t, "select 1 from blah", queries["users"].Query)
replacementMap["users"] = nil
- queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
+ queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap}, Integrations{})
_, exists := queries["users"]
assert.False(t, exists)
// put the query back again
replacementMap["users"] = ptr.String("select 1 from blah")
- queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
+ queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap}, Integrations{})
assert.NotEqual(t, originalQuery, queries["users"].Query)
assert.Equal(t, "select 1 from blah", queries["users"].Query)
// empty strings are also ignored
replacementMap["users"] = ptr.String("")
- queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap})
+ queries = GetDetailQueries(context.Background(), config.FleetConfig{}, nil, &fleet.Features{EnableHostUsers: true, DetailQueryOverrides: replacementMap}, Integrations{})
_, exists = queries["users"]
assert.False(t, exists)
}
diff --git a/server/service/service.go b/server/service/service.go
index 3b10648491..b3cfa86e41 100644
--- a/server/service/service.go
+++ b/server/service/service.go
@@ -19,6 +19,7 @@ import (
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
nanomdm_storage "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage"
"github.com/fleetdm/fleet/v4/server/service/async"
+ "github.com/fleetdm/fleet/v4/server/service/conditional_access_microsoft_proxy"
"github.com/fleetdm/fleet/v4/server/sso"
kitlog "github.com/go-kit/log"
)
@@ -63,6 +64,33 @@ type Service struct {
wstepCertManager microsoft_mdm.CertManager
scepConfigService fleet.SCEPConfigService
digiCertService fleet.DigiCertService
+
+ conditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
+}
+
+// ConditionalAccessMicrosoftProxy is the interface of the Microsoft compliance proxy.
+type ConditionalAccessMicrosoftProxy interface {
+ // Create creates the integration on the MS proxy and returns the consent URL.
+ Create(ctx context.Context, tenantID string) (*conditional_access_microsoft_proxy.CreateResponse, error)
+ // Get returns the integration settings.
+ Get(ctx context.Context, tenantID string, secret string) (*conditional_access_microsoft_proxy.GetResponse, error)
+ // Delete deprovisions the tenant on Microsoft and deletes the integration in the proxy service.
+ // Returns a fleet.IsNotFound error if the integration doesn't exist.
+ Delete(ctx context.Context, tenantID string, secret string) (*conditional_access_microsoft_proxy.DeleteResponse, error)
+ // SetComplianceStatus sets the inventory and compliance status of a host.
+ // Returns the message ID to query the status of the operation (MS has an asynchronous API).
+ SetComplianceStatus(
+ ctx context.Context,
+ tenantID string, secret string,
+ deviceID string,
+ userPrincipalName string,
+ mdmEnrolled bool,
+ deviceName, osName, osVersion string,
+ compliant bool,
+ lastCheckInTime time.Time,
+ ) (*conditional_access_microsoft_proxy.SetComplianceStatusResponse, error)
+ // GetMessageStatusResponse returns the status of a "compliance set" operation.
+ GetMessageStatus(ctx context.Context, tenantID string, secret string, messageID string) (*conditional_access_microsoft_proxy.GetMessageStatusResponse, error)
}
func (svc *Service) LookupGeoIP(ctx context.Context, ip string) *fleet.GeoLocation {
@@ -109,6 +137,7 @@ func NewService(
wstepCertManager microsoft_mdm.CertManager,
scepConfigService fleet.SCEPConfigService,
digiCertService fleet.DigiCertService,
+ conditionalAccessProxy ConditionalAccessMicrosoftProxy,
) (fleet.Service, error) {
authorizer, err := authz.NewAuthorizer()
if err != nil {
@@ -144,6 +173,8 @@ func NewService(
wstepCertManager: wstepCertManager,
scepConfigService: scepConfigService,
digiCertService: digiCertService,
+
+ conditionalAccessMicrosoftProxy: conditionalAccessProxy,
}
return validationMiddleware{svc, ds, sso}, nil
}
diff --git a/server/service/service_appconfig.go b/server/service/service_appconfig.go
index d61331f357..ae61c91afd 100644
--- a/server/service/service_appconfig.go
+++ b/server/service/service_appconfig.go
@@ -85,6 +85,14 @@ func (svc *Service) License(ctx context.Context) (*fleet.LicenseInfo, error) {
}
lic, _ := license.FromContext(ctx)
+
+ // Currently we use the presence of Microsoft Compliance Partner settings
+ // (only configured in cloud instances) to determine if a Fleet instance
+ // is a cloud managed instance.
+ if svc.config.MicrosoftCompliancePartner.IsSet() {
+ lic.ManagedCloud = true
+ }
+
return lic, nil
}
diff --git a/server/service/team_policies.go b/server/service/team_policies.go
index c055fba979..1f0fc1786e 100644
--- a/server/service/team_policies.go
+++ b/server/service/team_policies.go
@@ -20,19 +20,20 @@ import (
/////////////////////////////////////////////////////////////////////////////////
type teamPolicyRequest struct {
- TeamID uint `url:"team_id"`
- QueryID *uint `json:"query_id"`
- Query string `json:"query"`
- Name string `json:"name"`
- Description string `json:"description"`
- Resolution string `json:"resolution"`
- Platform string `json:"platform"`
- Critical bool `json:"critical" premium:"true"`
- CalendarEventsEnabled bool `json:"calendar_events_enabled"`
- SoftwareTitleID *uint `json:"software_title_id"`
- ScriptID *uint `json:"script_id"`
- LabelsIncludeAny []string `json:"labels_include_any"`
- LabelsExcludeAny []string `json:"labels_exclude_any"`
+ TeamID uint `url:"team_id"`
+ QueryID *uint `json:"query_id"`
+ Query string `json:"query"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Resolution string `json:"resolution"`
+ Platform string `json:"platform"`
+ Critical bool `json:"critical" premium:"true"`
+ CalendarEventsEnabled bool `json:"calendar_events_enabled"`
+ SoftwareTitleID *uint `json:"software_title_id"`
+ ScriptID *uint `json:"script_id"`
+ LabelsIncludeAny []string `json:"labels_include_any"`
+ LabelsExcludeAny []string `json:"labels_exclude_any"`
+ ConditionalAccessEnabled bool `json:"conditional_access_enabled"`
}
type teamPolicyResponse struct {
@@ -45,18 +46,19 @@ func (r teamPolicyResponse) Error() error { return r.Err }
func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*teamPolicyRequest)
resp, err := svc.NewTeamPolicy(ctx, req.TeamID, fleet.NewTeamPolicyPayload{
- QueryID: req.QueryID,
- Name: req.Name,
- Query: req.Query,
- Description: req.Description,
- Resolution: req.Resolution,
- Platform: req.Platform,
- Critical: req.Critical,
- CalendarEventsEnabled: req.CalendarEventsEnabled,
- SoftwareTitleID: req.SoftwareTitleID,
- ScriptID: req.ScriptID,
- LabelsIncludeAny: req.LabelsIncludeAny,
- LabelsExcludeAny: req.LabelsExcludeAny,
+ QueryID: req.QueryID,
+ Name: req.Name,
+ Query: req.Query,
+ Description: req.Description,
+ Resolution: req.Resolution,
+ Platform: req.Platform,
+ Critical: req.Critical,
+ CalendarEventsEnabled: req.CalendarEventsEnabled,
+ SoftwareTitleID: req.SoftwareTitleID,
+ ScriptID: req.ScriptID,
+ LabelsIncludeAny: req.LabelsIncludeAny,
+ LabelsExcludeAny: req.LabelsExcludeAny,
+ ConditionalAccessEnabled: req.ConditionalAccessEnabled,
})
if err != nil {
return teamPolicyResponse{Err: err}, nil
@@ -155,19 +157,20 @@ func (svc *Service) newTeamPolicyPayloadToPolicyPayload(ctx context.Context, tea
return fleet.PolicyPayload{}, err
}
return fleet.PolicyPayload{
- QueryID: p.QueryID,
- Name: p.Name,
- Query: p.Query,
- Critical: p.Critical,
- Description: p.Description,
- Resolution: p.Resolution,
- Platform: p.Platform,
- CalendarEventsEnabled: p.CalendarEventsEnabled,
- SoftwareInstallerID: softwareInstallerID,
- VPPAppsTeamsID: vppAppsTeamsID,
- ScriptID: p.ScriptID,
- LabelsIncludeAny: p.LabelsIncludeAny,
- LabelsExcludeAny: p.LabelsExcludeAny,
+ QueryID: p.QueryID,
+ Name: p.Name,
+ Query: p.Query,
+ Critical: p.Critical,
+ Description: p.Description,
+ Resolution: p.Resolution,
+ Platform: p.Platform,
+ CalendarEventsEnabled: p.CalendarEventsEnabled,
+ SoftwareInstallerID: softwareInstallerID,
+ VPPAppsTeamsID: vppAppsTeamsID,
+ ScriptID: p.ScriptID,
+ LabelsIncludeAny: p.LabelsIncludeAny,
+ LabelsExcludeAny: p.LabelsExcludeAny,
+ ConditionalAccessEnabled: p.ConditionalAccessEnabled,
}, nil
}
@@ -525,6 +528,9 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
if p.CalendarEventsEnabled != nil {
policy.CalendarEventsEnabled = *p.CalendarEventsEnabled
}
+ if p.ConditionalAccessEnabled != nil {
+ policy.ConditionalAccessEnabled = *p.ConditionalAccessEnabled
+ }
if removeStats {
policy.FailingHostCount = 0
policy.PassingHostCount = 0
diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go
index 6f938f91b5..6db344afd1 100644
--- a/server/service/team_schedule.go
+++ b/server/service/team_schedule.go
@@ -155,7 +155,6 @@ func modifyTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fl
return modifyTeamScheduleResponse{}, nil
}
-// TODO(lucas): Document new behavior.
// teamID is not used because of mismatch between old internal representation and API.
func (svc Service) ModifyTeamScheduledQueries(
ctx context.Context,
@@ -195,7 +194,6 @@ func deleteTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fl
return deleteTeamScheduleResponse{}, nil
}
-// TODO(lucas): Document new behavior.
// teamID is not used because of mismatch between old internal representation and API.
func (svc Service) DeleteTeamScheduledQueries(ctx context.Context, teamID uint, scheduledQueryID uint) error {
return svc.DeleteQueryByID(ctx, scheduledQueryID)
diff --git a/server/service/teams_test.go b/server/service/teams_test.go
index 9960093391..7156d9f101 100644
--- a/server/service/teams_test.go
+++ b/server/service/teams_test.go
@@ -72,6 +72,9 @@ func TestTeamAuth(t *testing.T) {
return &fleet.Team{ID: 2}, nil
}
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return nil, ¬FoundError{}
+ }
testCases := []struct {
name string
@@ -286,6 +289,9 @@ func TestApplyTeamSpecs(t *testing.T) {
require.Len(t, act.Teams, 1)
return nil
}
+ ds.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) {
+ return nil, ¬FoundError{}
+ }
_, err := svc.ApplyTeamSpecs(ctx, []*fleet.TeamSpec{{Name: "team1", Features: tt.spec}}, fleet.ApplyTeamSpecOptions{})
require.NoError(t, err)
diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go
index 1ebc643943..33a32c92ac 100644
--- a/server/service/testing_utils.go
+++ b/server/service/testing_utils.go
@@ -64,13 +64,14 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
logger := kitlog.NewNopLogger()
var (
- failingPolicySet fleet.FailingPolicySet = NewMemFailingPolicySet()
- enrollHostLimiter fleet.EnrollHostLimiter = nopEnrollHostLimiter{}
- depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{}
- mailer fleet.MailService = &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }}
- c clock.Clock = clock.C
- scepConfigService = eeservice.NewSCEPConfigService(logger, nil)
- digiCertService = digicert.NewService(digicert.WithLogger(logger))
+ failingPolicySet fleet.FailingPolicySet = NewMemFailingPolicySet()
+ enrollHostLimiter fleet.EnrollHostLimiter = nopEnrollHostLimiter{}
+ depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{}
+ mailer fleet.MailService = &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }}
+ c clock.Clock = clock.C
+ scepConfigService = eeservice.NewSCEPConfigService(logger, nil)
+ digiCertService = digicert.NewService(digicert.WithLogger(logger))
+ conditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
mdmStorage fleet.MDMAppleStore
mdmPusher nanomdm_push.Pusher
@@ -165,6 +166,10 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
if len(opts) > 0 && opts[0].DigiCertService != nil {
digiCertService = opts[0].DigiCertService
}
+ if len(opts) > 0 && opts[0].ConditionalAccessMicrosoftProxy != nil {
+ conditionalAccessMicrosoftProxy = opts[0].ConditionalAccessMicrosoftProxy
+ fleetConfig.MicrosoftCompliancePartner.ProxyAPIKey = "insecure" // setting this so the feature is "enabled".
+ }
var wstepManager microsoft_mdm.CertManager
if fleetConfig.MDM.WindowsWSTEPIdentityCert != "" && fleetConfig.MDM.WindowsWSTEPIdentityKey != "" {
@@ -200,6 +205,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
wstepManager,
scepConfigService,
digiCertService,
+ conditionalAccessMicrosoftProxy,
)
if err != nil {
panic(err)
@@ -324,38 +330,39 @@ func (svc *mockMailService) CanSendEmail(smtpSettings fleet.SMTPSettings) bool {
type TestNewScheduleFunc func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc
type TestServerOpts struct {
- Logger kitlog.Logger
- License *fleet.LicenseInfo
- SkipCreateTestUsers bool
- Rs fleet.QueryResultStore
- Lq fleet.LiveQueryStore
- Pool fleet.RedisPool
- FailingPolicySet fleet.FailingPolicySet
- Clock clock.Clock
- Task *async.Task
- EnrollHostLimiter fleet.EnrollHostLimiter
- Is fleet.InstallerStore
- FleetConfig *config.FleetConfig
- MDMStorage fleet.MDMAppleStore
- DEPStorage nanodep_storage.AllDEPStorage
- SCEPStorage scep_depot.Depot
- MDMPusher nanomdm_push.Pusher
- HTTPServerConfig *http.Server
- StartCronSchedules []TestNewScheduleFunc
- UseMailService bool
- APNSTopic string
- ProfileMatcher fleet.ProfileMatcher
- EnableCachedDS bool
- NoCacheDatastore bool
- SoftwareInstallStore fleet.SoftwareInstallerStore
- BootstrapPackageStore fleet.MDMBootstrapPackageStore
- KeyValueStore fleet.KeyValueStore
- EnableSCEPProxy bool
- WithDEPWebview bool
- FeatureRoutes []endpoint_utils.HandlerRoutesFunc
- SCEPConfigService fleet.SCEPConfigService
- DigiCertService fleet.DigiCertService
- EnableSCIM bool
+ Logger kitlog.Logger
+ License *fleet.LicenseInfo
+ SkipCreateTestUsers bool
+ Rs fleet.QueryResultStore
+ Lq fleet.LiveQueryStore
+ Pool fleet.RedisPool
+ FailingPolicySet fleet.FailingPolicySet
+ Clock clock.Clock
+ Task *async.Task
+ EnrollHostLimiter fleet.EnrollHostLimiter
+ Is fleet.InstallerStore
+ FleetConfig *config.FleetConfig
+ MDMStorage fleet.MDMAppleStore
+ DEPStorage nanodep_storage.AllDEPStorage
+ SCEPStorage scep_depot.Depot
+ MDMPusher nanomdm_push.Pusher
+ HTTPServerConfig *http.Server
+ StartCronSchedules []TestNewScheduleFunc
+ UseMailService bool
+ APNSTopic string
+ ProfileMatcher fleet.ProfileMatcher
+ EnableCachedDS bool
+ NoCacheDatastore bool
+ SoftwareInstallStore fleet.SoftwareInstallerStore
+ BootstrapPackageStore fleet.MDMBootstrapPackageStore
+ KeyValueStore fleet.KeyValueStore
+ EnableSCEPProxy bool
+ WithDEPWebview bool
+ FeatureRoutes []endpoint_utils.HandlerRoutesFunc
+ SCEPConfigService fleet.SCEPConfigService
+ DigiCertService fleet.DigiCertService
+ EnableSCIM bool
+ ConditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy
}
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
diff --git a/server/webhooks/failing_policies_test.go b/server/webhooks/failing_policies_test.go
index 8cf2829f31..74538169db 100644
--- a/server/webhooks/failing_policies_test.go
+++ b/server/webhooks/failing_policies_test.go
@@ -125,7 +125,8 @@ func TestTriggerFailingPoliciesWebhookBasic(t *testing.T) {
"failing_host_count": 2,
"host_count_updated_at": null,
"critical": true,
- "calendar_events_enabled": false
+ "calendar_events_enabled": false,
+ "conditional_access_enabled": false
},
"hosts": [
{
@@ -312,7 +313,8 @@ func TestTriggerFailingPoliciesWebhookTeam(t *testing.T) {
"failing_host_count": 1,
"host_count_updated_at": null,
"critical": false,
- "calendar_events_enabled": true
+ "calendar_events_enabled": true,
+ "conditional_access_enabled": false
},
"hosts": [
{
diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt
index 4cb68ec6b9..1a612a3a53 100644
--- a/tools/cloner-check/generated_files/appconfig.txt
+++ b/tools/cloner-check/generated_files/appconfig.txt
@@ -121,6 +121,10 @@ github.com/fleetdm/fleet/v4/pkg/optjson/Slice[github.com/fleetdm/fleet/v4/server
github.com/fleetdm/fleet/v4/server/fleet/CustomSCEPProxyIntegration Name string
github.com/fleetdm/fleet/v4/server/fleet/CustomSCEPProxyIntegration URL string
github.com/fleetdm/fleet/v4/server/fleet/CustomSCEPProxyIntegration Challenge string
+github.com/fleetdm/fleet/v4/server/fleet/Integrations ConditionalAccessEnabled optjson.Bool
+github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
+github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
+github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
github.com/fleetdm/fleet/v4/server/fleet/AppConfig MDM fleet.MDM
github.com/fleetdm/fleet/v4/server/fleet/MDM AppleServerURL string
github.com/fleetdm/fleet/v4/server/fleet/MDM DeprecatedAppleBMDefaultTeam string
@@ -162,9 +166,6 @@ github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup BootstrapPackage optjson.Str
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableEndUserAuthentication bool
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup MacOSSetupAssistant optjson.String
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup EnableReleaseDeviceManually optjson.Bool
-github.com/fleetdm/fleet/v4/pkg/optjson/Bool Set bool
-github.com/fleetdm/fleet/v4/pkg/optjson/Bool Valid bool
-github.com/fleetdm/fleet/v4/pkg/optjson/Bool Value bool
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup Script optjson.String
github.com/fleetdm/fleet/v4/server/fleet/MacOSSetup Software optjson.Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware]
github.com/fleetdm/fleet/v4/pkg/optjson/Slice[*github.com/fleetdm/fleet/v4/server/fleet.MacOSSetupSoftware] Set bool
diff --git a/tools/msal/README.md b/tools/msal/README.md
new file mode 100644
index 0000000000..c43b8f52b9
--- /dev/null
+++ b/tools/msal/README.md
@@ -0,0 +1,5 @@
+# MSAL sample app
+
+This is a sample Objective-C application to retrieve Entra's "Device ID" as documented by Microsoft.
+We found a way to retrieve this information with osquery via detail queries (using `certificates` and `keychain_items` tables).
+We are keeping this here for future reference in case we need to iterate the detail queries.
diff --git a/tools/msal/main.m b/tools/msal/main.m
new file mode 100644
index 0000000000..56dfa065af
--- /dev/null
+++ b/tools/msal/main.m
@@ -0,0 +1,34 @@
+#import
+#import
+
+void run(void) {
+ NSError *error = nil;
+ MSALPublicClientApplicationConfig *config = [[MSALPublicClientApplicationConfig alloc]
+ initWithClientId:@""
+ redirectUri:nil
+ authority:nil];
+
+ MSALPublicClientApplication *application = [[MSALPublicClientApplication alloc] initWithConfiguration:config error:&error];
+
+ if (error) {
+ NSLog(@"Failed to create application: %@", error);
+ return;
+ }
+
+ [application getDeviceInformationWithParameters:nil
+ completionBlock:^(MSALDeviceInformation * _Nullable deviceInformation, __unused NSError * _Nullable error) {
+ NSString *deviceId = deviceInformation.extraDeviceInformation[MSAL_PRIMARY_REGISTRATION_DEVICE_ID];
+ NSString *upn = deviceInformation.extraDeviceInformation[MSAL_PRIMARY_REGISTRATION_UPN];
+
+ NSLog(@"deviceId = %s, upn = %s", (char*)[deviceId UTF8String], (char*)[upn UTF8String]);
+
+ }];
+
+}
+
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ run();
+ }
+ return 0;
+}
diff --git a/webpack.config.js b/webpack.config.js
index 8786f73cd1..e99b2b67ca 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -27,9 +27,6 @@ let plugins = [
new webpack.DefinePlugin({
featureFlags: {
// e.g.: allowGitOpsMode: JSON.stringify(process.env.ALLOW_GITOPS_MODE),
- allowConditionalAccess: JSON.stringify(
- process.env.ALLOW_CONDITIONAL_ACCESS
- ),
},
}),
];