Add app_sso_platform table to orbit and use table in Entra ID query ingestion (#30140)

#28621

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [X] Added/updated automated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)).
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
This commit is contained in:
Lucas Manuel Rodriguez
2025-06-20 17:01:38 -03:00
committed by GitHub
parent d017f5a5a9
commit 39dc7a3772
14 changed files with 1018 additions and 8 deletions
@@ -59,10 +59,14 @@ SELECT email FROM users
- Platforms: darwin
- Discovery query:
```sql
SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = 'app_sso_platform'
```
- Query:
```sql
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);
SELECT * FROM app_sso_platform WHERE extension_identifier = 'com.microsoft.CompanyPortalMac.ssoextension' AND realm = 'KERBEROS.MICROSOFTONLINE.COM';
```
## disk_encryption_darwin
@@ -0,0 +1 @@
* Added `app_sso_platform` table to get Platform SSO extensions state information.
+9
View File
@@ -1,5 +1,14 @@
//go:debug x509negativeserial=1
package main
// Note about the above "go:debug x509negativeserial=1":
// https://pkg.go.dev/crypto/x509#ParseCertificate:
// "Before Go 1.23, ParseCertificate accepted certificates with negative serial numbers.
// This behavior can be restored by including "x509negativeserial=1" in the GODEBUG environment
// variable.
// Why do we need this?
// Certificates generated by the Platform SSO extesion of Microsoft Company Portal can have negative serial numbers.
import (
"bytes"
"context"
@@ -0,0 +1,247 @@
//go:build darwin
// +build darwin
package app_sso_platform
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os/exec"
"regexp"
"strings"
"github.com/fleetdm/fleet/v4/orbit/pkg/user"
"github.com/osquery/osquery-go/plugin/table"
"github.com/rs/zerolog/log"
)
// Columns is the schema of the table.
func Columns() []table.ColumnDefinition {
return []table.ColumnDefinition{
// Extension identifier of the Platform SSO extension (e.g. "com.microsoft.CompanyPortalMac.ssoextension").
// Required column, currently supports setting this once per query.
table.TextColumn("extension_identifier"),
// Realm of the user that logged via Platform SSO (e.g. "KERBEROS.MICROSOFTONLINE.COM").
// Required column, currently supports setting this once per query.
table.TextColumn("realm"),
// Device ID extracted from "Device Configuration" -> "deviceSigningCertificate" -> Subject -> CommonName.
table.TextColumn("device_id"),
// User principal name of the user that logged in via Platform SSO.
table.TextColumn("user_principal_name"),
}
}
// Generate is called to return the results for the table at query time.
//
// Constraints for generating can be retrieved from the queryContext.
func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
extensionIdentifierConstraints, ok := queryContext.Constraints["extension_identifier"]
if !ok || len(extensionIdentifierConstraints.Constraints) == 0 {
return nil, errors.New("missing extension_identifier")
}
var expectedExtensionIdentifiers []string
for _, constraint := range extensionIdentifierConstraints.Constraints {
if constraint.Operator != table.OperatorEquals {
return nil, errors.New("only supported operator for 'extension_identifier' is '='")
}
if constraint.Expression == "" {
continue
}
expectedExtensionIdentifiers = append(expectedExtensionIdentifiers, constraint.Expression)
}
if len(expectedExtensionIdentifiers) == 0 {
return nil, errors.New("missing extension_identifier")
} else if len(expectedExtensionIdentifiers) > 1 {
return nil, errors.New("only one extension_identifier can be set")
}
realmConstraints, ok := queryContext.Constraints["realm"]
if !ok || len(realmConstraints.Constraints) == 0 {
return nil, errors.New("missing realm")
}
var expectedRealms []string
for _, constraint := range realmConstraints.Constraints {
if constraint.Operator != table.OperatorEquals {
return nil, errors.New("only supported operator for 'realm' is '='")
}
if constraint.Expression == "" {
continue
}
expectedRealms = append(expectedRealms, constraint.Expression)
}
if len(expectedRealms) == 0 {
return nil, errors.New("missing realm")
} else if len(expectedRealms) > 1 {
return nil, errors.New("only one realm can be set")
}
loggedInUser, err := user.UserLoggedInViaGui()
if err != nil {
return nil, fmt.Errorf("failed to check user logged in: %w", err)
}
if loggedInUser == nil || *loggedInUser == "" {
// User is not logged in, nothing to do so we return no results.
return nil, nil
}
output, err := executeAppSSOPlatform(*loggedInUser)
if err != nil {
return nil, fmt.Errorf("failed to execute app-sso platform: %w", err)
}
appSSOPlatform, err := parseAppSSOPlatformCommandOutput(output, expectedExtensionIdentifiers[0], expectedRealms[0])
if err != nil {
return nil, fmt.Errorf("failed to parse \"app-sso platform --state\" output: %w", err)
}
if appSSOPlatform == nil {
// Device not registered, nothing to do so we return no results.
return nil, nil
}
return []map[string]string{{
"extension_identifier": appSSOPlatform.extensionIdentifier,
"realm": appSSOPlatform.realm,
"device_id": appSSOPlatform.deviceID,
"user_principal_name": appSSOPlatform.userPrincipalName,
}}, nil
}
func executeAppSSOPlatform(loggedInUser string) ([]byte, error) {
cmd := exec.Command("sh", "-c", fmt.Sprintf(`launchctl asuser $(id -u "%s") sudo -iu "%s" /usr/bin/app-sso platform --state`, loggedInUser, loggedInUser)) // #nosec G20: loggedInUser is not controlled by user.
return cmd.Output()
}
var (
// deviceRe extracts JSON after "Device Configuration:" and before "Login Configuration:"
deviceRe = regexp.MustCompile(`(?s)Device Configuration:\n\s(\{.*?\}|\(null\))\n\nLogin Configuration:`)
// userRe extracts JSON after "User Configuration:" and before "SSO Tokens:" (or end of string)
userRe = regexp.MustCompile(`(?s)User Configuration:\n\s(\{.*?\}|\(null\))\n\n`)
)
// extractJSONSections finds JSON blocks for "Device Configuration" and "User Configuration".
func extractJSONSections(s []byte) (deviceConfig string, userConfig string, err error) {
deviceMatch := deviceRe.FindSubmatch(s)
userMatch := userRe.FindSubmatch(s)
if len(deviceMatch) < 2 {
return "", "", errors.New("match for \"Device Configuration\" not found")
}
if len(userMatch) < 2 {
return "", "", errors.New("match for \"User Configuration\" JSON not found")
}
return string(deviceMatch[1]), string(userMatch[1]), nil
}
type appSSOPlatformData struct {
extensionIdentifier string
deviceID string
realm string
userPrincipalName string
}
func parseAppSSOPlatformCommandOutput(output []byte, expectedExtensionIdentifier string, expectedRealm string) (*appSSOPlatformData, error) {
deviceConfigJSON, userConfigJSON, err := extractJSONSections(output)
if err != nil {
return nil, fmt.Errorf("could not extract JSON sections: %w", err)
}
if deviceConfigJSON == "(null)" {
log.Debug().Msg("device not registered")
return nil, nil
}
deviceConfig := struct {
DeviceSigningCertificate string `json:"deviceSigningCertificate"`
ExtensionIdentifier string `json:"extensionIdentifier"`
}{}
if err := json.Unmarshal([]byte(deviceConfigJSON), &deviceConfig); err != nil {
return nil, fmt.Errorf("could not unmarshal \"Device Configuration\" JSON: %w", err)
}
if expectedExtensionIdentifier != deviceConfig.ExtensionIdentifier {
log.Debug().Str("extensionIdentifier", deviceConfig.ExtensionIdentifier).Msg("device registered, but found unmatched extension")
return nil, nil
}
dsc, err := base64.RawURLEncoding.DecodeString(deviceConfig.DeviceSigningCertificate)
if err != nil {
return nil, fmt.Errorf("failed to decode \"deviceSigningCertificate\": %w", err)
}
deviceSigningCertificate, err := x509.ParseCertificate(dsc)
if err != nil {
return nil, fmt.Errorf("failed to parse \"deviceSigningCertificate\": %w", err)
}
if deviceSigningCertificate.Subject.CommonName == "" {
return nil, errors.New("empty subject common name in \"deviceSigningCertificate\"")
}
log.Debug().Str(
"\"Device Configuration\"", deviceSigningCertificate.Subject.CommonName,
).Msg("found device ID")
userConfig := struct {
KerberosStatus []map[string]any `json:"kerberosStatus"`
}{}
if userConfigJSON == "(null)" {
log.Debug().Msg("user not registered")
return &appSSOPlatformData{
extensionIdentifier: deviceConfig.ExtensionIdentifier,
deviceID: deviceSigningCertificate.Subject.CommonName,
realm: expectedRealm,
userPrincipalName: "",
}, nil
}
if err := json.Unmarshal([]byte(userConfigJSON), &userConfig); err != nil {
return nil, fmt.Errorf("could not unmarshal \"User Configuration\" JSON: %w", err)
}
if len(userConfig.KerberosStatus) == 0 {
return nil, errors.New("\"kerberosStatus\" has no entries")
}
realm_, ok := userConfig.KerberosStatus[0]["realm"]
if !ok {
return nil, errors.New("missing \"realm\" key in \"kerberosStatus\"")
}
realm, ok := realm_.(string)
if !ok {
return nil, fmt.Errorf("unexpected type for \"realm\" key in \"kerberosStatus\": %T", err)
}
upn_, ok := userConfig.KerberosStatus[0]["upn"]
if !ok {
return nil, errors.New("missing \"upn\" key in \"kerberosStatus\"")
}
upn, ok := upn_.(string)
if !ok {
return nil, fmt.Errorf("unexpected type for \"upn\" key in \"kerberosStatus\": %T", err)
}
if upn == "" {
return nil, errors.New("empty \"upn\" key in \"kerberosStatus\"")
}
if expectedRealm != realm {
log.Debug().Str("realm", realm).Msg("user registered, but found unmatched realm")
return &appSSOPlatformData{
extensionIdentifier: deviceConfig.ExtensionIdentifier,
deviceID: deviceSigningCertificate.Subject.CommonName,
realm: expectedRealm,
userPrincipalName: "",
}, nil
}
suffix := fmt.Sprintf("@%s", realm)
upn = strings.TrimSuffix(upn, suffix)
upn = strings.ReplaceAll(upn, "\\@", "@")
log.Debug().Str(
"extension_identifier", deviceConfig.ExtensionIdentifier,
).Str(
"device_id", deviceSigningCertificate.Subject.CommonName,
).Str(
"realm", realm,
).Str(
"user_principal_name", upn,
).Msg("device and user found")
return &appSSOPlatformData{
extensionIdentifier: deviceConfig.ExtensionIdentifier,
deviceID: deviceSigningCertificate.Subject.CommonName,
realm: realm,
userPrincipalName: upn,
}, nil
}
@@ -0,0 +1,248 @@
//go:build darwin
// +build darwin
package app_sso_platform
import (
"context"
_ "embed"
"testing"
"github.com/osquery/osquery-go/plugin/table"
"github.com/stretchr/testify/require"
)
var (
//go:embed testdata/app_sso_platform_state_sample1.txt
sample1 string
//go:embed testdata/app_sso_platform_state_sample2_user_null.txt
sample2 string
//go:embed testdata/app_sso_platform_state_empty.txt
empty string
)
func TestParseAppSSOPlatformCommandOutput(t *testing.T) {
// Match
data, err := parseAppSSOPlatformCommandOutput([]byte(sample1), "com.microsoft.CompanyPortalMac.ssoextension", "KERBEROS.MICROSOFTONLINE.COM")
require.NoError(t, err)
require.NotNil(t, data)
require.Equal(t, "34b1ba9a-3b2d-4c6c-ab4b-615f4b143eab", data.deviceID)
require.Equal(t, "com.microsoft.CompanyPortalMac.ssoextension", data.extensionIdentifier)
require.Equal(t, "KERBEROS.MICROSOFTONLINE.COM", data.realm)
require.Equal(t, "foobar@contoso.onmicrosoft.com", data.userPrincipalName)
// Empty, Platform SSO not set yet.
data, err = parseAppSSOPlatformCommandOutput([]byte(empty), "com.microsoft.CompanyPortalMac.ssoextension", "KERBEROS.MICROSOFTONLINE.COM")
require.NoError(t, err)
require.Nil(t, data)
// Platform SSO extension identifier does not match.
data, err = parseAppSSOPlatformCommandOutput([]byte(sample1), "com.microsoft.Other.other", "KERBEROS.MICROSOFTONLINE.COM")
require.NoError(t, err)
require.Nil(t, data)
// Platform SSO extension identifier matches, but user realm doesn't match.
data, err = parseAppSSOPlatformCommandOutput([]byte(sample1), "com.microsoft.CompanyPortalMac.ssoextension", "FOOBAR.OTHER.COM")
require.NoError(t, err)
require.NotNil(t, data)
require.Equal(t, "34b1ba9a-3b2d-4c6c-ab4b-615f4b143eab", data.deviceID)
require.Equal(t, "com.microsoft.CompanyPortalMac.ssoextension", data.extensionIdentifier)
require.Equal(t, "FOOBAR.OTHER.COM", data.realm)
require.Equal(t, "", data.userPrincipalName)
// None matches.
data, err = parseAppSSOPlatformCommandOutput([]byte(sample1), "com.microsoft.Other.other", "FOOBAR.OTHER.COM")
require.NoError(t, err)
require.Nil(t, data)
// Platform SSO extension identifier matches, but user is not registered yet (null).
// Can happen if Platform SSO configuration profile was deployed and this is a workstation with two users,
// and one user registered but not the other one.
data, err = parseAppSSOPlatformCommandOutput([]byte(sample2), "com.microsoft.CompanyPortalMac.ssoextension", "KERBEROS.MICROSOFTONLINE.COM")
require.NoError(t, err)
require.NotNil(t, data)
require.Equal(t, "34b1ba9a-3b2d-4c6c-ab4b-615f4b143eab", data.deviceID)
require.Equal(t, "com.microsoft.CompanyPortalMac.ssoextension", data.extensionIdentifier)
require.Equal(t, "KERBEROS.MICROSOFTONLINE.COM", data.realm)
require.Equal(t, "", data.userPrincipalName)
}
func TestGenerateErrors(t *testing.T) {
// Multiple extension_identifier values.
_, err := Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "extension_identifier_value1",
},
{
Operator: table.OperatorEquals,
Expression: "extension_identifier_value2",
},
},
},
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "realm_value",
},
},
},
},
})
require.Error(t, err)
// Multiple realm values.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "extension_identifier_value",
},
},
},
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "realm_value1",
},
{
Operator: table.OperatorEquals,
Expression: "realm_value2",
},
},
},
},
})
require.Error(t, err)
// Multiple extension_identifier value.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "realm_value",
},
},
},
},
})
require.Error(t, err)
// Missing realm value.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "extension_identifier_value",
},
},
},
},
})
require.Error(t, err)
// Not using equality on extension_identifier.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorLike,
Expression: "extension_identifier_value",
},
},
},
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "realm_value",
},
},
},
},
})
require.Error(t, err)
// Not using equality on realm.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "extension_identifier_value",
},
},
},
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorLike,
Expression: "realm_value",
},
},
},
},
})
require.Error(t, err)
// Empty extension_identifier.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "",
},
},
},
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "realm_value",
},
},
},
},
})
require.Error(t, err)
// Empty realm.
_, err = Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "extension_identifier_value",
},
},
},
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: "",
},
},
},
},
})
require.Error(t, err)
}
@@ -0,0 +1,11 @@
Time: 2025-06-18 20:42:20 +0000
Device Configuration:
(null)
Login Configuration:
(null)
User Configuration:
(null)
@@ -0,0 +1,175 @@
Time: 2025-06-18 20:23:40 +0000
Device Configuration:
{
"_deviceEncryptionKeyData" : "<REDACTED>",
"_deviceSigningKeyData" : "<REDACTED>",
"allowDeviceIdentifiersInAttestation" : false,
"authGracePeriodStart" : "2025-06-18T13:10:46Z",
"authorizationEnabled" : false,
"created" : "2025-06-18T20:23:40Z",
"createUsersEnabled" : false,
"deviceSigningCertificate" : "MIIDNzCCAh-gAwIBAgIQcdJMRMM2o4xHxrDE4zP1tzANBgkqhkiG9w0BAQsFADB4MXYwEQYKCZImiZPyLGQBGRYDbmV0MBUGCgmSJomT8ixkARkWB3dpbmRvd3MwHQYDVQQDExZNUy1Pcmdhbml6YXRpb24tQWNjZXNzMCsGA1UECxMkODJkYmFjYTQtM2U4MS00NmNhLTljNzMtMDk1MGMxZWFjYTk3MB4XDTI1MDYxODEyNDIyNloXDTM1MDYxODEzMTIyNlowLzEtMCsGA1UEAxMkMzRiMWJhOWEtM2IyZC00YzZjLWFiNGItNjE1ZjRiMTQzZWFiMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7CIbedoCo3XPErh3BOXJBajYifimV1fEt9aSWEYnrnKW5nB6Ynr38taXo8ZeiRB2uN7fJrqtqo-Vd2nY8G8VNqOB0DCBzTAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB_wQMMAoGCCsGAQUFBwMCMA4GA1UdDwEB_wQEAwIHgDAiBgsqhkiG9xQBBYIcAgQTBIEQmrqxNC07bEyrS2FfSxQ-qzAiBgsqhkiG9xQBBYIcAwQTBIEQMnxB-ihBNUyHp4spMP1LXTAiBgsqhkiG9xQBBYIcBQQTBIEQ3hS3ryz08kGAL2WykR3TRjAUBgsqhkiG9xQBBYIcCAQFBIECTkEwEwYLKoZIhvcUAQWCHAcEBASBATEwDQYJKoZIhvcNAQELBQADggEBAD3BG_COLS5iniJ0-pir1snO5W7JHVKKP97GLEtAbvvpJ1eG7lI5zQKZGqGyxHquq1p8rnCSf-cxIgl-OufMC6yRvO0M4yztd1X6DG4LZUZlV39LW8SM-Ag_08CBpYRAZuYPks5DY-VgF8zXEfTfvrJIjGcd3Vhd3nH0wzyI-OcC12qV7dC2PKP5B3ZCLmUUOgQ0giCBXO1LhW397HOewrnk-0B5n25KBLK7WYBG5qx9eTm2U7mMMHs93--VsIDjDkgBYd5EJPHylQDG_pkCdYDTDOfL9wWrAUiOu3pKi4yAuYNKqC2h06sg6xDomoVuDoGWhLdzoTpcmdzBYjPAutg",
"encryptionAlgorithm" : "ECDHE-A256GCM",
"extensionIdentifier" : "com.microsoft.CompanyPortalMac.ssoextension",
"fileVaultPolicy" : "None (0)",
"lastEncryptionKeyChange" : "2025-06-18T13:10:46Z",
"loginFrequency" : 64800,
"loginPolicy" : "None (0)",
"loginType" : "POLoginTypeUserSecureEnclaveKey (2)",
"newUserAuthorizationMode" : "None",
"offlineGracePeriod" : "0 hours",
"pendingEncryptionAlgorithm" : "none",
"pendingSigningAlgorithm" : "none",
"protocolVersion" : 1,
"registrationCompleted" : true,
"requireAuthGracePeriod" : "0 hours",
"sdkVersionString" : 0,
"sharedDeviceKeys" : true,
"signingAlgorithm" : "ES256",
"tokenToUserMapping" : {
"AccountName" : "preferred_username",
"FullName" : "name"
},
"unlockPolicy" : "None (0)",
"userAuthorizationMode" : "None",
"version" : 1
}
Login Configuration:
{
"accountDisplayName" : "Microsoft Entra",
"additionalScopes" : "aza urn:aad:tb:update:prt/.default profile offline_access openid",
"audience" : "login.microsoftonline.com",
"clientID" : "<REDACTED>",
"created" : "2025-06-18T20:23:40Z",
"customAssertionRequestHeaderClaims" : {
"typ" : "JWT",
"use" : "ngc"
},
"customKeyExchangeRequestBodyClaims" : {
"aud" : "https://login.microsoftonline.com/<REDACTED>/getkeydata"
},
"customKeyExchangeRequestHeaderClaims" : {
"typ" : "JWT"
},
"customKeyExchangeRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customKeyRequestBodyClaims" : {
"aud" : "https://login.microsoftonline.com/<REDACTED>/getkeydata"
},
"customKeyRequestHeaderClaims" : {
"typ" : "JWT"
},
"customKeyRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customLoginRequestHeaderClaims" : {
"typ" : "JWT"
},
"customLoginRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customNonceRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customRequestJWTParameterName" : "request",
"deviceContext" : "<REDACTED>",
"federationMexURLKeypath" : "federation_metadata_url",
"federationPredicate" : "account_type = 'Federated'",
"federationRequestURN" : "urn:federation:MicrosoftOnline",
"federationType" : 2,
"federationUserPreauthenticationURL" : "https://login.windows.net/common/UserRealm?api-version=1.0&checkForMicrosoftAccount=false",
"includePreviousRefreshTokenInLoginRequest" : true,
"invalidCredentialPredicate" : "error = 'invalid_grant' AND suberror != 'device_authentication_failed'",
"issuer" : "https://login.microsoftonline.com/<REDACTED>/v2.0",
"jwksEndpointURL" : "https://login.microsoftonline.com/<REDACTED>/discovery/v2.0/keys",
"kerberosTicketMappings" : [
{
"clientNameKeyName" : "cn",
"encryptionKeyTypeKeyName" : "keyType",
"messageBufferKeyName" : "messageBuffer",
"realmKeyName" : "realm",
"serviceNameKeyName" : "sn",
"sessionKeyKeyName" : "clientKey",
"ticketKeyPath" : "tgt_ad"
},
{
"clientNameKeyName" : "cn",
"encryptionKeyTypeKeyName" : "keyType",
"messageBufferKeyName" : "messageBuffer",
"realmKeyName" : "realm",
"serviceNameKeyName" : "sn",
"sessionKeyKeyName" : "clientKey",
"ticketKeyPath" : "tgt_cloud"
}
],
"keyEndpointURL" : "https://login.microsoftonline.com/<REDACTED>/getkeydata",
"loginRequestEncryptionAlgorithm" : "ECDHE-A256GCM",
"nonceResponseKeypath" : "Nonce",
"previousRefreshTokenClaimName" : "previous_refresh_token",
"serverNonceClaimName" : "request_nonce",
"tokenEndpointURL" : "https://login.microsoftonline.com/<REDACTED>/oauth2/v2.0/token",
"uniqueIdentifierClaimName" : "oid",
"userSEPKeyBiometricPolicy" : "None (0)"
}
User Configuration:
{
"_sepKeyData" : "EOKfOXCpi9nQHsrm6EZtXXUiMJvabJeFmaiNoBHyuZE=",
"created" : "2025-06-18T20:23:40Z",
"kerberosStatus" : [
{
"cacheName" : "C0379BAF-5FE3-4002-9B66-F95205EE7BF6",
"exchangeRequired" : false,
"failedToConnect" : false,
"importSuccessful" : true,
"realm" : "KERBEROS.MICROSOFTONLINE.COM",
"ticketKeyPath" : "tgt_cloud",
"upn" : "foobar\\@contoso.onmicrosoft.com@KERBEROS.MICROSOFTONLINE.COM"
}
],
"lastLoginDate" : "2025-06-18T20:22:55Z",
"loginType" : "POLoginTypeUserSecureEnclaveKey (2)",
"pendingSigningAlgorithm" : "none",
"signingAlgorithm" : "ES256",
"state" : "POUserStateNormal (0)",
"uniqueIdentifier" : "<REDACTED>",
"userLoginConfiguration" : {
"created" : "2025-06-18T20:23:40Z",
"loginUserName" : "f***@contoso.onmicrosoft.com"
},
"version" : 1
}
SSO Tokens:
Received:
2025-06-18T20:22:55Z
Expiration:
2025-07-02T20:22:54Z (Not Expired)
@@ -0,0 +1,144 @@
Time: 2025-06-18 20:23:40 +0000
Device Configuration:
{
"_deviceEncryptionKeyData" : "<REDACTED>",
"_deviceSigningKeyData" : "<REDACTED>",
"allowDeviceIdentifiersInAttestation" : false,
"authGracePeriodStart" : "2025-06-18T13:10:46Z",
"authorizationEnabled" : false,
"created" : "2025-06-18T20:23:40Z",
"createUsersEnabled" : false,
"deviceSigningCertificate" : "MIIDNzCCAh-gAwIBAgIQcdJMRMM2o4xHxrDE4zP1tzANBgkqhkiG9w0BAQsFADB4MXYwEQYKCZImiZPyLGQBGRYDbmV0MBUGCgmSJomT8ixkARkWB3dpbmRvd3MwHQYDVQQDExZNUy1Pcmdhbml6YXRpb24tQWNjZXNzMCsGA1UECxMkODJkYmFjYTQtM2U4MS00NmNhLTljNzMtMDk1MGMxZWFjYTk3MB4XDTI1MDYxODEyNDIyNloXDTM1MDYxODEzMTIyNlowLzEtMCsGA1UEAxMkMzRiMWJhOWEtM2IyZC00YzZjLWFiNGItNjE1ZjRiMTQzZWFiMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7CIbedoCo3XPErh3BOXJBajYifimV1fEt9aSWEYnrnKW5nB6Ynr38taXo8ZeiRB2uN7fJrqtqo-Vd2nY8G8VNqOB0DCBzTAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB_wQMMAoGCCsGAQUFBwMCMA4GA1UdDwEB_wQEAwIHgDAiBgsqhkiG9xQBBYIcAgQTBIEQmrqxNC07bEyrS2FfSxQ-qzAiBgsqhkiG9xQBBYIcAwQTBIEQMnxB-ihBNUyHp4spMP1LXTAiBgsqhkiG9xQBBYIcBQQTBIEQ3hS3ryz08kGAL2WykR3TRjAUBgsqhkiG9xQBBYIcCAQFBIECTkEwEwYLKoZIhvcUAQWCHAcEBASBATEwDQYJKoZIhvcNAQELBQADggEBAD3BG_COLS5iniJ0-pir1snO5W7JHVKKP97GLEtAbvvpJ1eG7lI5zQKZGqGyxHquq1p8rnCSf-cxIgl-OufMC6yRvO0M4yztd1X6DG4LZUZlV39LW8SM-Ag_08CBpYRAZuYPks5DY-VgF8zXEfTfvrJIjGcd3Vhd3nH0wzyI-OcC12qV7dC2PKP5B3ZCLmUUOgQ0giCBXO1LhW397HOewrnk-0B5n25KBLK7WYBG5qx9eTm2U7mMMHs93--VsIDjDkgBYd5EJPHylQDG_pkCdYDTDOfL9wWrAUiOu3pKi4yAuYNKqC2h06sg6xDomoVuDoGWhLdzoTpcmdzBYjPAutg",
"encryptionAlgorithm" : "ECDHE-A256GCM",
"extensionIdentifier" : "com.microsoft.CompanyPortalMac.ssoextension",
"fileVaultPolicy" : "None (0)",
"lastEncryptionKeyChange" : "2025-06-18T13:10:46Z",
"loginFrequency" : 64800,
"loginPolicy" : "None (0)",
"loginType" : "POLoginTypeUserSecureEnclaveKey (2)",
"newUserAuthorizationMode" : "None",
"offlineGracePeriod" : "0 hours",
"pendingEncryptionAlgorithm" : "none",
"pendingSigningAlgorithm" : "none",
"protocolVersion" : 1,
"registrationCompleted" : true,
"requireAuthGracePeriod" : "0 hours",
"sdkVersionString" : 0,
"sharedDeviceKeys" : true,
"signingAlgorithm" : "ES256",
"tokenToUserMapping" : {
"AccountName" : "preferred_username",
"FullName" : "name"
},
"unlockPolicy" : "None (0)",
"userAuthorizationMode" : "None",
"version" : 1
}
Login Configuration:
{
"accountDisplayName" : "Microsoft Entra",
"additionalScopes" : "aza urn:aad:tb:update:prt/.default profile offline_access openid",
"audience" : "login.microsoftonline.com",
"clientID" : "<REDACTED>",
"created" : "2025-06-18T20:23:40Z",
"customAssertionRequestHeaderClaims" : {
"typ" : "JWT",
"use" : "ngc"
},
"customKeyExchangeRequestBodyClaims" : {
"aud" : "https://login.microsoftonline.com/<REDACTED>/getkeydata"
},
"customKeyExchangeRequestHeaderClaims" : {
"typ" : "JWT"
},
"customKeyExchangeRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customKeyRequestBodyClaims" : {
"aud" : "https://login.microsoftonline.com/<REDACTED>/getkeydata"
},
"customKeyRequestHeaderClaims" : {
"typ" : "JWT"
},
"customKeyRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customLoginRequestHeaderClaims" : {
"typ" : "JWT"
},
"customLoginRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customNonceRequestValues" : {
"client_info" : "1",
"prt_protocol_version" : "4.0",
"tgt" : "true",
"x-client-brkrver" : "3.6.4",
"x-client-OS" : "15.5.0",
"x-client-SKU" : "MSAL.OSX",
"x-client-Ver" : "1.8.1"
},
"customRequestJWTParameterName" : "request",
"deviceContext" : "<REDACTED>",
"federationMexURLKeypath" : "federation_metadata_url",
"federationPredicate" : "account_type = 'Federated'",
"federationRequestURN" : "urn:federation:MicrosoftOnline",
"federationType" : 2,
"federationUserPreauthenticationURL" : "https://login.windows.net/common/UserRealm?api-version=1.0&checkForMicrosoftAccount=false",
"includePreviousRefreshTokenInLoginRequest" : true,
"invalidCredentialPredicate" : "error = 'invalid_grant' AND suberror != 'device_authentication_failed'",
"issuer" : "https://login.microsoftonline.com/<REDACTED>/v2.0",
"jwksEndpointURL" : "https://login.microsoftonline.com/<REDACTED>/discovery/v2.0/keys",
"kerberosTicketMappings" : [
{
"clientNameKeyName" : "cn",
"encryptionKeyTypeKeyName" : "keyType",
"messageBufferKeyName" : "messageBuffer",
"realmKeyName" : "realm",
"serviceNameKeyName" : "sn",
"sessionKeyKeyName" : "clientKey",
"ticketKeyPath" : "tgt_ad"
},
{
"clientNameKeyName" : "cn",
"encryptionKeyTypeKeyName" : "keyType",
"messageBufferKeyName" : "messageBuffer",
"realmKeyName" : "realm",
"serviceNameKeyName" : "sn",
"sessionKeyKeyName" : "clientKey",
"ticketKeyPath" : "tgt_cloud"
}
],
"keyEndpointURL" : "https://login.microsoftonline.com/<REDACTED>/getkeydata",
"loginRequestEncryptionAlgorithm" : "ECDHE-A256GCM",
"nonceResponseKeypath" : "Nonce",
"previousRefreshTokenClaimName" : "previous_refresh_token",
"serverNonceClaimName" : "request_nonce",
"tokenEndpointURL" : "https://login.microsoftonline.com/<REDACTED>/oauth2/v2.0/token",
"uniqueIdentifierClaimName" : "oid",
"userSEPKeyBiometricPolicy" : "None (0)"
}
User Configuration:
(null)
+3
View File
@@ -5,6 +5,7 @@ package table
import (
"context"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/app_sso_platform"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/codesign"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/csrutil_info"
@@ -95,6 +96,8 @@ func PlatformTables(opts PluginOpts) ([]osquery.OsqueryPlugin, error) {
dataflattentable.TablePlugin(log.Logger, dataflattentable.PlistType), // table name is "parse_plist"
table.NewPlugin("codesign", codesign.Columns(), codesign.Generate),
table.NewPlugin("app_sso_platform", app_sso_platform.Columns(), app_sso_platform.Generate),
}
// append platform specific tables
+37
View File
@@ -561,6 +561,43 @@
],
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/app_schemes.yml"
},
{
"name": "app_sso_platform",
"platforms": [
"darwin"
],
"description": "Returns device and login information parsed from the \"app-sso platform -s\" command (\"Platform SSO\" extensions).",
"columns": [
{
"name": "extension_identifier",
"type": "text",
"required": true,
"description": "Extension identifier of the Platform SSO extension (e.g. \"com.microsoft.CompanyPortalMac.ssoextension\")."
},
{
"name": "realm",
"type": "text",
"required": true,
"description": "Realm of the user that logged via Platform SSO (e.g. \"KERBEROS.MICROSOFTONLINE.COM\")."
},
{
"name": "device_id",
"type": "text",
"required": false,
"description": "Device ID extracted from \"Device Configuration\" -> \"deviceSigningCertificate\" -> Subject -> CommonName."
},
{
"name": "user_principal_name",
"type": "text",
"required": false,
"description": "User principal name of the user that logged in via Platform SSO."
}
],
"notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"evented": false,
"url": "https://fleetdm.com/tables/app_sso_platform",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/app_sso_platform.yml"
},
{
"name": "apparmor_events",
"description": "Track AppArmor events.",
+23
View File
@@ -0,0 +1,23 @@
name: app_sso_platform
platforms:
- darwin
description: Returns device and login information parsed from the "app-sso platform -s" command ("Platform SSO" extensions).
columns:
- name: extension_identifier
type: text
required: true
description: Extension identifier of the Platform SSO extension (e.g. "com.microsoft.CompanyPortalMac.ssoextension").
- name: realm
type: text
required: true
description: Realm of the user that logged via Platform SSO (e.g. "KERBEROS.MICROSOFTONLINE.COM").
- name: device_id
type: text
required: false
description: Device ID extracted from "Device Configuration" -> "deviceSigningCertificate" -> Subject -> CommonName.
- name: user_principal_name
type: text
required: false
description: User principal name of the user that logged in via Platform SSO.
notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
evented: false
@@ -184,4 +184,59 @@ func testConditionalAccessHosts(t *testing.T, ds *Datastore) {
require.NotZero(t, s.UpdatedAt)
require.Nil(t, s.Managed)
require.Nil(t, s.Compliant)
err = ds.SetHostConditionalAccessStatus(ctx, noTeamHost.ID, false, true)
require.NoError(t, err)
// Simulate a device with same device ID but empty username.
// This can happen on workstations with two user macOS accounts where one is registered and another one is not.
err = ds.CreateHostConditionalAccessStatus(ctx, noTeamHost.ID, "entraDeviceID2", "")
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, "", 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)
err = ds.SetHostConditionalAccessStatus(ctx, noTeamHost.ID, false, true)
require.NoError(t, err)
// Simulate now that the second user has logged in to Entra.
err = ds.CreateHostConditionalAccessStatus(ctx, noTeamHost.ID, "entraDeviceID2", "foobar3@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, "foobar3@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)
err = ds.SetHostConditionalAccessStatus(ctx, noTeamHost.ID, false, true)
require.NoError(t, err)
// Simulate that the first user has logged in again to the workstation.
err = ds.CreateHostConditionalAccessStatus(ctx, noTeamHost.ID, "entraDeviceID2", "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, "entraDeviceID2", 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.Nil(t, s.Managed)
require.Nil(t, s.Compliant)
}
+7 -6
View File
@@ -861,9 +861,10 @@ var windowsUpdateHistory = DetailQuery{
// 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);`,
// The query ingests Entra's Device ID and User Principal Name of the account
// that logged in to the device (using Company Portal.app with the Platform SSO extension).
Query: "SELECT * FROM app_sso_platform WHERE extension_identifier = 'com.microsoft.CompanyPortalMac.ssoextension' AND realm = 'KERBEROS.MICROSOFTONLINE.COM';",
Discovery: discoveryTable("app_sso_platform"),
Platforms: []string{"darwin"},
DirectIngestFunc: directIngestEntraIDDetails,
}
@@ -1545,9 +1546,9 @@ func directIngestEntraIDDetails(
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")
}
// userPrincipalName can be empty on macOS workstations with e.g. two accounts:
// one logged in to Entra and the other one not logged in.
// While the second one is logged in, it would report the same Device ID but empty 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")
+52
View File
@@ -0,0 +1,52 @@
//go:build darwin
// +build darwin
//go:debug x509negativeserial=1
// Package main is a macOS application to test the app_sso_platform table in the command line.
// Usage for SSO Platform extension for Microsoft Company Portal:
// "go run ./tools/app-sso-platform com.microsoft.CompanyPortalMac.ssoextension KERBEROS.MICROSOFTONLINE.COM"
package main
import (
"context"
"fmt"
"os"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/app_sso_platform"
"github.com/osquery/osquery-go/plugin/table"
)
func main() {
if len(os.Args) < 3 {
fmt.Printf("usage: %s <extensionIdentifier> <realm>\n", os.Args[0])
os.Exit(1)
}
extensionIdentifier := os.Args[1]
realm := os.Args[2]
rows, err := app_sso_platform.Generate(context.Background(), table.QueryContext{
Constraints: map[string]table.ConstraintList{
"extension_identifier": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: extensionIdentifier,
},
},
},
"realm": {
Constraints: []table.Constraint{
{
Operator: table.OperatorEquals,
Expression: realm,
},
},
},
},
})
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", rows)
}