diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go index bda73f3ce8..d6f856be22 100644 --- a/cmd/api/src/analysis/ad/adcs_integration_test.go +++ b/cmd/api/src/analysis/ad/adcs_integration_test.go @@ -639,6 +639,33 @@ func TestADCSESC9a(t *testing.T) { } return nil }) + + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + if results, err := ops.FetchRelationships(tx.Relationships().Filterf(func() graph.Criteria { + return query.Kind(query.Relationship(), ad.ADCSESC9a) + })); err != nil { + t.Fatalf("error fetching esc9a edges in integration test; %v", err) + } else { + assert.Equal(t, 1, len(results)) + edge := results[0] + + if edgeComp, err := ad2.GetEdgeCompositionPath(context.Background(), db, edge); err != nil { + t.Fatalf("error getting edge composition for esc9: %v", err) + } else { + nodes := edgeComp.AllNodes().Slice() + assert.Contains(t, nodes, harness.ESC9AHarness.Attacker) + assert.Contains(t, nodes, harness.ESC9AHarness.Victim) + assert.Contains(t, nodes, harness.ESC9AHarness.Domain) + assert.Contains(t, nodes, harness.ESC9AHarness.NTAuthStore) + assert.Contains(t, nodes, harness.ESC9AHarness.RootCA) + assert.Contains(t, nodes, harness.ESC9AHarness.DC) + assert.Contains(t, nodes, harness.ESC9AHarness.EnterpriseCA) + assert.Contains(t, nodes, harness.ESC9AHarness.CertTemplate) + } + } + + return nil + }) }) } diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx index bc0e4217bb..2f46bf7109 100644 --- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx +++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx @@ -73,7 +73,7 @@ const EdgeInfoContent: FC<{ selectedEdge: NonNullable }> = ({ sele const sendOnChange = (selectedEdge.name === 'GoldenCert' || selectedEdge.name === 'ADCSESC1' || - selectedEdge.name === 'ADCSESC3') && + selectedEdge.name === 'ADCSESC3' || selectedEdge.name === 'ADCSESC9a') && section[0] === 'composition'; return ( diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 1c0d029a56..cdc9dadeab 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -549,6 +549,12 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph. } else { pathSet = results } + } else if edge.Kind == ad.ADCSESC9a { + if results, err := GetADCSESC9aEdgeComposition(ctx, db, edge); err != nil { + return err + } else { + pathSet = results + } } return nil }) @@ -556,7 +562,7 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph. func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -591,7 +597,7 @@ func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[ui func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -618,7 +624,7 @@ func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates c func ADCSESC3Path3Pattern() traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -807,7 +813,7 @@ func getDelegatedEnrollmentAgentPath(ctx context.Context, startNode, certTemplat func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -846,7 +852,7 @@ func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation { func ADCSESC1Path2Pattern(domainID graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -931,8 +937,6 @@ func GetADCSESC1EdgeComposition(ctx context.Context, db graph.Database, edge *gr // Render paths from the segments return paths, path1EnterpriseCAs.Each(func(value uint32) (bool, error) { for _, segment := range candidateSegments[graph.ID(value)] { - log.Infof("Found ESC1 Path: %s", graph.FormatPathSegment(segment)) - paths.AddPath(segment.Path()) } @@ -975,3 +979,248 @@ func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship return finalPaths, nil } } + +func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + OutboundWithDepth( + 1, 1, + query.And( + query.KindIn(query.Relationship(), ad.GenericWrite, ad.GenericAll, ad.Owns, ad.WriteOwner, ad.WriteDACL), + query.KindIn(query.End(), ad.Computer, ad.User), + ), + ). + OutboundWithDepth( + 0, 0, + query.And( + query.Kind(query.Relationship(), ad.MemberOf), + query.Kind(query.End(), ad.Group), + ), + ). + Outbound( + query.And( + query.KindIn(query.Relationship(), ad.GenericAll, ad.Enroll, ad.AllExtendedRights), + query.Kind(query.End(), ad.CertTemplate), + query.Equals(query.EndProperty(ad.RequiresManagerApproval.String()), false), + query.Equals(query.EndProperty(ad.AuthenticationEnabled.String()), true), + query.Equals(query.EndProperty(ad.NoSecurityExtension.String()), true), + query.Equals(query.EndProperty(ad.EnrolleeSuppliesSubject.String()), false), + query.Or( + query.Equals(query.EndProperty(ad.SubjectAltRequireUPN.String()), true), + query.Equals(query.EndProperty(ad.SubjectAltRequireSPN.String()), true), + ), + query.Or( + query.Equals(query.EndProperty(ad.SchemaVersion.String()), 1), + query.And( + query.GreaterThan(query.EndProperty(ad.SchemaVersion.String()), 1), + query.Equals(query.EndProperty(ad.AuthorizedSignatures.String()), 0), + ), + ), + ), + ). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.PublishedTo, ad.IssuedSignedBy), + query.Kind(query.End(), ad.EnterpriseCA), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.IssuedSignedBy, ad.EnterpriseCAFor), + query.Kind(query.End(), ad.RootCA), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.RootCAFor), + query.Equals(query.EndID(), domainID), + )) +} + +func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + OutboundWithDepth(0, 0, query.And( + query.Kind(query.Relationship(), ad.MemberOf), + query.Kind(query.End(), ad.Group), + )). + Outbound(query.And( + query.Kind(query.Relationship(), ad.Enroll), + query.InIDs(query.End(), caNodes...), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.TrustedForNTAuth), + query.Kind(query.End(), ad.NTAuthStore), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.NTAuthStoreFor), + query.Equals(query.EndID(), domainId), + )) +} + +func adcsESC9APath3Pattern(caIDs []graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Inbound( + query.KindIn(query.Relationship(), ad.DCFor, ad.TrustedBy), + ). + Inbound(query.And( + query.Kind(query.Relationship(), ad.CanAbuseWeakCertBinding), + query.InIDs(query.StartID(), caIDs...), + )) +} + +func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) { + /* + MATCH (n {objectid:'S-1-5-21-3933516454-2894985453-2515407000-500'})-[:ADCSESC9a]->(d:Domain {objectid:'S-1-5-21-3933516454-2894985453-2515407000'}) + OPTIONAL MATCH p1 = (n)-[:GenericAll|GenericWrite|Owns|WriteOwner|WriteDacl]->(m)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor|RootCAFor*1..]->(d) + WHERE ct.requiresmanagerapproval = false + AND ct.authenticationenabled = true + AND ct.nosecurityextension = true + AND ct.enrolleesuppliessubject = false + AND (ct.subjectaltrequireupn = true OR ct.subjectaltrequirespn = true) + AND ( + (ct.schemaversion > 1 AND ct.authorizedsignatures = 0) + OR ct.schemaversion = 1 + ) + AND ( + m:Computer + OR (m:User AND ct.subjectaltrequiredns = false AND ct.subjectaltrequiredomaindns = false) + ) + OPTIONAL MATCH p2 = (m)-[:MemberOf*0..]->()-[:Enroll]->(ca)-[:TrustedForNTAuth]->(nt)-[:NTAuthStoreFor]->(d) + OPTIONAL MATCH p3 = (ca)-[:CanAbuseWeakCertBinding|DCFor|TrustedBy*1..]->(d) + RETURN p1,p2,p3 + */ + + var ( + startNode *graph.Node + endNode *graph.Node + + traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) + paths = graph.PathSet{} + path1CandidateSegments = map[graph.ID][]*graph.PathSegment{} + victimCANodes = map[graph.ID][]graph.ID{} + path2CandidateSegments = map[graph.ID][]*graph.PathSegment{} + path3CandidateSegments = map[graph.ID][]*graph.PathSegment{} + p2canodes = make([]graph.ID, 0) + nodeMap = map[graph.ID]*graph.Node{} + lock = &sync.Mutex{} + ) + + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + if node, err := ops.FetchNode(tx, edge.StartID); err != nil { + return err + } else if eNode, err := ops.FetchNode(tx, edge.EndID); err != nil { + return err + } else { + startNode = node + endNode = eNode + return nil + } + }); err != nil { + return nil, err + } + + //Fully manifest p1 + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: startNode, + Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { + victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Depth() == 1 + }) + + if victimNode.Kinds.ContainsOneOf(ad.User) { + certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate) + }) + + if !certTemplateValidForUserVictim(certTemplate) { + return nil + } + } + + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path1CandidateSegments[victimNode.ID] = append(path1CandidateSegments[victimNode.ID], terminal) + nodeMap[victimNode.ID] = victimNode + victimCANodes[victimNode.ID] = append(victimCANodes[victimNode.ID], caNode.ID) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + + for victim, p1CANodes := range victimCANodes { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: nodeMap[victim], + Driver: adcsESC9APath2Pattern(p1CANodes, edge.EndID).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) + p2canodes = append(p2canodes, caNode.ID) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + } + + if len(p2canodes) > 0 { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: endNode, + Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) + lock.Unlock() + return nil + }), + }); err != nil { + return nil, err + } + } + + for _, p1paths := range path1CandidateSegments { + for _, p1path := range p1paths { + caNode := p1path.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + if p2segments, ok := path2CandidateSegments[caNode.ID]; !ok { + continue + } else if p3segments, ok := path3CandidateSegments[caNode.ID]; !ok { + continue + } else { + paths.AddPath(p1path.Path()) + for _, p2 := range p2segments { + paths.AddPath(p2.Path()) + } + + for _, p3 := range p3segments { + paths.AddPath(p3.Path()) + } + } + } + } + + return paths, nil +} + +func certTemplateValidForUserVictim(certTemplate *graph.Node) bool { + if subjectAltRequireDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil { + return false + } else if subjectAltRequireDNS { + return false + } else if subjectAltRequireDomainDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil { + return false + } else if subjectAltRequireDomainDNS { + return false + } else { + return true + } +} diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 3ec0b72b44..dcfe83f106 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -49,7 +49,9 @@ type PatternMatchDelegate = func(terminal *graph.PathSegment) error // The return value of the Do(...) function may be passed directly to a Traversal via a Plan as the Plan.Driver field. type PatternContinuation interface { Outbound(criteria ...graph.Criteria) PatternContinuation + OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation Inbound(criteria ...graph.Criteria) PatternContinuation + InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation Do(delegate PatternMatchDelegate) Driver } @@ -57,6 +59,8 @@ type PatternContinuation interface { type expansion struct { criteria []graph.Criteria direction graph.Direction + minDepth int + maxDepth int } func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, error) { @@ -84,6 +88,7 @@ func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, type patternTag struct { patternIdx int + depth int } func popSegmentPatternTag(segment *graph.PathSegment) *patternTag { @@ -95,6 +100,7 @@ func popSegmentPatternTag(segment *graph.PathSegment) *patternTag { } else { tag = &patternTag{ patternIdx: 0, + depth: 0, } } @@ -112,26 +118,62 @@ func (s *pattern) Do(delegate PatternMatchDelegate) Driver { return s.Driver } -// Outbound specifies the next outbound expansion step for this pattern. -func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation { +// OutboundWithDepth specifies the next outbound expansion step for this pattern with depth parameters. +func (s *pattern) OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { + if min < 0 { + min = 1 + log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1") + } + + if max < 0 { + max = 0 + log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0") + } + s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionOutbound, + minDepth: min, + maxDepth: max, }) return s } -// Inbound specifies the next inbound expansion step for this pattern. -func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { +// Outbound specifies the next outbound expansion step for this pattern. By default, this expansion will use a minimum +// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. +func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation { + return s.OutboundWithDepth(1, 0, criteria...) +} + +// InboundWithDepth specifies the next inbound expansion step for this pattern with depth parameters. +func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { + if min < 0 { + min = 1 + log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1") + } + + if max < 0 { + max = 0 + log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0") + } + s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionInbound, + minDepth: min, + maxDepth: max, }) return s } +// Inbound specifies the next inbound expansion step for this pattern. By default, this expansion will use a minimum +// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. +func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { + return s.InboundWithDepth(1, 0, criteria...) +} + // NewPattern returns a new PatternContinuation for building a new pattern. func NewPattern() PatternContinuation { return &pattern{} @@ -152,9 +194,9 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra for next := range cursor.Chan() { nextSegment := segment.Descend(next.Node, next.Relationship) nextSegment.Tag = &patternTag{ - // Use the tag's patternIdx here since this is the reference that will see the increment when - // the current expansion is exhausted + // Use the tag's patternIdx and depth since this is a continuation of the expansions patternIdx: tag.patternIdx, + depth: tag.depth + 1, } nextSegments = append(nextSegments, nextSegment) @@ -168,34 +210,51 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra if fetchDirection, err := currentExpansion.direction.Reverse(); err != nil { return nil, err } else { - // Perform the current expansion. - if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil { - return nil, err - } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { - return nil, err - } - - // No further expansions means this pattern segment is complete. Increment the pattern index to select the - // next pattern expansion. - tag.patternIdx++ - - // Perform the next expansion if there is one. - if tag.patternIdx < len(s.expansions) { - nextExpansion := s.expansions[tag.patternIdx] - - // Expand the next segments - if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil { + // If no max depth was set or if a max depth was set expand the current step further + if currentExpansion.maxDepth == 0 || tag.depth < currentExpansion.maxDepth { + // Perform the current expansion. + if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil { return nil, err } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { return nil, err } - } else if len(nextSegments) == 0 { - // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. - // Hand it off to the delegate and handle any returned error. - if err := s.delegate(segment); err != nil { - return nil, err + } + + // Check first if this current segment was fetched using the current expansion (i.e. non-optional) + if tag.depth > 0 && currentExpansion.minDepth == 0 || tag.depth >= currentExpansion.minDepth { + // No further expansions means this pattern segment is complete. Increment the pattern index to select the + // next pattern expansion. Additionally, set the depth back to zero for the tag since we are leaving the + // current expansion. + tag.patternIdx++ + tag.depth = 0 + + // Perform the next expansion if there is one. + if tag.patternIdx < len(s.expansions) { + nextExpansion := s.expansions[tag.patternIdx] + + // Expand the next segments + if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil { + return nil, err + } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { + return nil, err + } + + // If the next expansion is optional, make sure to preserve the current traversal branch + if nextExpansion.minDepth == 0 { + // Reattach the tag to the segment before adding it to the returned segments for the next expansion + segment.Tag = tag + nextSegments = append(nextSegments, segment) + } + } else if len(nextSegments) == 0 { + // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. + // Hand it off to the delegate and handle any returned error. + if err := s.delegate(segment); err != nil { + return nil, err + } } } + + // If the above condition does not match then this current expansion is non-terminal and non-continuable } // Return any collected segments diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx index 9de2b72c5f..7dcde37b44 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx @@ -19,6 +19,7 @@ import WindowsAbuse from './WindowsAbuse'; import LinuxAbuse from './LinuxAbuse'; import Opsec from './Opsec'; import References from './References'; +import Composition from "./Composition"; const ADCSESC9a = { general: General, @@ -26,6 +27,7 @@ const ADCSESC9a = { linuxAbuse: LinuxAbuse, opsec: Opsec, references: References, + composition: Composition }; export default ADCSESC9a; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx new file mode 100644 index 0000000000..4a24ff5eab --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx @@ -0,0 +1,54 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { FC } from 'react'; +import { Alert, Box, Skeleton, Typography } from '@mui/material'; +import { apiClient } from '../../../utils/api'; +import { EdgeInfoProps } from '..'; +import { useQuery } from 'react-query'; +import VirtualizedNodeList, { VirtualizedNodeListItem } from '../../VirtualizedNodeList'; + +const Composition: FC = ({ sourceDBId, targetDBId, edgeName }) => { + const { data, isLoading, isError } = useQuery(['edgeComposition', sourceDBId, targetDBId, edgeName], ({ signal }) => + apiClient.getEdgeComposition(sourceDBId!, targetDBId!, edgeName!).then((result) => result.data) + ); + + const nodesArray: VirtualizedNodeListItem[] = Object.values(data?.data.nodes || {}).map((node) => ({ + name: node.label, + objectId: node.objectId, + kind: node.kind, + })); + + return ( + <> + + The relationship represents the effective outcome of the configuration and relationships between several + different objects. All objects involved in the creation of this relationship are listed here: + + + {isLoading ? ( + + ) : isError ? ( + Couldn't load edge composition + ) : ( + + )} + + + ); +}; + +export default Composition;