Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BED-4611: Citrix CanRDP Post Processing #818

Merged
merged 16 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/api/src/analysis/ad/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/specterops/bloodhound/graphschema/ad"
)

func Post(ctx context.Context, db graph.Database, adcsEnabled bool) (*analysis.AtomicPostProcessingStats, error) {
func Post(ctx context.Context, db graph.Database, adcsEnabled bool, citrixEnabled bool) (*analysis.AtomicPostProcessingStats, error) {
aggregateStats := analysis.NewAtomicPostProcessingStats()
if stats, err := analysis.DeleteTransitEdges(ctx, db, ad.Entity, ad.Entity, adAnalysis.PostProcessedRelationships()...); err != nil {
return &aggregateStats, err
Expand All @@ -35,7 +35,7 @@ func Post(ctx context.Context, db graph.Database, adcsEnabled bool) (*analysis.A
return &aggregateStats, err
} else if syncLAPSStats, err := adAnalysis.PostSyncLAPSPassword(ctx, db, groupExpansions); err != nil {
return &aggregateStats, err
} else if localGroupStats, err := adAnalysis.PostLocalGroups(ctx, db, groupExpansions, false); err != nil {
} else if localGroupStats, err := adAnalysis.PostLocalGroups(ctx, db, groupExpansions, false, citrixEnabled); err != nil {
return &aggregateStats, err
} else if adcsStats, err := adAnalysis.PostADCS(ctx, db, groupExpansions, adcsEnabled); err != nil {
return &aggregateStats, err
Expand Down
70 changes: 61 additions & 9 deletions cmd/api/src/analysis/analysis_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestFetchRDPEnsureNoDescent(t *testing.T) {
require.Nil(t, err)

require.Nil(t, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
rdpEnabledEntityIDBitmap, err := analysis.FetchRDPEntityBitmapForComputer(tx, harness.RDPB.Computer.ID, groupExpansions, false)
rdpEnabledEntityIDBitmap, err := analysis.FetchRemoteDesktopUsersBitmapForComputer(tx, harness.RDPB.Computer.ID, groupExpansions, false)
require.Nil(t, err)

// We should expect all groups that have the RIL incoming privilege to the computer
Expand All @@ -55,7 +55,7 @@ func TestFetchRDPEnsureNoDescent(t *testing.T) {
})
}

func TestFetchRDPEntityBitmapForComputer(t *testing.T) {
func TestFetchRemoteDesktopUsersBitmapForComputer(t *testing.T) {
testContext := integration.NewGraphTestContext(t, schema.DefaultGraphSchema())
testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error {
harness.RDP.Setup(testContext)
Expand All @@ -66,34 +66,34 @@ func TestFetchRDPEntityBitmapForComputer(t *testing.T) {

// Enforced URA validation
require.Nil(t, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
rdpEnabledEntityIDBitmap, err := analysis.FetchRDPEntityBitmapForComputer(tx, harness.RDP.Computer.ID, groupExpansions, true)
rdpEnabledEntityIDBitmap, err := analysis.FetchRemoteDesktopUsersBitmapForComputer(tx, harness.RDP.Computer.ID, groupExpansions, true)
require.Nil(t, err)

// We should expect all groups that have the RIL incoming privilege to the computer
require.Equal(t, 6, int(rdpEnabledEntityIDBitmap.Cardinality()))
// We should expect all entities that have the RIL incoming privilege to the computer
require.Equal(t, 7, int(rdpEnabledEntityIDBitmap.Cardinality()))

require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.DillonUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.IrshadUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.UliUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.EliUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.DomainGroupA.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.DomainGroupB.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.RohanUser.ID.Uint32()))

require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.DomainGroupC.ID.Uint32()))
require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.DomainGroupD.ID.Uint32()))
require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.DomainGroupE.ID.Uint32()))
require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.RDPDomainUsersGroup.ID.Uint32()))
require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.AlyxUser.ID.Uint32()))
require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.AndyUser.ID.Uint32()))
require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.RohanUser.ID.Uint32()))
require.False(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.JohnUser.ID.Uint32()))

return nil
}))

// Unenforced URA validation
// Unenforced URA validation. result set should only include first degree members of `Remote Desktop Users` group
require.Nil(t, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
rdpEnabledEntityIDBitmap, err := analysis.FetchRDPEntityBitmapForComputer(tx, harness.RDP.Computer.ID, groupExpansions, false)
rdpEnabledEntityIDBitmap, err := analysis.FetchRemoteDesktopUsersBitmapForComputer(tx, harness.RDP.Computer.ID, groupExpansions, false)
require.Nil(t, err)

require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDP.IrshadUser.ID.Uint32()))
Expand Down Expand Up @@ -125,8 +125,9 @@ func TestFetchRDPEntityBitmapForComputer(t *testing.T) {
groupExpansions, err = analysis.ExpandAllRDPLocalGroups(context.Background(), db)
require.Nil(t, err)

// result set should only include first degree members of `Remote Desktop Users` group.
test.RequireNilErr(t, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
rdpEnabledEntityIDBitmap, err := analysis.FetchRDPEntityBitmapForComputer(tx, harness.RDP.Computer.ID, groupExpansions, true)
rdpEnabledEntityIDBitmap, err := analysis.FetchRemoteDesktopUsersBitmapForComputer(tx, harness.RDP.Computer.ID, groupExpansions, true)
require.Nil(t, err)

require.Equal(t, 6, int(rdpEnabledEntityIDBitmap.Cardinality()))
Expand All @@ -142,3 +143,54 @@ func TestFetchRDPEntityBitmapForComputer(t *testing.T) {
}))
})
}

func TestFetchRDPEntityBitmapForComputer(t *testing.T) {
testContext := integration.NewGraphTestContext(t, schema.DefaultGraphSchema())
testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error {
harness.RDPHarnessWithCitrix.Setup(testContext)
return nil
}, func(harness integration.HarnessDetails, db graph.Database) {
groupExpansions, err := analysis.ExpandAllRDPLocalGroups(context.Background(), db)
require.Nil(t, err)

// the Remote Desktop Users group does not have an RIL(Remote Interactive Login) edge to the computer.
require.Nil(t, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
rdpEnabledEntityIDBitmap, err := analysis.FetchCanRDPEntityBitmapForComputer(tx, harness.RDPHarnessWithCitrix.Computer.ID, groupExpansions, true, true)
require.Nil(t, err)

// We should expect the intersection of members of `Direct Access Users`, with entities that have the RIL privilege to the computer
require.Equal(t, 4, int(rdpEnabledEntityIDBitmap.Cardinality()))

require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDPHarnessWithCitrix.UliUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDPHarnessWithCitrix.IrshadUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDPHarnessWithCitrix.DillonUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDPHarnessWithCitrix.RohanUser.ID.Uint32()))

return nil
}))

// Create a RemoteInteractiveLogonPrivilege relationship from the RDP local group to the computer to test our most common case
require.Nil(t, db.WriteTransaction(context.Background(), func(tx graph.Transaction) error {
_, err := tx.CreateRelationshipByIDs(harness.RDPHarnessWithCitrix.RDPLocalGroup.ID, harness.RDPHarnessWithCitrix.Computer.ID, ad.RemoteInteractiveLogonPrivilege, graph.NewProperties())
return err
}))

// Recalculate group expansions
groupExpansions, err = analysis.ExpandAllRDPLocalGroups(context.Background(), db)
require.Nil(t, err)

require.Nil(t, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
rdpEnabledEntityIDBitmap, err := analysis.FetchCanRDPEntityBitmapForComputer(tx, harness.RDPHarnessWithCitrix.Computer.ID, groupExpansions, true, true)
require.Nil(t, err)

// We should expect the intersection of members of `Direct Access Users,` with entities that are first degree members of the `Remote Desktop Users` group
require.Equal(t, 3, int(rdpEnabledEntityIDBitmap.Cardinality()))

require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDPHarnessWithCitrix.DomainGroupC.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDPHarnessWithCitrix.IrshadUser.ID.Uint32()))
require.True(t, rdpEnabledEntityIDBitmap.Contains(harness.RDPHarnessWithCitrix.UliUser.ID.Uint32()))

return nil
}))
})
}
2 changes: 1 addition & 1 deletion cmd/api/src/analysis/membership_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestResolveAllGroupMemberships(t *testing.T) {
test.RequireNilErr(t, err)

require.Equal(t, 3, int(memberships.Cardinality(harness.RDP.DomainGroupA.ID.Uint32()).Cardinality()))
require.Equal(t, 2, int(memberships.Cardinality(harness.RDP.DomainGroupB.ID.Uint32()).Cardinality()))
require.Equal(t, 1, int(memberships.Cardinality(harness.RDP.DomainGroupB.ID.Uint32()).Cardinality()))
require.Equal(t, 1, int(memberships.Cardinality(harness.RDP.DomainGroupC.ID.Uint32()).Cardinality()))
require.Equal(t, 1, int(memberships.Cardinality(harness.RDP.DomainGroupD.ID.Uint32()).Cardinality()))
require.Equal(t, 2, int(memberships.Cardinality(harness.RDP.DomainGroupE.ID.Uint32()).Cardinality()))
Expand Down
9 changes: 8 additions & 1 deletion cmd/api/src/api/v2/app_config_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ func Test_GetAppConfigs(t *testing.T) {
var (
passwordExpirationWindowFound = false
neo4jConfigsFound = false
citrixConfigsFound = false
passwordExpirationValue appcfg.PasswordExpiration
neo4jParametersValue appcfg.Neo4jParameters
citrixConfigValue appcfg.CitrixRDPSupport
testCtx = integration.NewFOSSContext(t)
)

Expand All @@ -56,11 +58,16 @@ func Test_GetAppConfigs(t *testing.T) {
require.Equal(t, neo4j.DefaultBatchWriteSize, neo4jParametersValue.BatchWriteSize)
require.Equal(t, neo4j.DefaultWriteFlushSize, neo4jParametersValue.WriteFlushSize)
neo4jConfigsFound = true
case appcfg.CitrixRDPSupportKey:
mapParameter(t, &citrixConfigValue, parameter)
require.False(t, citrixConfigValue.Enabled)
citrixConfigsFound = true
}
}

require.True(t, passwordExpirationWindowFound, "Failed to find Password Expiration Window in response")
require.True(t, neo4jConfigsFound, "Failed to find Neo4J Configs in response")
require.True(t, citrixConfigsFound, "Failed to find Citrix configs in response")
}

func Test_GetAppConfigWithParameter(t *testing.T) {
Expand Down Expand Up @@ -107,7 +114,7 @@ func Test_PutAppConfig(t *testing.T) {
require.Equal(t, updatedDuration, updatedPasswordExpiration.Duration)
}

func mapParameter[T *appcfg.PasswordExpiration | *appcfg.Neo4jParameters](t *testing.T, value T, parameter appcfg.Parameter) {
func mapParameter[T *appcfg.PasswordExpiration | *appcfg.Neo4jParameters | *appcfg.CitrixRDPSupport](t *testing.T, value T, parameter appcfg.Parameter) {
err := parameter.Value.Map(&value)
require.Nilf(t, err, "Failed to map parameter value to %T type: %v", value, err)
}
2 changes: 1 addition & 1 deletion cmd/api/src/daemons/datapipe/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func RunAnalysisOperations(ctx context.Context, db database.Database, graphDB gr
// TODO: Cleanup #ADCSFeatureFlag after full launch.
if adcsFlag, err := db.GetFlagByKey(ctx, appcfg.FeatureAdcs); err != nil {
collectedErrors = append(collectedErrors, fmt.Errorf("error retrieving ADCS feature flag: %w", err))
} else if stats, err := ad.Post(ctx, graphDB, adcsFlag.Enabled); err != nil {
} else if stats, err := ad.Post(ctx, graphDB, adcsFlag.Enabled, appcfg.GetCitrixRDPSupport(ctx, db)); err != nil {
collectedErrors = append(collectedErrors, fmt.Errorf("error during ad post: %w", err))
adFailed = true
} else {
Expand Down
8 changes: 8 additions & 0 deletions cmd/api/src/database/migration/migrations/v5.16.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX IF NOT EXISTS idx_saved_queries_description ON saved_queries using gin(description gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_saved_queries_name ON saved_queries USING gin(name gin_trgm_ops);

VALUES (3,
'analysis.citrix_rdp_support',
'Citrix RDP Support',
'This configuration parameter toggles Citrix support during post-processing. When enabled, computers identified with a ''Direct Access Users''',
brandonshearin marked this conversation as resolved.
Show resolved Hide resolved
'{"enabled": false}',
current_timestamp,
current_timestamp) ON CONFLICT DO NOTHING;
30 changes: 30 additions & 0 deletions cmd/api/src/model/appcfg/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const (
Neo4jConfigsName = "Neo4j Configuration Parameters"
PasswordExpirationWindowDescription = "This configuration parameter sets the local auth password expiry window for users that have valid auth secrets. Values for this configuration must follow the duration specification of ISO-8601."
Neo4jConfigsDescription = "This configuration parameter sets the BatchWriteSize and the BatchFlushSize for Neo4J."

CitrixRDPSupportKey = "analysis.citrix_rdp_support"
CitrixRDPSupportName = "Citrix RDP Support"
CitrixRDPSupportDescription = "This configuration parameter toggles Citrix support during post-processing. When enabled, computers identified with a 'Direct Access Users' local group will assume that Citrix is installed and CanRDP edges will require membership of both 'Direct Access Users' and 'Remote Desktop Users' local groups on the computer."
)

// Parameter is a runtime configuration parameter that can be fetched from the appcfg.ParameterService interface. The
Expand Down Expand Up @@ -95,6 +99,10 @@ func AvailableParameters() (ParameterSet, error) {
WriteFlushSize: neo4j.DefaultWriteFlushSize,
}); err != nil {
return ParameterSet{}, fmt.Errorf("error creating neo4jExpirationValue parameter: %w", err)
} else if citrixRDPSupportValue, err := types.NewJSONBObject(CitrixRDPSupport{
Enabled: false,
}); err != nil {
return ParameterSet{}, fmt.Errorf("error creating CitrixRDPSupport parameter: %w", err)
} else {
return ParameterSet{
PasswordExpirationWindow: {
Expand All @@ -110,6 +118,12 @@ func AvailableParameters() (ParameterSet, error) {
Description: Neo4jConfigsDescription,
Value: neo4jExpirationValue,
},
CitrixRDPSupportKey: {
Key: CitrixRDPSupportKey,
Name: CitrixRDPSupportName,
Description: CitrixRDPSupportDescription,
Value: citrixRDPSupportValue,
},
}, nil
}
}
Expand Down Expand Up @@ -162,3 +176,19 @@ func GetNeo4jParameters(ctx context.Context, service ParameterService) Neo4jPara

return result
}

type CitrixRDPSupport struct {
Enabled bool `json:"enabled,omitempty"`
}

func GetCitrixRDPSupport(ctx context.Context, service ParameterService) bool {
var result CitrixRDPSupport

if cfg, err := service.GetConfigurationParameter(ctx, CitrixRDPSupportKey); err != nil {
log.Warnf("Failed to fetch CitrixRDPSupport configuration; returning default values")
} else if err := cfg.Map(&result); err != nil {
log.Warnf("Invalid CitrixRDPSupport configuration supplied, %v. returning default values.", err)
}

return result.Enabled
}
Loading
Loading