diff --git a/cmd/api/src/analysis/azure/azure_integration_test.go b/cmd/api/src/analysis/azure/azure_integration_test.go index 90f81a735f..3453df27a4 100644 --- a/cmd/api/src/analysis/azure/azure_integration_test.go +++ b/cmd/api/src/analysis/azure/azure_integration_test.go @@ -127,7 +127,7 @@ func TestAzureEntityGroupMembership(t *testing.T) { if groupPaths, err := azureanalysis.FetchEntityGroupMembershipPaths(tx, harness.AZBaseHarness.User); err != nil { t.Fatal(err) } else { - assert.ElementsMatch(t, harness.AZBaseHarness.UserFirstDegreeGroups.IDs(), groupPaths.AllNodes().ContainingNodeKinds(azure.Group).IDs()) + assert.ElementsMatch(t, harness.AZBaseHarness.UserFirstDegreeGroups.IDs(), groupPaths.AllNodes().ContainingNodeKinds(azure.Group, azure.Group365).IDs()) } }) } @@ -554,6 +554,31 @@ func TestGroupEntityDetails(t *testing.T) { }) } +func TestGroup365EntityDetails(t *testing.T) { + testContext := integration.NewGraphTestContext(t, schema.DefaultGraphSchema()) + testContext.ReadTransactionTestWithSetup(func(harness *integration.HarnessDetails) error { + harness.AZEntityPanelHarness.Setup(testContext) + return nil + + }, func(harness integration.HarnessDetails, tx graph.Transaction) { + + groupObjectID, err := harness.AZEntityPanelHarness.Group365.Properties.Get(common.ObjectID.String()).String() + require.Nil(t, err) + + assert.NotEqual(t, "", groupObjectID) + + group, err := azureanalysis.Group365EntityDetails(testContext.Graph.Database, groupObjectID, false) + + require.Nil(t, err) + assert.Equal(t, harness.AZEntityPanelHarness.Group365.Properties.Get(common.ObjectID.String()).Any(), group.Properties[common.ObjectID.String()]) + assert.Equal(t, 0, group.InboundObjectControl) + + group, err = azureanalysis.Group365EntityDetails(testContext.Graph.Database, groupObjectID, true) + require.Nil(t, err) + assert.NotEqual(t, 0, group.InboundObjectControl) + }) +} + func TestManagementGroupEntityDetails(t *testing.T) { testContext := integration.NewGraphTestContext(t, schema.DefaultGraphSchema()) testContext.ReadTransactionTestWithSetup(func(harness *integration.HarnessDetails) error { @@ -807,11 +832,13 @@ func TestFetchInboundEntityObjectControlPaths(t *testing.T) { paths, err := azureanalysis.FetchInboundEntityObjectControlPaths(tx, harness.AZInboundControlHarness.ControlledAZUser) require.Nil(t, err) nodes := paths.AllNodes().IDs() - require.Equal(t, 8, len(nodes)) + require.Equal(t, 10, len(nodes)) require.NotContains(t, nodes, harness.AZInboundControlHarness.AZAppA.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.ControlledAZUser.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupA.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupB.ID) + require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365A.ID) + require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365B.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalA.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalB.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZUserA.ID) @@ -830,11 +857,13 @@ func TestFetchInboundEntityObjectControllers(t *testing.T) { control, err := azureanalysis.FetchInboundEntityObjectControllers(tx, harness.AZInboundControlHarness.ControlledAZUser, 0, 0) require.Nil(t, err) nodes := control.IDs() - require.Equal(t, 7, len(nodes)) + require.Equal(t, 9, len(nodes)) require.NotContains(t, nodes, harness.AZInboundControlHarness.ControlledAZUser.ID) require.NotContains(t, nodes, harness.AZInboundControlHarness.AZAppA.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupA.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupB.ID) + require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365A.ID) + require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365B.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalA.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalB.ID) require.Contains(t, nodes, harness.AZInboundControlHarness.AZUserA.ID) diff --git a/cmd/api/src/analysis/azure/queries.go b/cmd/api/src/analysis/azure/queries.go index 9be76367a6..b3aa3971db 100644 --- a/cmd/api/src/analysis/azure/queries.go +++ b/cmd/api/src/analysis/azure/queries.go @@ -99,6 +99,10 @@ func GraphStats(ctx context.Context, db graph.Database) (model.AzureDataQualityS stat.Groups = int(count) aggregation.Groups += int(count) + case azure.Group365: + stat.Groups365 = int(count) + aggregation.Groups365 += int(count) + case azure.App: stat.Apps = int(count) aggregation.Apps += int(count) diff --git a/cmd/api/src/analysis/azure/queries_test.go b/cmd/api/src/analysis/azure/queries_test.go index 10f57150a5..8922c1682f 100644 --- a/cmd/api/src/analysis/azure/queries_test.go +++ b/cmd/api/src/analysis/azure/queries_test.go @@ -41,6 +41,7 @@ func TestAnalysisAzure_GraphStats(t *testing.T) { assert.NotZero(t, agg.Tenants) assert.NotZero(t, agg.Users) assert.NotZero(t, agg.Groups) + assert.NotZero(t, agg.Groups365) assert.NotZero(t, agg.Apps) assert.NotZero(t, agg.ServicePrincipals) assert.NotZero(t, agg.Devices) diff --git a/cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go b/cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go index 7b379adae8..6e8e14edd7 100644 --- a/cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go +++ b/cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go @@ -150,6 +150,10 @@ func (s *BloodHoundGraphNode) SetIcon(nType string) { s.FontIcon = &BloodHoundGraphFontIcon{ Text: "fa-users", } + case "AZGroup365": + s.FontIcon = &BloodHoundGraphFontIcon{ + Text: "fa-users", + } case "AZKeyVault": s.FontIcon = &BloodHoundGraphFontIcon{ Text: "fa-lock", @@ -319,6 +323,8 @@ func (s *BloodHoundGraphNode) SetBackground(nType string) { s.BloodHoundGraphItem.Color = "#17E625" case "Group": s.BloodHoundGraphItem.Color = "#DBE617" + case "AZGroup365": + s.BloodHoundGraphItem.Color = "#34D2EB" case "Computer": s.BloodHoundGraphItem.Color = "#E67873" case "Container": diff --git a/cmd/api/src/api/v2/azure.go b/cmd/api/src/api/v2/azure.go index 74f3ec6d37..a6e02cd4b3 100644 --- a/cmd/api/src/api/v2/azure.go +++ b/cmd/api/src/api/v2/azure.go @@ -45,6 +45,7 @@ const ( entityTypeBase = "az-base" entityTypeUsers = "users" entityTypeGroups = "groups" + entityTypeGroups365 = "groups365" entityTypeTenants = "tenants" entityTypeManagementGroups = "management-groups" entityTypeSubscriptions = "subscriptions" @@ -339,6 +340,9 @@ func GetAZEntityInformation(ctx context.Context, db graph.Database, entityType, case entityTypeGroups: return azure.GroupEntityDetails(db, objectID, hydrateCounts) + case entityTypeGroups365: + return azure.Group365EntityDetails(db, objectID, hydrateCounts) + case entityTypeTenants: return azure.TenantEntityDetails(db, objectID, hydrateCounts) diff --git a/cmd/api/src/daemons/datapipe/azure_convertors.go b/cmd/api/src/daemons/datapipe/azure_convertors.go index 0149c6083b..e238cb15c4 100644 --- a/cmd/api/src/daemons/datapipe/azure_convertors.go +++ b/cmd/api/src/daemons/datapipe/azure_convertors.go @@ -55,10 +55,16 @@ func getKindConverter(kind enums.Kind) func(json.RawMessage, *ConvertedAzureData return convertAzureFunctionAppRoleAssignment case enums.KindAZGroup: return convertAzureGroup + case enums.KindAZGroup365: + return convertAzureGroup365 case enums.KindAZGroupMember: return convertAzureGroupMember + case enums.KindAZGroup365Member: + return convertAzureGroup365Member case enums.KindAZGroupOwner: return convertAzureGroupOwner + case enums.KindAZGroup365Owner: + return convertAzureGroup365Owner case enums.KindAZKeyVault: return convertAzureKeyVault case enums.KindAZKeyVaultAccessPolicy: @@ -282,6 +288,24 @@ func convertAzureGroup(raw json.RawMessage, converted *ConvertedAzureData) { } } +func convertAzureGroup365(raw json.RawMessage, converted *ConvertedAzureData) { + + var data models.Group365 + + if err := json.Unmarshal(raw, &data); err != nil { + + slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 36 group", err)) + + } else { + + converted.NodeProps = append(converted.NodeProps, ein.ConvertAzureGroup365ToNode(data)) + + converted.RelProps = append(converted.RelProps, ein.ConvertAzureGroup365ToRel(data)) + + } + +} + func convertAzureGroupMember(raw json.RawMessage, converted *ConvertedAzureData) { var ( data models.GroupMembers @@ -294,6 +318,18 @@ func convertAzureGroupMember(raw json.RawMessage, converted *ConvertedAzureData) } } +func convertAzureGroup365Member(raw json.RawMessage, converted *ConvertedAzureData) { + var ( + data models.Group365Members + ) + + if err := json.Unmarshal(raw, &data); err != nil { + slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 365 group members", err)) + } else { + converted.RelProps = append(converted.RelProps, ein.ConvertAzureGroup365MembersToRels(data)...) + } +} + func convertAzureGroupOwner(raw json.RawMessage, converted *ConvertedAzureData) { var ( data models.GroupOwners @@ -305,6 +341,17 @@ func convertAzureGroupOwner(raw json.RawMessage, converted *ConvertedAzureData) } } +func convertAzureGroup365Owner(raw json.RawMessage, converted *ConvertedAzureData) { + var ( + data models.Group365Owners + ) + if err := json.Unmarshal(raw, &data); err != nil { + slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 365 group owners", err)) + } else { + converted.RelProps = append(converted.RelProps, ein.ConvertAzureGroup365OwnerToRels(data)...) + } +} + func convertAzureKeyVault(raw json.RawMessage, converted *ConvertedAzureData) { var data models.KeyVault if err := json.Unmarshal(raw, &data); err != nil { diff --git a/cmd/api/src/database/dataquality.go b/cmd/api/src/database/dataquality.go index d51c1f7bd0..1512d3e816 100644 --- a/cmd/api/src/database/dataquality.go +++ b/cmd/api/src/database/dataquality.go @@ -77,6 +77,7 @@ WITH aggregated_quality_stats AS ( DATE_TRUNC('day', created_at) AS created_date, MAX(users) AS max_users, MAX(groups) AS max_groups, + MAX(groups365) AS max_groups365, MAX(computers) AS max_computers, MAX(ous) AS max_ous, MAX(containers) AS max_containers, @@ -101,6 +102,7 @@ SELECT created_date AS created_at, SUM(max_users) AS users, SUM(max_groups) AS groups, + SUM(max_groups365) AS groups365, SUM(max_computers) AS computers, SUM(max_ous) AS ous, SUM(max_containers) AS containers, diff --git a/cmd/api/src/database/migration/migrations/v7.4.0.sql b/cmd/api/src/database/migration/migrations/v7.4.0.sql new file mode 100644 index 0000000000..32ff3cca5e --- /dev/null +++ b/cmd/api/src/database/migration/migrations/v7.4.0.sql @@ -0,0 +1,25 @@ +-- Copyright 2025 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 + +-- Migration to add `Microsoft 365 groups` column to relevant tables + +-- Add `groups365` column to `azure_data_quality_aggregations` table +ALTER TABLE IF EXISTS azure_data_quality_aggregations + ADD COLUMN groups365 bigint; + +-- Add `groups365` column to `azure_data_quality_stats` table +ALTER TABLE IF EXISTS azure_data_quality_stats + ADD COLUMN groups365 bigint; \ No newline at end of file diff --git a/cmd/api/src/migrations/manifest.go b/cmd/api/src/migrations/manifest.go index 46605c7f49..88bde1e0ca 100644 --- a/cmd/api/src/migrations/manifest.go +++ b/cmd/api/src/migrations/manifest.go @@ -155,7 +155,7 @@ func Version_508_Migration(ctx context.Context, db graph.Database) error { return query.And( query.Kind(query.Start(), azure.Entity), // Not all of these node types are being changed, but there's no harm in adding them to the migration - query.KindIn(query.End(), azure.ManagementGroup, azure.ResourceGroup, azure.Subscription, azure.KeyVault, azure.AutomationAccount, azure.ContainerRegistry, azure.LogicApp, azure.VMScaleSet, azure.WebApp, azure.FunctionApp, azure.ManagedCluster, azure.VM), + query.KindIn(query.End(), azure.ManagementGroup, azure.ResourceGroup, azure.Subscription, azure.KeyVault, azure.AutomationAccount, azure.ContainerRegistry, azure.LogicApp, azure.VMScaleSet, azure.WebApp, azure.FunctionApp, azure.ManagedCluster, azure.VM, azure.Group365), query.Kind(query.Relationship(), azure.Owns), ) }).Fetch(func(cursor graph.Cursor[*graph.Relationship]) error { diff --git a/cmd/api/src/model/azurequality.go b/cmd/api/src/model/azurequality.go index d24d309379..0ac134eb15 100644 --- a/cmd/api/src/model/azurequality.go +++ b/cmd/api/src/model/azurequality.go @@ -20,6 +20,7 @@ type AzureStatKinds struct { Relationships int `json:"relationships"` Users int `json:"users"` Groups int `json:"groups"` + Groups365 int `json:"groups365"` Apps int `json:"apps"` ServicePrincipals int `json:"service_principals"` Devices int `json:"devices"` diff --git a/cmd/api/src/test/integration/graph.go b/cmd/api/src/test/integration/graph.go index 8e65f9962e..b4f230a259 100644 --- a/cmd/api/src/test/integration/graph.go +++ b/cmd/api/src/test/integration/graph.go @@ -228,6 +228,17 @@ func (s *GraphTestContext) NewAzureGroup(name, objectID, tenantID string) *graph }), azure.Entity, azure.Group) } +func (s *GraphTestContext) NewAzureGroup365(name, objectID, tenantID string) *graph.Node { + + return s.NewNode(graph.AsProperties(graph.PropertyMap{ + + common.Name: name, + common.ObjectID: objectID, + azure.TenantID: tenantID, + azure.IsAssignableToRole: true, + }), azure.Entity, azure.Group365) +} + func (s *GraphTestContext) NewAzureVM(name, objectID, tenantID string) *graph.Node { return s.NewNode(graph.AsProperties(graph.PropertyMap{ common.Name: name, diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index fea0151f22..6a40ca0dfd 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -806,7 +806,7 @@ func (s *AZBaseHarness) Setup(testCtx *GraphTestContext) { s.ServicePrincipal = testCtx.NewAzureServicePrincipal(HarnessServicePrincipalName, RandomObjectID(testCtx.testCtx), tenantID) s.Nodes.Add(s.Tenant, s.User, s.Application, s.ServicePrincipal) s.UserFirstDegreeGroups = graph.NewNodeSet() - s.NumPaths = 1287 + s.NumPaths = 1307 // Tie the user to the tenant and vice-versa // Note: This will cause a full re-traversal of paths outbound from the user object @@ -816,6 +816,7 @@ func (s *AZBaseHarness) Setup(testCtx *GraphTestContext) { // Create some MemberOf relationships for the new user for nestingDepth := numGroups; nestingDepth > 0; nestingDepth-- { newGroups := s.CreateAzureNestedGroupChain(testCtx, tenantID, nestingDepth) + newGroups.AddSet(s.CreateAzureNestedGroup365Chain(testCtx, tenantID, nestingDepth)) s.Nodes.Add(newGroups.Slice()...) for _, newGroup := range newGroups { @@ -892,12 +893,38 @@ func (s *AZBaseHarness) CreateAzureNestedGroupChain(testCtx *GraphTestContext, t return groupNodes } +func (s *AZBaseHarness) CreateAzureNestedGroup365Chain(testCtx *GraphTestContext, tenantID string, chainDepth int) graph.NodeSet { + var ( + previousGroup *graph.Node + groupNodes = graph.NewNodeSet() + ) + + for groupIdx := 0; groupIdx < chainDepth; groupIdx++ { + var ( + objectID = RandomObjectID(testCtx.testCtx) + newGroup = testCtx.NewAzureGroup365(fmt.Sprintf("AZGroup365_%s", objectID), objectID, tenantID) + ) + + if previousGroup == nil { + testCtx.NewRelationship(s.User, newGroup, azure.MemberOf) + s.UserFirstDegreeGroups.Add(newGroup) + } else { + testCtx.NewRelationship(previousGroup, newGroup, azure.MemberOf) + } + + groupNodes.Add(newGroup) + previousGroup = newGroup + } + return groupNodes +} + type AZGroupMembershipHarness struct { - Tenant *graph.Node - UserA *graph.Node - UserB *graph.Node - UserC *graph.Node - Group *graph.Node + Tenant *graph.Node + UserA *graph.Node + UserB *graph.Node + UserC *graph.Node + Group *graph.Node + Group365 *graph.Node } func (s *AZGroupMembershipHarness) Setup(testCtx *GraphTestContext) { @@ -907,12 +934,17 @@ func (s *AZGroupMembershipHarness) Setup(testCtx *GraphTestContext) { s.UserB = testCtx.NewAzureUser("UserB", "UserB", "", RandomObjectID(testCtx.testCtx), "", tenantID, false) s.UserC = testCtx.NewAzureUser("UserC", "UserC", "", RandomObjectID(testCtx.testCtx), "", tenantID, false) s.Group = testCtx.NewAzureGroup("Group", RandomObjectID(testCtx.testCtx), tenantID) + s.Group365 = testCtx.NewAzureGroup365("Group365", RandomObjectID(testCtx.testCtx), tenantID) testCtx.NewRelationship(s.Tenant, s.Group, azure.Contains) + testCtx.NewRelationship(s.Tenant, s.Group365, azure.Contains) testCtx.NewRelationship(s.UserA, s.Group, azure.MemberOf) testCtx.NewRelationship(s.UserB, s.Group, azure.MemberOf) testCtx.NewRelationship(s.UserC, s.Group, azure.MemberOf) + testCtx.NewRelationship(s.UserA, s.Group365, azure.MemberOf) + testCtx.NewRelationship(s.UserB, s.Group365, azure.MemberOf) + testCtx.NewRelationship(s.UserC, s.Group365, azure.MemberOf) } type AZManagementGroupHarness struct { @@ -941,6 +973,7 @@ type AZEntityPanelHarness struct { Application *graph.Node Device *graph.Node Group *graph.Node + Group365 *graph.Node ManagementGroup *graph.Node ResourceGroup *graph.Node KeyVault *graph.Node @@ -957,6 +990,7 @@ func (s *AZEntityPanelHarness) Setup(testCtx *GraphTestContext) { s.Application = testCtx.NewAzureApplication("App", RandomObjectID(testCtx.testCtx), tenantID) s.Device = testCtx.NewAzureDevice("Device", RandomObjectID(testCtx.testCtx), RandomObjectID(testCtx.testCtx), tenantID) s.Group = testCtx.NewAzureGroup("Group", RandomObjectID(testCtx.testCtx), tenantID) + s.Group365 = testCtx.NewAzureGroup("Group365", RandomObjectID(testCtx.testCtx), tenantID) s.ManagementGroup = testCtx.NewAzureResourceGroup("Mgmt Group", RandomObjectID(testCtx.testCtx), tenantID) s.ResourceGroup = testCtx.NewAzureResourceGroup("Resource Group", RandomObjectID(testCtx.testCtx), tenantID) s.KeyVault = testCtx.NewAzureKeyVault("Key Vault", RandomObjectID(testCtx.testCtx), tenantID) @@ -978,6 +1012,7 @@ func (s *AZEntityPanelHarness) Setup(testCtx *GraphTestContext) { testCtx.NewRelationship(s.User, s.Group, azure.Owns) testCtx.NewRelationship(s.User, s.ResourceGroup, azure.Owns) testCtx.NewRelationship(s.User, s.ManagementGroup, azure.Owner) + testCtx.NewRelationship(s.User, s.Group365, azure.Owns) // Key Vault testCtx.NewRelationship(s.User, s.KeyVault, azure.Owns) @@ -1209,6 +1244,8 @@ type AZInboundControlHarness struct { AZAppA *graph.Node AZGroupA *graph.Node AZGroupB *graph.Node + AZGroup365A *graph.Node + AZGroup365B *graph.Node AZUserA *graph.Node AZUserB *graph.Node AZServicePrincipalA *graph.Node @@ -1222,20 +1259,27 @@ func (s *AZInboundControlHarness) Setup(testCtx *GraphTestContext) { s.AZAppA = testCtx.NewAzureApplication("AZAppA", RandomObjectID(testCtx.testCtx), tenantID) s.AZGroupA = testCtx.NewAzureGroup("AZGroupA", RandomObjectID(testCtx.testCtx), tenantID) s.AZGroupB = testCtx.NewAzureGroup("AZGroupB", RandomObjectID(testCtx.testCtx), tenantID) + s.AZGroup365A = testCtx.NewAzureGroup365("AZGroup365A", RandomObjectID(testCtx.testCtx), tenantID) + s.AZGroup365B = testCtx.NewAzureGroup365("AZGroup365B", RandomObjectID(testCtx.testCtx), tenantID) s.AZUserA = testCtx.NewAzureUser("AZUserA", "AZUserA", "", RandomObjectID(testCtx.testCtx), HarnessUserLicenses, tenantID, HarnessUserMFAEnabled) s.AZUserB = testCtx.NewAzureUser("AZUserB", "AZUserB", "", RandomObjectID(testCtx.testCtx), HarnessUserLicenses, tenantID, HarnessUserMFAEnabled) s.AZServicePrincipalA = testCtx.NewAzureServicePrincipal("AZServicePrincipalA", RandomObjectID(testCtx.testCtx), tenantID) s.AZServicePrincipalB = testCtx.NewAzureServicePrincipal("AZServicePrincipalB", RandomObjectID(testCtx.testCtx), tenantID) testCtx.NewRelationship(s.AZTenant, s.AZGroupA, azure.Contains) + testCtx.NewRelationship(s.AZTenant, s.AZGroup365A, azure.Contains) testCtx.NewRelationship(s.AZUserA, s.AZGroupA, azure.MemberOf) testCtx.NewRelationship(s.AZServicePrincipalB, s.AZGroupB, azure.MemberOf) + testCtx.NewRelationship(s.AZUserA, s.AZGroup365A, azure.MemberOf) + testCtx.NewRelationship(s.AZServicePrincipalB, s.AZGroup365B, azure.MemberOf) testCtx.NewRelationship(s.AZAppA, s.AZServicePrincipalA, azure.RunsAs) testCtx.NewRelationship(s.AZGroupA, s.ControlledAZUser, azure.ResetPassword) testCtx.NewRelationship(s.AZGroupB, s.ControlledAZUser, azure.ResetPassword) + testCtx.NewRelationship(s.AZGroup365A, s.ControlledAZUser, azure.ResetPassword) + testCtx.NewRelationship(s.AZGroup365B, s.ControlledAZUser, azure.ResetPassword) testCtx.NewRelationship(s.AZUserB, s.ControlledAZUser, azure.ResetPassword) testCtx.NewRelationship(s.AZServicePrincipalA, s.ControlledAZUser, azure.ResetPassword) } diff --git a/cmd/ui/src/ducks/entityinfo/types.ts b/cmd/ui/src/ducks/entityinfo/types.ts index 72eb4ad651..5312334a7e 100644 --- a/cmd/ui/src/ducks/entityinfo/types.ts +++ b/cmd/ui/src/ducks/entityinfo/types.ts @@ -305,6 +305,11 @@ export interface AZGroupInfo extends AZEntityInfo { roles: number; } +export interface AZGroup365Info extends AZEntityInfo { + props: BasicInfo; + inbound_object_control: number; +} + export interface AZKeyVaultInfo extends AZEntityInfo { props: BasicInfo; Readers: { diff --git a/cmd/ui/src/ducks/graph/graphutils.ts b/cmd/ui/src/ducks/graph/graphutils.ts index 1227107075..92d66df5ee 100644 --- a/cmd/ui/src/ducks/graph/graphutils.ts +++ b/cmd/ui/src/ducks/graph/graphutils.ts @@ -227,6 +227,7 @@ const ICONS: { [id in GraphNodeTypes]: string } = { [GraphNodeTypes.Domain]: 'fa-globe', [GraphNodeTypes.GPO]: 'fa-th-list', [GraphNodeTypes.Group]: 'fa-users', + [GraphNodeTypes.AZGroup365]: 'fa-users', [GraphNodeTypes.OU]: 'fa-sitemap', [GraphNodeTypes.User]: 'fa-user', [GraphNodeTypes.Container]: 'fa-box', diff --git a/cmd/ui/src/ducks/graph/types.ts b/cmd/ui/src/ducks/graph/types.ts index 3ee5002d3b..b233eedde4 100644 --- a/cmd/ui/src/ducks/graph/types.ts +++ b/cmd/ui/src/ducks/graph/types.ts @@ -22,6 +22,7 @@ export enum GraphNodeTypes { AZDevice = 'AZDevice', AZFunctionApp = 'AZFunctionApp', AZGroup = 'AZGroup', + AZGroup365 = 'AZGroup365', AZKeyVault = 'AZKeyVault', AZManagementGroup = 'AZManagementGroup', AZResourceGroup = 'AZResourceGroup', diff --git a/docs/resources/nodes/az-group365.mdx b/docs/resources/nodes/az-group365.mdx new file mode 100644 index 0000000000..e3d59590c0 --- /dev/null +++ b/docs/resources/nodes/az-group365.mdx @@ -0,0 +1,30 @@ +--- +title: AZGroup365 +--- + + + +## Node properties +The node supports the properties of the table below. + + +Properties which are blank/null will not be shown in the Entity Panel. + + +| | | +| --- | --- | +| **Entity Panel name** | **Description** | +| Tier Zero / High Value | BloodHound Enterprise: Whether the object is part of Tier Zero of the Microsoft's Active Directory Tier Model, or the Control Plane of Microsoft's Enterprise Access Model.

BloodHound CE: Whether the object is currently marked as High Value. By default any object that belongs to Tier Zero is marked as High Value. | +| Display Name | The display name for the object. | +| Object ID | The object's security identifier (SID), a unique identifier in the directory. | +| Admin Count | Whether the object currently, or possibly ever has belonged to a certain set of highly privileged groups. For Active Directory nodes this is related to the AdminSDHolder object and the SDProp process, read about that [here](https://adsecurity.org/?p=2053). | +| Created | The time when the object was created in the directory. | +| Description | The contents of the description field for the object. | +| Is Role Assignable | Whether the group can be assigned to Azure roles. When set to "True," group members inherit role-based permissions. When set to "False," role assignments are not allowed for the group. | +| On-Prem Sync Enabled | Whether the object is synchronized to on-premises Active Directory. | +| Security Enabled | Whether the group is a Security Principal, meaning it can be used to secure objects in Entra ID. | +| Security Identifier | - | +| Mail | The mail of the Group | +| Visibility | Set to "True" if the group can be join by anyone, "False" if not | +| Tenant ID | Unique identifier for the Azure tenant. | + diff --git a/packages/cue/bh/azure/azure.cue b/packages/cue/bh/azure/azure.cue index f4d82c412b..af8904505c 100644 --- a/packages/cue/bh/azure/azure.cue +++ b/packages/cue/bh/azure/azure.cue @@ -254,6 +254,21 @@ TenantID: types.#StringEnum & { representation: "tenantid" } +Visibility: types.#StringEnum & { + symbol: "Visibility" + schema: "azure" + name: "Visibility" + representation: "visibility" +} + +M365GroupMail: types.#StringEnum & { + symbol: "Mail" + schema: "azure" + name: "M365 Group Mail" + representation: "mail" +} + + Properties: [ AppOwnerOrganizationID, AppDescription, @@ -287,6 +302,8 @@ Properties: [ PublisherDomain, SignInAudience, RoleTemplateID, + Visibility, + M365GroupMail, ] // Kinds @@ -332,6 +349,12 @@ Group: types.#Kind & { representation: "AZGroup" } +Group365: types.#Kind & { + symbol: "Group365" + schema: "azure" + representation: "AZGroup365" +} + KeyVault: types.#Kind & { symbol: "KeyVault" schema: "azure" @@ -418,6 +441,7 @@ NodeKinds: [ Device, FunctionApp, Group, + Group365, KeyVault, ManagementGroup, ResourceGroup, diff --git a/packages/go/analysis/analysis_test.go b/packages/go/analysis/analysis_test.go index 2b7ebc8288..17a54f1cd5 100644 --- a/packages/go/analysis/analysis_test.go +++ b/packages/go/analysis/analysis_test.go @@ -105,6 +105,7 @@ func TestGetNodeKindDisplayLabel(t *testing.T) { assert.Equal(ad.Group.String(), analysis.GetNodeKindDisplayLabel(graph.PrepareNode(graph.NewProperties(), ad.Entity, ad.Group, ad.LocalGroup)), "should return valid kind other than LocalGroup if one is present") assert.Equal(ad.LocalGroup.String(), analysis.GetNodeKindDisplayLabel(graph.PrepareNode(graph.NewProperties(), ad.Entity, ad.LocalGroup)), "should return LocalGroup if no other valid kinds are present") assert.Equal(azure.Group.String(), analysis.GetNodeKindDisplayLabel(graph.PrepareNode(graph.NewProperties(), azure.Entity, azure.Group)), "should return valid Azure kind when base and kind are present") + assert.Equal(azure.Group365.String(), analysis.GetNodeKindDisplayLabel(graph.PrepareNode(graph.NewProperties(), azure.Entity, azure.Group365)), "should return valid Azure kind when base and kind are present") assert.Equal(analysis.NodeKindUnknown, analysis.GetNodeKindDisplayLabel(graph.PrepareNode(graph.NewProperties(), unsupportedKind)), "should return Unknown when only an unsupported kind is present") assert.Equal(ad.Entity.String(), analysis.GetNodeKindDisplayLabel(graph.PrepareNode(graph.NewProperties(), ad.Entity, unsupportedKind)), "should return valid kind if one is preseneven if an unsupported kind is also present") assert.Equal(analysis.NodeKindUnknown, analysis.GetNodeKindDisplayLabel(graph.PrepareNode(graph.NewProperties())), "should return Unknown if no node has no kinds on it") diff --git a/packages/go/analysis/azure/azure.go b/packages/go/analysis/azure/azure.go index 35881ed687..13a0d9c8de 100644 --- a/packages/go/analysis/azure/azure.go +++ b/packages/go/analysis/azure/azure.go @@ -34,6 +34,7 @@ func GetDescendentKinds(kind graph.Kind) []graph.Kind { return []graph.Kind{ azure.User, azure.Group, + azure.Group365, azure.ManagementGroup, azure.Subscription, azure.ResourceGroup, diff --git a/packages/go/analysis/azure/group365.go b/packages/go/analysis/azure/group365.go new file mode 100644 index 0000000000..5d7e6c803e --- /dev/null +++ b/packages/go/analysis/azure/group365.go @@ -0,0 +1,62 @@ +// Copyright 2025 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 + +package azure + +import ( + "context" + + "github.com/specterops/bloodhound/dawgs/graph" +) + +func NewGroup365EntityDetails(node *graph.Node) Group365Details { + + return Group365Details{ + Node: FromGraphNode(node), + } +} + +func Group365EntityDetails(db graph.Database, objectID string, hydrateCounts bool) (Group365Details, error) { + + var details Group365Details + return details, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + + if node, err := FetchEntityByObjectID(tx, objectID); err != nil { + return err + + } else { + details = NewGroup365EntityDetails(node) + + if hydrateCounts { + details, err = PopulateGroup365EntityDetailsCounts(tx, node, details) + } + + return err + } + }) +} + +func PopulateGroup365EntityDetailsCounts(tx graph.Transaction, node *graph.Node, details Group365Details) (Group365Details, error) { + + if inboundObjectControl, err := FetchInboundEntityObjectControllers(tx, node, 0, 0); err != nil { + return details, err + + } else { + details.InboundObjectControl = inboundObjectControl.Len() + } + + return details, nil +} diff --git a/packages/go/analysis/azure/model.go b/packages/go/analysis/azure/model.go index 5a18517dce..96364d45a2 100644 --- a/packages/go/analysis/azure/model.go +++ b/packages/go/analysis/azure/model.go @@ -116,6 +116,11 @@ type GroupDetails struct { InboundObjectControl int `json:"inbound_object_control"` } +type Group365Details struct { + Node + InboundObjectControl int `json:"inbound_object_control"` +} + type TenantDetails struct { Node diff --git a/packages/go/ein/azure.go b/packages/go/ein/azure.go index 18250274bc..b1d3113e47 100644 --- a/packages/go/ein/azure.go +++ b/packages/go/ein/azure.go @@ -479,6 +479,82 @@ func ConvertAzureGroupMembersToRels(data models.GroupMembers) []IngestibleRelati return relationships } +func ConvertAzureGroup365ToNode(data models.Group365) IngestibleNode { + + return IngestibleNode{ + ObjectID: strings.ToUpper(data.Id), + PropertyMap: map[string]any{ + common.Name.String(): strings.ToUpper(fmt.Sprintf("%s@%s", data.DisplayName, data.TenantName)), + common.WhenCreated.String(): ParseISO8601(data.CreatedDateTime), + common.Description.String(): data.Description, + common.DisplayName.String(): data.DisplayName, + azure.IsAssignableToRole.String(): data.IsAssignableToRole, + azure.OnPremID.String(): data.OnPremisesSecurityIdentifier, + azure.OnPremSyncEnabled.String(): data.OnPremisesSyncEnabled, + azure.SecurityEnabled.String(): data.SecurityEnabled, + azure.SecurityIdentifier.String(): data.SecurityIdentifier, + azure.Visibility.String(): data.Visibility, + azure.TenantID.String(): strings.ToUpper(data.TenantId), + azure.Mail.String(): data.Mail, + }, + Label: azure.Group365, + } +} + +func ConvertAzureGroup365ToRel(data models.Group365) IngestibleRelationship { + + return NewIngestibleRelationship( + IngestibleSource{ + Source: strings.ToUpper(data.TenantId), + SourceType: azure.Tenant, + }, + + IngestibleTarget{ + TargetType: azure.Group365, + Target: strings.ToUpper(data.Id), + }, + + IngestibleRel{ + RelProps: map[string]any{}, + RelType: azure.Contains, + }, + ) + +} + +func ConvertAzureGroup365MembersToRels(data models.Group365Members) []IngestibleRelationship { + relationships := make([]IngestibleRelationship, 0) + + for _, raw := range data.Members { + var ( + member azure2.DirectoryObject + ) + if err := json.Unmarshal(raw.Member, &member); err != nil { + slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 365 group member", err)) + } else if memberType, err := ExtractTypeFromDirectoryObject(member); errors.Is(err, ErrInvalidType) { + slog.Warn(fmt.Sprintf(ExtractError, err)) + } else if err != nil { + slog.Error(fmt.Sprintf(ExtractError, err)) + } else { + relationships = append(relationships, NewIngestibleRelationship( + IngestibleSource{ + Source: strings.ToUpper(member.Id), + SourceType: memberType, + }, + IngestibleTarget{ + TargetType: azure.Group365, + Target: strings.ToUpper(data.GroupId), + }, + IngestibleRel{ + RelProps: map[string]any{}, + RelType: azure.MemberOf, + }, + )) + } + } + return relationships +} + func ConvertAzureGroupOwnerToRels(data models.GroupOwners) []IngestibleRelationship { relationships := make([]IngestibleRelationship, 0) @@ -513,6 +589,40 @@ func ConvertAzureGroupOwnerToRels(data models.GroupOwners) []IngestibleRelations return relationships } +func ConvertAzureGroup365OwnerToRels(data models.Group365Owners) []IngestibleRelationship { + relationships := make([]IngestibleRelationship, 0) + + for _, raw := range data.Owners { + var ( + owner azure2.DirectoryObject + ) + if err := json.Unmarshal(raw.Owner, &owner); err != nil { + slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 365 group owner", err)) + } else if ownerType, err := ExtractTypeFromDirectoryObject(owner); errors.Is(err, ErrInvalidType) { + slog.Warn(fmt.Sprintf(ExtractError, err)) + } else if err != nil { + slog.Error(fmt.Sprintf(ExtractError, err)) + } else { + relationships = append(relationships, NewIngestibleRelationship( + IngestibleSource{ + Source: strings.ToUpper(owner.Id), + SourceType: ownerType, + }, + IngestibleTarget{ + TargetType: azure.Group365, + Target: strings.ToUpper(data.GroupId), + }, + IngestibleRel{ + RelProps: map[string]any{}, + RelType: azure.Owns, + }, + )) + } + } + + return relationships +} + func ConvertAzureKeyVault(data models.KeyVault) (IngestibleNode, IngestibleRelationship) { return IngestibleNode{ ObjectID: strings.ToUpper(data.Id), diff --git a/packages/go/ein/incoming_models.go b/packages/go/ein/incoming_models.go index 2fa883504f..7ad8668caa 100644 --- a/packages/go/ein/incoming_models.go +++ b/packages/go/ein/incoming_models.go @@ -210,6 +210,11 @@ type Group struct { Members []TypedPrincipal } +type Group365 struct { + IngestBase + Members []TypedPrincipal +} + type User struct { IngestBase AllowedToDelegate []TypedPrincipal diff --git a/packages/go/graphschema/azure/azure.go b/packages/go/graphschema/azure/azure.go index c0e8651a4b..5cac4b439e 100644 --- a/packages/go/graphschema/azure/azure.go +++ b/packages/go/graphschema/azure/azure.go @@ -32,6 +32,7 @@ var ( Device = graph.StringKind("AZDevice") FunctionApp = graph.StringKind("AZFunctionApp") Group = graph.StringKind("AZGroup") + Group365 = graph.StringKind("AZGroup365") KeyVault = graph.StringKind("AZKeyVault") ManagementGroup = graph.StringKind("AZManagementGroup") ResourceGroup = graph.StringKind("AZResourceGroup") @@ -129,10 +130,12 @@ const ( PublisherDomain Property = "publisherdomain" SignInAudience Property = "signinaudience" RoleTemplateID Property = "templateid" + Visibility Property = "visibility" + Mail Property = "mail" ) func AllProperties() []Property { - return []Property{AppOwnerOrganizationID, AppDescription, AppDisplayName, ServicePrincipalType, UserType, TenantID, ServicePrincipalID, ServicePrincipalNames, OperatingSystemVersion, TrustType, IsBuiltIn, AppID, AppRoleID, DeviceID, NodeResourceGroupID, OnPremID, OnPremSyncEnabled, SecurityEnabled, SecurityIdentifier, EnableRBACAuthorization, Scope, Offer, MFAEnabled, License, Licenses, LoginURL, MFAEnforced, UserPrincipalName, IsAssignableToRole, PublisherDomain, SignInAudience, RoleTemplateID} + return []Property{AppOwnerOrganizationID, AppDescription, AppDisplayName, ServicePrincipalType, UserType, TenantID, ServicePrincipalID, ServicePrincipalNames, OperatingSystemVersion, TrustType, IsBuiltIn, AppID, AppRoleID, DeviceID, NodeResourceGroupID, OnPremID, OnPremSyncEnabled, SecurityEnabled, SecurityIdentifier, EnableRBACAuthorization, Scope, Offer, MFAEnabled, License, Licenses, LoginURL, MFAEnforced, UserPrincipalName, IsAssignableToRole, PublisherDomain, SignInAudience, RoleTemplateID, Visibility, Mail} } func ParseProperty(source string) (Property, error) { switch source { @@ -200,6 +203,10 @@ func ParseProperty(source string) (Property, error) { return SignInAudience, nil case "templateid": return RoleTemplateID, nil + case "visibility": + return Visibility, nil + case "mail": + return Mail, nil default: return "", errors.New("Invalid enumeration value: " + source) } @@ -270,6 +277,10 @@ func (s Property) String() string { return string(SignInAudience) case RoleTemplateID: return string(RoleTemplateID) + case Visibility: + return string(Visibility) + case Mail: + return string(Mail) default: return "Invalid enumeration case: " + string(s) } @@ -340,6 +351,10 @@ func (s Property) Name() string { return "Sign In Audience" case RoleTemplateID: return "Role Template ID" + case Visibility: + return "Visibility" + case Mail: + return "M365 Group Mail" default: return "Invalid enumeration case: " + string(s) } @@ -371,5 +386,5 @@ func PathfindingRelationships() []graph.Kind { return []graph.Kind{AvereContributor, Contributor, GetCertificates, GetKeys, GetSecrets, HasRole, MemberOf, Owner, RunsAs, VMContributor, AutomationContributor, KeyVaultContributor, VMAdminLogin, AddMembers, AddSecret, ExecuteCommand, GlobalAdmin, PrivilegedAuthAdmin, Grant, GrantSelf, PrivilegedRoleAdmin, ResetPassword, UserAccessAdministrator, Owns, CloudAppAdmin, AppAdmin, AddOwner, ManagedIdentity, AKSContributor, NodeResourceGroup, WebsiteContributor, LogicAppContributor, AZMGAddMember, AZMGAddOwner, AZMGAddSecret, AZMGGrantAppRoles, AZMGGrantRole, SyncedToADUser, Contains} } func NodeKinds() []graph.Kind { - return []graph.Kind{Entity, VMScaleSet, App, Role, Device, FunctionApp, Group, KeyVault, ManagementGroup, ResourceGroup, ServicePrincipal, Subscription, Tenant, User, VM, ManagedCluster, ContainerRegistry, WebApp, LogicApp, AutomationAccount} + return []graph.Kind{Entity, VMScaleSet, App, Role, Device, FunctionApp, Group, Group365, KeyVault, ManagementGroup, ResourceGroup, ServicePrincipal, Subscription, Tenant, User, VM, ManagedCluster, ContainerRegistry, WebApp, LogicApp, AutomationAccount} } diff --git a/packages/go/schemagen/go.sum b/packages/go/schemagen/go.sum index 76b7e82947..bf503cb08f 100644 --- a/packages/go/schemagen/go.sum +++ b/packages/go/schemagen/go.sum @@ -1,14 +1,21 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY= +cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= +cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk= github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= +github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -23,19 +30,33 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/protocolbuffers/txtpbfmt v0.0.0-20240823084532-8e6b51fa9bef h1:ej+64jiny5VETZTqcc1GFVAPEtaSk6U1D0kKC2MS5Yc= +github.com/protocolbuffers/txtpbfmt v0.0.0-20240823084532-8e6b51fa9bef/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 7f72d67ab1..836d5a6ee8 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -753,6 +753,7 @@ export enum AzureNodeKind { Device = 'AZDevice', FunctionApp = 'AZFunctionApp', Group = 'AZGroup', + Group365 = 'AZGroup365', KeyVault = 'AZKeyVault', ManagementGroup = 'AZManagementGroup', ResourceGroup = 'AZResourceGroup', @@ -783,6 +784,8 @@ export function AzureNodeKindToDisplay(value: AzureNodeKind): string | undefined return 'FunctionApp'; case AzureNodeKind.Group: return 'Group'; + case AzureNodeKind.Group365: + return 'Group365'; case AzureNodeKind.KeyVault: return 'KeyVault'; case AzureNodeKind.ManagementGroup: @@ -996,6 +999,8 @@ export enum AzureKindProperties { PublisherDomain = 'publisherdomain', SignInAudience = 'signinaudience', RoleTemplateID = 'templateid', + Visibility = 'visibility', + Mail = 'mail', } export function AzureKindPropertiesToDisplay(value: AzureKindProperties): string | undefined { switch (value) { @@ -1063,6 +1068,10 @@ export function AzureKindPropertiesToDisplay(value: AzureKindProperties): string return 'Sign In Audience'; case AzureKindProperties.RoleTemplateID: return 'Role Template ID'; + case AzureKindProperties.Visibility: + return 'Visibility'; + case AzureKindProperties.Mail: + return 'M365 Group Mail'; default: return undefined; } @@ -1179,4 +1188,4 @@ export function CommonKindPropertiesToDisplay(value: CommonKindProperties): stri default: return undefined; } -} +} \ No newline at end of file diff --git a/packages/javascript/bh-shared-ui/src/utils/content.ts b/packages/javascript/bh-shared-ui/src/utils/content.ts index 693883054a..44be6182bd 100644 --- a/packages/javascript/bh-shared-ui/src/utils/content.ts +++ b/packages/javascript/bh-shared-ui/src/utils/content.ts @@ -59,6 +59,8 @@ export const entityInformationEndpoints: Record apiClient.getAZEntityInfoV2('groups', id, undefined, false, undefined, undefined, undefined, options), + [AzureNodeKind.Group365]: (id: string, options?: RequestOptions) => + apiClient.getAZEntityInfoV2('groups365', id, undefined, false, undefined, undefined, undefined, options), [AzureNodeKind.KeyVault]: (id: string, options?: RequestOptions) => apiClient.getAZEntityInfoV2('key-vaults', id, undefined, false, undefined, undefined, undefined, options), [AzureNodeKind.ManagementGroup]: (id: string, options?: RequestOptions) => @@ -227,6 +229,33 @@ export const allSections: Partial EntityInfo queryType: 'azgroup-outbound_object_control', }, ], + [AzureNodeKind.Group365]: (id: string) => [ + { + id, + label: 'Members', + queryType: 'azgroup365-members', + }, + { + id, + label: 'Member Of', + queryType: 'azgroup365-member_of', + }, + { + id, + label: 'Roles', + queryType: 'azgroup365-roles', + }, + { + id, + label: 'Inbound Object Control', + queryType: 'azgroup365-inbound_object_control', + }, + { + id, + label: 'Outbound Object Control', + queryType: 'azgroup365-outbound_object_control', + }, + ], [AzureNodeKind.KeyVault]: (id: string) => [ { id, @@ -1102,6 +1131,33 @@ export const entityRelationshipEndpoints = { signal: controller.signal, }) .then((res) => res.data), + 'azgroup365-members': ({ id, counts, skip, limit, type }) => + apiClient + .getAZEntityInfoV2('groups365', id, 'group-members', counts, skip, limit, type, { signal: controller.signal }) + .then((res : any) => res.data), + 'azgroup365-member_of': ({ id, counts, skip, limit, type }) => + apiClient + .getAZEntityInfoV2('groups365', id, 'group-membership', counts, skip, limit, type, { + signal: controller.signal, + }) + .then((res : any) => res.data), + 'azgroup365-roles': ({ id, counts, skip, limit, type }) => + apiClient + .getAZEntityInfoV2('groups365', id, 'roles', counts, skip, limit, type, { signal: controller.signal }) + .then((res : any) => res.data), + + 'azgroup365-inbound_object_control': ({ id, counts, skip, limit, type }) => + apiClient + .getAZEntityInfoV2('groups365', id, 'inbound-control', counts, skip, limit, type, { + signal: controller.signal, + }) + .then((res : any) => res.data), + 'azgroup365-outbound_object_control': ({ id, counts, skip, limit, type }) => + apiClient + .getAZEntityInfoV2('groups365', id, 'outbound-control', counts, skip, limit, type, { + signal: controller.signal, + }) + .then((res: any) => res.data), 'azkeyvault-key_readers': ({ id, counts, skip, limit, type }) => apiClient .getAZEntityInfoV2('key-vaults', id, 'key-readers', counts, skip, limit, type, { diff --git a/packages/javascript/bh-shared-ui/src/utils/icons.ts b/packages/javascript/bh-shared-ui/src/utils/icons.ts index 546a80d399..4b3653a413 100644 --- a/packages/javascript/bh-shared-ui/src/utils/icons.ts +++ b/packages/javascript/bh-shared-ui/src/utils/icons.ts @@ -151,6 +151,11 @@ export const NODE_ICON: IconDictionary = { color: '#F57C9B', }, + [AzureNodeKind.Group365]: { + icon: faUsers, + color: '#34D2EB', + }, + [AzureNodeKind.Tenant]: { icon: faCloud, color: '#54F2F2', diff --git a/packages/javascript/bh-shared-ui/src/views/DataQuality/TenantInfo.tsx b/packages/javascript/bh-shared-ui/src/views/DataQuality/TenantInfo.tsx index e1a3fa6d5e..ced463aef5 100644 --- a/packages/javascript/bh-shared-ui/src/views/DataQuality/TenantInfo.tsx +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/TenantInfo.tsx @@ -39,6 +39,7 @@ const useStyles = makeStyles((theme) => ({ export const TenantMap = { users: { displayText: 'Users', kind: AzureNodeKind.User }, groups: { displayText: 'Groups', kind: AzureNodeKind.Group }, + groups365: { displayText: 'Microsoft 365', kind: AzureNodeKind.Group365 }, apps: { displayText: 'Apps', kind: AzureNodeKind.App }, service_principals: { displayText: 'Service Principals', diff --git a/packages/javascript/js-client-library/package-lock.json b/packages/javascript/js-client-library/package-lock.json new file mode 100644 index 0000000000..916445095b --- /dev/null +++ b/packages/javascript/js-client-library/package-lock.json @@ -0,0 +1,1977 @@ +{ + "name": "js-client-library", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "js-client-library", + "dependencies": { + "axios": "^1.8.2" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.1.6", + "@typescript-eslint/eslint-plugin": "^7.3.0", + "@typescript-eslint/parser": "^7.3.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.2.5", + "prettier-plugin-organize-imports": "^4.1.0", + "rollup": "^4.13.2", + "rollup-plugin-delete": "^2.0.0", + "typescript": "^5.1.6" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.13.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.11.29", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.3.0", + "@typescript-eslint/type-utils": "7.3.0", + "@typescript-eslint/utils": "7.3.0", + "@typescript-eslint/visitor-keys": "7.3.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.3.0", + "@typescript-eslint/types": "7.3.0", + "@typescript-eslint/typescript-estree": "7.3.0", + "@typescript-eslint/visitor-keys": "7.3.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.3.0", + "@typescript-eslint/visitor-keys": "7.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.3.0", + "@typescript-eslint/utils": "7.3.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.3.0", + "@typescript-eslint/visitor-keys": "7.3.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.3.0", + "@typescript-eslint/types": "7.3.0", + "@typescript-eslint/typescript-estree": "7.3.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.3.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.11.3", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.2", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/del": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^10.0.1", + "graceful-fs": "^4.2.2", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.1", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.15.0", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "10.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.12.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.13.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.13.2", + "@rollup/rollup-android-arm64": "4.13.2", + "@rollup/rollup-darwin-arm64": "4.13.2", + "@rollup/rollup-darwin-x64": "4.13.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.2", + "@rollup/rollup-linux-arm64-gnu": "4.13.2", + "@rollup/rollup-linux-arm64-musl": "4.13.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.13.2", + "@rollup/rollup-linux-riscv64-gnu": "4.13.2", + "@rollup/rollup-linux-s390x-gnu": "4.13.2", + "@rollup/rollup-linux-x64-gnu": "4.13.2", + "@rollup/rollup-linux-x64-musl": "4.13.2", + "@rollup/rollup-win32-arm64-msvc": "4.13.2", + "@rollup/rollup-win32-ia32-msvc": "4.13.2", + "@rollup/rollup-win32-x64-msvc": "4.13.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-delete": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "del": "^5.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +}