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:
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
+175
@@ -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)
|
||||
|
||||
+144
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user