From 539840adef88ebffd435c7f89c2632f73b2fd7dd Mon Sep 17 00:00:00 2001 From: svozza Date: Tue, 26 Nov 2024 19:11:18 +0000 Subject: [PATCH] add missing js files from backend --- .prettierignore | 8 + .prettierrc | 7 + source/backend/discovery/src/index.mjs | 43 + .../addBatchedRelationships.mjs | 145 + .../addIndividualRelationships.mjs | 604 ++ ...createEnvironmentVariableRelationships.mjs | 38 + .../createLookUpMaps.mjs | 114 + .../src/lib/additionalRelationships/index.mjs | 124 + .../lib/aggregator/getAllConfigResources.mjs | 96 + .../discovery/src/lib/apiClient/appSync.mjs | 341 + .../discovery/src/lib/apiClient/index.mjs | 245 + .../backend/discovery/src/lib/awsClient.mjs | 771 ++ source/backend/discovery/src/lib/config.mjs | 15 + .../backend/discovery/src/lib/constants.mjs | 164 + .../createResourceAndRelationshipDeltas.mjs | 180 + source/backend/discovery/src/lib/errors.mjs | 26 + source/backend/discovery/src/lib/index.mjs | 51 + .../discovery/src/lib/intialisation.mjs | 72 + source/backend/discovery/src/lib/logger.mjs | 21 + .../discovery/src/lib/persistence/index.mjs | 74 + .../src/lib/persistence/transformers.mjs | 260 + .../sdkResources/createAllBatchResources.mjs | 211 + .../lib/sdkResources/firstOrderHandlers.mjs | 239 + .../discovery/src/lib/sdkResources/index.mjs | 121 + .../lib/sdkResources/secondOrderHandlers.mjs | 64 + source/backend/discovery/src/lib/utils.mjs | 240 + .../AWS::AppSync::DataSource.json | 40 + .../AWS::AutoScaling::AutoScalingGroup.json | 14 + .../AWS::AutoScaling::WarmPool.json | 14 + .../AWS::CodeBuild::Project.json | 25 + .../resourceTypes/AWS::DynamoDB::Table.json | 13 + .../resourceTypes/AWS::EC2::Instance.json | 13 + .../resourceTypes/AWS::EC2::SpotFleet.json | 19 + .../AWS::EC2::TransitGateway.json | 20 + .../resourceTypes/AWS::ECS::Cluster.json | 25 + .../resourceTypes/AWS::ECS::Service.json | 40 + .../AWS::ECS::TaskDefinition.json | 30 + .../resourceTypes/AWS::EFS::AccessPoint.json | 14 + .../resourceTypes/AWS::EFS::FileSystem.json | 13 + .../resourceTypes/AWS::EKS::Cluster.json | 31 + .../resourceTypes/AWS::EKS::Nodegroup.json | 37 + ...S::ElasticLoadBalancing::LoadBalancer.json | 19 + .../resourceTypes/AWS::Events::Rule.json | 18 + .../AWS::IAM::InstanceProfile.json | 14 + .../resourceTypes/AWS::Lambda::Function.json | 23 + .../resourceTypes/AWS::MSK::Cluster.json | 20 + .../AWS::MediaConnect::FlowEntitlement.json | 13 + .../AWS::MediaConnect::FlowSource.json | 34 + .../AWS::MediaConnect::FlowVpcInterface.json | 26 + ...:MediaPackage::PackagingConfiguration.json | 19 + .../AWS::MediaPackage::PackagingGroup.json | 13 + .../AWS::OpenSearch::Domain.json | 26 + .../resourceTypes/AWS::S3::Bucket.json | 20 + .../backend/discovery/src/schemas/schema.json | 114 + .../test/additionalRelationships.test.mjs | 2967 ++++++++ .../discovery/test/apiClient/index.test.mjs | 617 ++ .../backend/discovery/test/awsClient.test.mjs | 1989 +++++ ...eateResourceAndRelationshipDeltas.test.mjs | 340 + .../appregistry/application.json | 10 + .../appsync/graphQlApi.json | 35 + .../mediaconnect/flows.json | 12 + .../appregistry/application/default.json | 48 + .../application/noApplicationTag.json | 18 + .../relationships/appsync/graphQlApi.json | 143 + .../asg/warmPool/configuration.json | 25 + .../ec2/instance/configuration.json | 24 + .../ecs/cluster/configuration.json | 20 + .../ecs/taskDefinitions/efs.json | 38 + .../iam/instanceProfile/mutipleRoles.json | 25 + .../relationships/lambda/configuration.json | 19 + .../mediaconnect/entitlement/flow.json | 25 + .../flowVpcInterface/networking.json | 59 + .../mediaconnect/flowsource/encrypted.json | 35 + .../mediaconnect/flowsource/entitlement.json | 29 + .../mediaconnect/flowsource/vpc.json | 32 + .../packagingConfiguration/encryption.json | 107 + .../packagingConfiguration/group.json | 25 + .../packagingGroup/authorization.json | 31 + .../s3/bucket/supplementary.json | 34 + source/backend/discovery/test/generator.mjs | 78 + .../test/getAllConfigResources.test.mjs | 109 + .../test/getAllSdkResources.test.mjs | 1932 +++++ .../discovery/test/initialisation.test.mjs | 134 + .../test/mocks/agents/ConnectionClosed.mjs | 31 + .../DeleteIndexedResourcesPartialSuccess.mjs | 18 + .../test/mocks/agents/GenericError.mjs | 18 + .../mocks/agents/GetAccountsOrgsDeleted.mjs | 26 + .../mocks/agents/GetAccountsOrgsEmpty.mjs | 16 + .../agents/GetAccountsOrgsLastCrawled.mjs | 22 + .../mocks/agents/GetAccountsSelfManaged.mjs | 24 + .../GetDbRelationshipsMapPagination.mjs | 40 + .../agents/GetDbResourcesMapPagination.mjs | 32 + .../agents/IndexResourcesPartialSuccess.mjs | 18 + .../UpdateIndexedResourcesPartialSuccess.mjs | 18 + .../discovery/test/mocks/agents/utils.mjs | 26 + .../discovery/test/persistence/index.test.mjs | 147 + .../test/persistence/transformers.test.mjs | 250 + source/backend/discovery/test/utils.test.mjs | 89 + source/backend/discovery/vitest.config.mjs | 15 + .../src/index.mjs | 94 + .../test/index.test.mjs | 105 + .../vitest.config.mjs | 15 + .../cur-notification/src/cfn-response.mjs | 57 + .../functions/cur-notification/src/index.mjs | 57 + .../functions/cur-setup/src/cfn-response.mjs | 57 + .../backend/functions/cur-setup/src/index.mjs | 79 + .../backend/functions/graph-api/src/index.mjs | 297 + .../functions/graph-api/src/logger.mjs | 10 + .../functions/graph-api/test/index.test.mjs | 532 ++ .../functions/graph-api/vitest.config.mjs | 15 + .../functions/identity-provider/Pipfile | 19 + .../functions/identity-provider/Pipfile.lock | 658 ++ .../identity-provider/identity_provider.py | 106 + .../test_identity_provider.py | 229 + .../package-lock.json | 6669 +++++++++++++++++ .../metrics-subscription-filter/package.json | 34 + .../metrics-subscription-filter/src/index.mjs | 115 + .../test/contants.mjs | 10 + .../test/index.test.mjs | 182 + .../test/mocks/handlers.mjs | 17 + .../test/mocks/node.mjs | 7 + .../vitest.config.mjs | 15 + source/backend/functions/metrics-uuid/Pipfile | 19 + .../functions/metrics-uuid/Pipfile.lock | 658 ++ .../functions/metrics-uuid/metrics_uuid.py | 54 + .../metrics-uuid/test_metrics_uuid.py | 106 + .../functions/metrics/test/constants.ts | 10 + .../functions/metrics/test/mocks/handlers.ts | 65 + .../functions/metrics/test/mocks/node.ts | 7 + .../functions/metrics/vitest.config.ts | 15 + .../myapplications/package-lock.json | 6057 +++++++++++++++ .../functions/myapplications/package.json | 47 + .../functions/myapplications/src/index.mjs | 472 ++ .../functions/myapplications/src/types.d.ts | 104 + .../myapplications/test/index.test.mjs | 525 ++ .../functions/myapplications/tsconfig.json | 14 + .../myapplications/vitest.config.mjs | 15 + .../functions/search-api/src/index.mjs | 259 + .../functions/search-api/test/index.test.mjs | 255 + .../functions/search-api/vitest.config.mjs | 15 + .../backend/functions/settings/src/index.mjs | 839 +++ .../functions/settings/test/index.test.mjs | 3247 ++++++++ .../templates/application-insights.template | 38 + .../myapplications-resolvers.template | 147 + ...nagement-IAM_Instance_Profile_light-bg.svg | 15 + .../icons/Arch_AWS-AppSync_64-DataSource.svg | 12 + .../icons/Arch_AWS-AppSync_64-Resolver.svg | 12 + .../Arch_AWS-Elemental-MediaConnect_64.svg | 10 + .../Arch_AWS-Elemental-MediaTailor_64.svg | 10 + .../icons/Arch_AWS-Service-Catalog_64.svg | 10 + .../Draw/Canvas/Export/ExportDiagramModal.js | 378 + .../Diagrams/Draw/Utils/ResourceSearch.css | 12 + .../src/components/Hooks/useMyApplications.js | 55 + .../src/cytoscape/plugins/svg/exportToSvg.js | 110 + .../src/cytoscape/plugins/svg/index.js | 13 + .../src/cytoscape/plugins/svg/svgCanvas.js | 115 + .../DrawDiagram/DrawDiagramPageExport.cy.js | 659 ++ .../ExportToSvgTestDiagramCi.svg | 1 + .../ExportToSvgTestDiagramLocal.svg | 1 + .../JsonExportTestDiagramCi.json | 864 +++ .../JsonExportTestDiagramLocal.json | 864 +++ ... to be added to an existing diagram #0.png | Bin 0 -> 122240 bytes ... preview of diagram before creation #0.png | Bin 0 -> 132310 bytes .../fixtures/getResourceGraph/sqs-lambda.json | 180 + .../Canvas/Export/ExportDiagramModal.test.js | 668 ++ 165 files changed, 41067 insertions(+) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100755 source/backend/discovery/src/index.mjs create mode 100644 source/backend/discovery/src/lib/additionalRelationships/addBatchedRelationships.mjs create mode 100644 source/backend/discovery/src/lib/additionalRelationships/addIndividualRelationships.mjs create mode 100644 source/backend/discovery/src/lib/additionalRelationships/createEnvironmentVariableRelationships.mjs create mode 100644 source/backend/discovery/src/lib/additionalRelationships/createLookUpMaps.mjs create mode 100644 source/backend/discovery/src/lib/additionalRelationships/index.mjs create mode 100644 source/backend/discovery/src/lib/aggregator/getAllConfigResources.mjs create mode 100644 source/backend/discovery/src/lib/apiClient/appSync.mjs create mode 100644 source/backend/discovery/src/lib/apiClient/index.mjs create mode 100644 source/backend/discovery/src/lib/awsClient.mjs create mode 100644 source/backend/discovery/src/lib/config.mjs create mode 100644 source/backend/discovery/src/lib/constants.mjs create mode 100644 source/backend/discovery/src/lib/createResourceAndRelationshipDeltas.mjs create mode 100644 source/backend/discovery/src/lib/errors.mjs create mode 100644 source/backend/discovery/src/lib/index.mjs create mode 100644 source/backend/discovery/src/lib/intialisation.mjs create mode 100644 source/backend/discovery/src/lib/logger.mjs create mode 100644 source/backend/discovery/src/lib/persistence/index.mjs create mode 100644 source/backend/discovery/src/lib/persistence/transformers.mjs create mode 100644 source/backend/discovery/src/lib/sdkResources/createAllBatchResources.mjs create mode 100644 source/backend/discovery/src/lib/sdkResources/firstOrderHandlers.mjs create mode 100644 source/backend/discovery/src/lib/sdkResources/index.mjs create mode 100644 source/backend/discovery/src/lib/sdkResources/secondOrderHandlers.mjs create mode 100644 source/backend/discovery/src/lib/utils.mjs create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::AppSync::DataSource.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::AutoScalingGroup.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::WarmPool.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::CodeBuild::Project.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::DynamoDB::Table.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::Instance.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::SpotFleet.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::TransitGateway.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Cluster.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Service.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::TaskDefinition.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::AccessPoint.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::FileSystem.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Cluster.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Nodegroup.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::ElasticLoadBalancing::LoadBalancer.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::Events::Rule.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::IAM::InstanceProfile.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::Lambda::Function.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::MSK::Cluster.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowEntitlement.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowSource.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowVpcInterface.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingConfiguration.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingGroup.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::OpenSearch::Domain.json create mode 100644 source/backend/discovery/src/schemas/resourceTypes/AWS::S3::Bucket.json create mode 100644 source/backend/discovery/src/schemas/schema.json create mode 100644 source/backend/discovery/test/additionalRelationships.test.mjs create mode 100644 source/backend/discovery/test/apiClient/index.test.mjs create mode 100644 source/backend/discovery/test/awsClient.test.mjs create mode 100644 source/backend/discovery/test/createResourceAndRelationshipDeltas.test.mjs create mode 100644 source/backend/discovery/test/fixtures/additionalResources/appregistry/application.json create mode 100644 source/backend/discovery/test/fixtures/additionalResources/appsync/graphQlApi.json create mode 100644 source/backend/discovery/test/fixtures/additionalResources/mediaconnect/flows.json create mode 100644 source/backend/discovery/test/fixtures/relationships/appregistry/application/default.json create mode 100644 source/backend/discovery/test/fixtures/relationships/appregistry/application/noApplicationTag.json create mode 100644 source/backend/discovery/test/fixtures/relationships/appsync/graphQlApi.json create mode 100644 source/backend/discovery/test/fixtures/relationships/asg/warmPool/configuration.json create mode 100644 source/backend/discovery/test/fixtures/relationships/ec2/instance/configuration.json create mode 100644 source/backend/discovery/test/fixtures/relationships/ecs/cluster/configuration.json create mode 100644 source/backend/discovery/test/fixtures/relationships/ecs/taskDefinitions/efs.json create mode 100644 source/backend/discovery/test/fixtures/relationships/iam/instanceProfile/mutipleRoles.json create mode 100644 source/backend/discovery/test/fixtures/relationships/lambda/configuration.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediaconnect/entitlement/flow.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediaconnect/flowVpcInterface/networking.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/encrypted.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/entitlement.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/vpc.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/encryption.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/group.json create mode 100644 source/backend/discovery/test/fixtures/relationships/mediapackage/packagingGroup/authorization.json create mode 100644 source/backend/discovery/test/fixtures/relationships/s3/bucket/supplementary.json create mode 100644 source/backend/discovery/test/generator.mjs create mode 100644 source/backend/discovery/test/getAllConfigResources.test.mjs create mode 100644 source/backend/discovery/test/getAllSdkResources.test.mjs create mode 100644 source/backend/discovery/test/initialisation.test.mjs create mode 100644 source/backend/discovery/test/mocks/agents/ConnectionClosed.mjs create mode 100644 source/backend/discovery/test/mocks/agents/DeleteIndexedResourcesPartialSuccess.mjs create mode 100644 source/backend/discovery/test/mocks/agents/GenericError.mjs create mode 100644 source/backend/discovery/test/mocks/agents/GetAccountsOrgsDeleted.mjs create mode 100644 source/backend/discovery/test/mocks/agents/GetAccountsOrgsEmpty.mjs create mode 100644 source/backend/discovery/test/mocks/agents/GetAccountsOrgsLastCrawled.mjs create mode 100644 source/backend/discovery/test/mocks/agents/GetAccountsSelfManaged.mjs create mode 100644 source/backend/discovery/test/mocks/agents/GetDbRelationshipsMapPagination.mjs create mode 100644 source/backend/discovery/test/mocks/agents/GetDbResourcesMapPagination.mjs create mode 100644 source/backend/discovery/test/mocks/agents/IndexResourcesPartialSuccess.mjs create mode 100644 source/backend/discovery/test/mocks/agents/UpdateIndexedResourcesPartialSuccess.mjs create mode 100644 source/backend/discovery/test/mocks/agents/utils.mjs create mode 100644 source/backend/discovery/test/persistence/index.test.mjs create mode 100644 source/backend/discovery/test/persistence/transformers.test.mjs create mode 100644 source/backend/discovery/test/utils.test.mjs create mode 100644 source/backend/discovery/vitest.config.mjs create mode 100644 source/backend/functions/account-import-templates-api/src/index.mjs create mode 100644 source/backend/functions/account-import-templates-api/test/index.test.mjs create mode 100644 source/backend/functions/account-import-templates-api/vitest.config.mjs create mode 100644 source/backend/functions/cur-notification/src/cfn-response.mjs create mode 100644 source/backend/functions/cur-notification/src/index.mjs create mode 100644 source/backend/functions/cur-setup/src/cfn-response.mjs create mode 100644 source/backend/functions/cur-setup/src/index.mjs create mode 100644 source/backend/functions/graph-api/src/index.mjs create mode 100644 source/backend/functions/graph-api/src/logger.mjs create mode 100644 source/backend/functions/graph-api/test/index.test.mjs create mode 100644 source/backend/functions/graph-api/vitest.config.mjs create mode 100644 source/backend/functions/identity-provider/Pipfile create mode 100644 source/backend/functions/identity-provider/Pipfile.lock create mode 100644 source/backend/functions/identity-provider/identity_provider.py create mode 100644 source/backend/functions/identity-provider/test_identity_provider.py create mode 100644 source/backend/functions/metrics-subscription-filter/package-lock.json create mode 100644 source/backend/functions/metrics-subscription-filter/package.json create mode 100644 source/backend/functions/metrics-subscription-filter/src/index.mjs create mode 100644 source/backend/functions/metrics-subscription-filter/test/contants.mjs create mode 100644 source/backend/functions/metrics-subscription-filter/test/index.test.mjs create mode 100644 source/backend/functions/metrics-subscription-filter/test/mocks/handlers.mjs create mode 100644 source/backend/functions/metrics-subscription-filter/test/mocks/node.mjs create mode 100644 source/backend/functions/metrics-subscription-filter/vitest.config.mjs create mode 100644 source/backend/functions/metrics-uuid/Pipfile create mode 100644 source/backend/functions/metrics-uuid/Pipfile.lock create mode 100644 source/backend/functions/metrics-uuid/metrics_uuid.py create mode 100644 source/backend/functions/metrics-uuid/test_metrics_uuid.py create mode 100644 source/backend/functions/metrics/test/constants.ts create mode 100644 source/backend/functions/metrics/test/mocks/handlers.ts create mode 100644 source/backend/functions/metrics/test/mocks/node.ts create mode 100644 source/backend/functions/metrics/vitest.config.ts create mode 100644 source/backend/functions/myapplications/package-lock.json create mode 100644 source/backend/functions/myapplications/package.json create mode 100644 source/backend/functions/myapplications/src/index.mjs create mode 100644 source/backend/functions/myapplications/src/types.d.ts create mode 100644 source/backend/functions/myapplications/test/index.test.mjs create mode 100644 source/backend/functions/myapplications/tsconfig.json create mode 100644 source/backend/functions/myapplications/vitest.config.mjs create mode 100644 source/backend/functions/search-api/src/index.mjs create mode 100644 source/backend/functions/search-api/test/index.test.mjs create mode 100644 source/backend/functions/search-api/vitest.config.mjs create mode 100644 source/backend/functions/settings/src/index.mjs create mode 100644 source/backend/functions/settings/test/index.test.mjs create mode 100644 source/cfn/templates/application-insights.template create mode 100644 source/cfn/templates/myapplications-resolvers.template create mode 100644 source/frontend/public/icons/AWS-Identity-and-Access-Management-IAM_Instance_Profile_light-bg.svg create mode 100644 source/frontend/public/icons/Arch_AWS-AppSync_64-DataSource.svg create mode 100644 source/frontend/public/icons/Arch_AWS-AppSync_64-Resolver.svg create mode 100644 source/frontend/public/icons/Arch_AWS-Elemental-MediaConnect_64.svg create mode 100644 source/frontend/public/icons/Arch_AWS-Elemental-MediaTailor_64.svg create mode 100644 source/frontend/public/icons/Arch_AWS-Service-Catalog_64.svg create mode 100644 source/frontend/src/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.js create mode 100644 source/frontend/src/components/Diagrams/Draw/Utils/ResourceSearch.css create mode 100644 source/frontend/src/components/Hooks/useMyApplications.js create mode 100644 source/frontend/src/cytoscape/plugins/svg/exportToSvg.js create mode 100644 source/frontend/src/cytoscape/plugins/svg/index.js create mode 100644 source/frontend/src/cytoscape/plugins/svg/svgCanvas.js create mode 100644 source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/DrawDiagramPageExport.cy.js create mode 100644 source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramCi.svg create mode 100644 source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramLocal.svg create mode 100644 source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramCi.json create mode 100644 source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramLocal.json create mode 100644 source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__image_snapshots__/Diagrams Page should allow resources to be added to an existing diagram #0.png create mode 100644 source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__image_snapshots__/Diagrams Page shows preview of diagram before creation #0.png create mode 100644 source/frontend/src/tests/mocks/fixtures/getResourceGraph/sqs-lambda.json create mode 100644 source/frontend/src/tests/vitest/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.test.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..05ac381a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +package.json +package-lock.json +tsconfig.json +Pipfile.lock + +# Frontend +source/frontend/public +source/frontend/src/tests/cypress/**/__file_snapshots__/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..a3cd592b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "singleQuote": true, + "bracketSpacing": false, + "arrowParens": "avoid" +} diff --git a/source/backend/discovery/src/index.mjs b/source/backend/discovery/src/index.mjs new file mode 100755 index 00000000..f9e40d38 --- /dev/null +++ b/source/backend/discovery/src/index.mjs @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import logger from './lib/logger.mjs'; +import * as config from './lib/config.mjs'; +import {DISCOVERY_PROCESS_RUNNING, AWS_ORGANIZATIONS} from './lib/constants.mjs'; +import {createAwsClient} from './lib/awsClient.mjs'; +import appSync from './lib/apiClient/appSync.mjs'; +import {discoverResources} from './lib/index.mjs'; +import {AggregatorNotFoundError, OrgAggregatorValidationError} from './lib/errors.mjs'; + +const awsClient = createAwsClient(); + +const discover = async () => { + logger.profile('Discovery of resources complete.'); + + await discoverResources(appSync, awsClient, config) + .catch(err => { + if([DISCOVERY_PROCESS_RUNNING].includes(err.message)) { + logger.info(err.message); + } else { + throw err; + } + }); + + logger.profile('Discovery of resources complete.'); +}; + +discover().catch(err => { + if(err instanceof AggregatorNotFoundError) { + logger.error(`${err.message}. Ensure the name of the supplied aggregator is correct.`); + } else if(err instanceof OrgAggregatorValidationError) { + logger.error(`${err.message}. You cannot use an individual accounts aggregator when cross account discovery is set to ${AWS_ORGANIZATIONS}.`, { + aggregator: err.aggregator + }); + } else { + logger.error('Unexpected error in Discovery process.', { + msg: err.message, + stack: err.stack + }); + } + process.exit(1); +}); diff --git a/source/backend/discovery/src/lib/additionalRelationships/addBatchedRelationships.mjs b/source/backend/discovery/src/lib/additionalRelationships/addBatchedRelationships.mjs new file mode 100644 index 00000000..42784d52 --- /dev/null +++ b/source/backend/discovery/src/lib/additionalRelationships/addBatchedRelationships.mjs @@ -0,0 +1,145 @@ +import * as R from 'ramda'; +import createEnvironmentVariableRelationships from './createEnvironmentVariableRelationships.mjs'; +import logger from '../logger.mjs'; +import { + safeForEach, + createAssociatedRelationship, + createArn, + createAttachedRelationship +} from '../utils.mjs'; +import { + VPC, + EC2, + TRANSIT_GATEWAY_ATTACHMENT, + AWS_EC2_TRANSIT_GATEWAY, + AWS_EC2_VPC, + AWS_EC2_SUBNET, + FULFILLED, + SUBNET +} from '../constants.mjs'; + +function createBatchedHandlers(lookUpMaps, awsClient) { + const { + envVarResourceIdentifierToIdMap, + endpointToIdMap, + resourceMap + } = lookUpMaps; + + return { + eventSources: async (credentials, accountId, region) => { + const lambdaClient = awsClient.createLambdaClient(credentials, region); + const eventSourceMappings = await lambdaClient.listEventSourceMappings(); + + return safeForEach(({EventSourceArn, FunctionArn}) => { + if(resourceMap.has(EventSourceArn) && resourceMap.has(FunctionArn)) { + const {resourceType} = resourceMap.get(EventSourceArn); + const lambda = resourceMap.get(FunctionArn); + + lambda.relationships.push(createAssociatedRelationship(resourceType, { + arn: EventSourceArn + })); + } + }, eventSourceMappings); + }, + functions: async (credentials, accountId, region) => { + const lambdaClient = awsClient.createLambdaClient(credentials, region); + + const lambdas = await lambdaClient.getAllFunctions(); + + return safeForEach(({FunctionArn, Environment}) => { + const lambda = resourceMap.get(FunctionArn); + // Environment can be null (not undefined) which means default function parameters can't be used + const environment = Environment ?? {}; + // a lambda may have been created between the time we got the data from config + // and made our api request + if(lambda != null && !R.isEmpty(environment)) { + // The lambda API returns an error object if there are encrypted environment variables + // that the discovery process does not have permissions to decrypt + if(R.isNil(environment.Error)) { + //TODO: add env var name as a property of the edge + lambda.relationships.push(...createEnvironmentVariableRelationships( + {resourceMap, envVarResourceIdentifierToIdMap, endpointToIdMap}, + {accountId, awsRegion: region}, + environment.Variables)); + } + } + }, lambdas); + }, + snsSubscriptions: async (credentials, accountId, region) => { + const snsClient = awsClient.createSnsClient(credentials, region); + + const subscriptions = await snsClient.getAllSubscriptions(); + + return safeForEach(({Endpoint, TopicArn}) => { + // an SNS topic may have been created between the time we got the data from config + // and made our api request or the endpoint may have been created in a region that + // has not been imported + if(resourceMap.has(TopicArn) && resourceMap.has(Endpoint)) { + const snsTopic = resourceMap.get(TopicArn); + const {resourceType} = resourceMap.get(Endpoint); + snsTopic.relationships.push(createAssociatedRelationship(resourceType, {arn: Endpoint})); + } + }, subscriptions); + }, + transitGatewayVpcAttachments: async (credentials, accountId, region) => { + // Whilst AWS Config supports the AWS::EC2::TransitGatewayAttachment resource type, + // it is missing information on the account that VPCs referred to by the attachment + // are deployed in. Therefore we need to supplement this with info from the EC2 API. + const ec2Client = awsClient.createEc2Client(credentials, region); + + const tgwAttachments = await ec2Client.getAllTransitGatewayAttachments([ + {Name: 'resource-type', Values: [VPC.toLowerCase()]} + ]); + + return safeForEach(tgwAttachment => { + const { + TransitGatewayAttachmentId, ResourceOwnerId, TransitGatewayOwnerId, TransitGatewayId + } = tgwAttachment; + const tgwAttachmentArn = createArn({ + service: EC2, region, accountId, resource: `${TRANSIT_GATEWAY_ATTACHMENT}/${TransitGatewayAttachmentId}`} + ); + + if(resourceMap.has(tgwAttachmentArn)) { + const tgwAttachmentFromConfig = resourceMap.get(tgwAttachmentArn); + const {relationships, configuration: {SubnetIds, VpcId}} = tgwAttachmentFromConfig; + + relationships.push( + createAttachedRelationship(AWS_EC2_TRANSIT_GATEWAY, {accountId: TransitGatewayOwnerId, awsRegion: region, resourceId: TransitGatewayId}), + createAssociatedRelationship(AWS_EC2_VPC, {relNameSuffix: VPC, accountId: ResourceOwnerId, awsRegion: region, resourceId: VpcId}), + ...SubnetIds.map(subnetId => createAssociatedRelationship(AWS_EC2_SUBNET, {relNameSuffix: SUBNET, accountId: ResourceOwnerId, awsRegion: region, resourceId: subnetId})) + ); + } + }, tgwAttachments); + } + } +} + +function logErrors(results) { + const errors = results.flatMap(({status, value, reason}) => { + if(status === FULFILLED) { + return value.errors; + } else { + return [{error: reason}] + } + }); + + logger.error(`There were ${errors.length} errors when adding batch additional relationships.`); + logger.debug('Errors: ', {errors: errors}); +} + +async function addBatchedRelationships(lookUpMaps, awsClient) { + const credentialsTuples = Array.from(lookUpMaps.accountsMap.entries()); + + const batchedHandlers = createBatchedHandlers(lookUpMaps, awsClient); + + const results = await Promise.allSettled(Object.values(batchedHandlers).flatMap(handler => { + return credentialsTuples + .flatMap( ([accountId, {regions, credentials}]) => + regions.map(region => handler(credentials, accountId, region)) + ); + })); + + logErrors(results); +} + +export default addBatchedRelationships; \ No newline at end of file diff --git a/source/backend/discovery/src/lib/additionalRelationships/addIndividualRelationships.mjs b/source/backend/discovery/src/lib/additionalRelationships/addIndividualRelationships.mjs new file mode 100644 index 00000000..d91300b7 --- /dev/null +++ b/source/backend/discovery/src/lib/additionalRelationships/addIndividualRelationships.mjs @@ -0,0 +1,604 @@ +import Ajv from 'ajv' +import fs from 'node:fs/promises'; +import {PromisePool} from '@supercharge/promise-pool'; +import * as R from 'ramda'; +import jmesPath from 'jmespath'; +import {parse as parseArn} from '@aws-sdk/util-arn-parser'; +import createEnvironmentVariableRelationships from './createEnvironmentVariableRelationships.mjs'; +import { + AWS_API_GATEWAY_METHOD, + AWS_LAMBDA_FUNCTION, + AWS_AUTOSCALING_AUTOSCALING_GROUP, + AWS_CLOUDFRONT_DISTRIBUTION, + AWS_S3_BUCKET, + AWS_CLOUDFRONT_STREAMING_DISTRIBUTION, + AWS_IAM_ROLE, + AWS_EC2_SECURITY_GROUP, + AWS_EC2_SUBNET, + AWS_EC2_ROUTE_TABLE, + AWS_ECS_CLUSTER, + AWS_EC2_INSTANCE, + AWS_ECS_SERVICE, + AWS_ECS_TASK_DEFINITION, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_ECS_TASK, + SUBNET_ID, + NETWORK_INTERFACE_ID, + AWS_EC2_NETWORK_INTERFACE, + AWS_EFS_ACCESS_POINT, + AWS_EFS_FILE_SYSTEM, + AWS_EKS_NODE_GROUP, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER, + AWS_COGNITO_USER_POOL, + AWS_IAM_INLINE_POLICY, + AWS_IAM_USER, + UNKNOWN, + AWS_RDS_DB_INSTANCE, + AWS_EC2_NAT_GATEWAY, + AWS_EC2_VPC_ENDPOINT, + AWS_EC2_INTERNET_GATEWAY, + AWS_EVENT_EVENT_BUS, + AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + ENI_NAT_GATEWAY_INTERFACE_TYPE, + ENI_ALB_DESCRIPTION_PREFIX, + ENI_ELB_DESCRIPTION_PREFIX, + ELASTIC_LOAD_BALANCING, + LOAD_BALANCER, + ENI_VPC_ENDPOINT_INTERFACE_TYPE, + ENI_SEARCH_REQUESTER_ID, + ENI_SEARCH_DESCRIPTION_PREFIX, + IS_ATTACHED_TO, + LAMBDA, + S3, + AWS, + AWS_IAM_AWS_MANAGED_POLICY, + IS_ASSOCIATED_WITH, + CONTAINS, + TAGS, + TAG, + APPLICATION_TAG_NAME +} from '../constants.mjs'; +import { + createAssociatedRelationship, + createContainedInVpcRelationship, + createContainedInSubnetRelationship, + createAssociatedSecurityGroupRelationship, + createContainedInRelationship, + createContainsRelationship, + createAttachedRelationship, + createArnRelationship, + createArn, + createResourceNameKey, + createResourceIdKey, + isQualifiedRelationshipName +} from '../utils.mjs'; +import logger from '../logger.mjs'; +import schema from '../../schemas/schema.json' with { type: 'json' }; + +import { iterate } from "iterare" +const ajv = new Ajv(); +const validate = ajv.compile(schema) + +function createRelationship(descriptor, id) { + const {resourceType} = descriptor + // to match Config, we need precisely one space at the end relationship names that have not been appended + // with resource types such as `Is contained in (Vpc|Subnet|Role|Etc)`` + const relationshipName = isQualifiedRelationshipName(descriptor.relationshipName) + ? descriptor.relationshipName : descriptor.relationshipName.trim() + ' '; + + if(descriptor.identifierType === 'arn') { + return { + arn: id, + relationshipName, + } + } else { + return { + [descriptor.identifierType]: id, + relationshipName, + resourceType + } + } +} + +function mapEndpointToId(endpointsToIdMap, {descriptor, result}) { + if (descriptor.identifierType === 'endpoint') { + const arn = endpointsToIdMap.get(result); + return { + descriptor: { + ...descriptor, + identifierType: 'arn', + }, + result: arn, + }; + } + return {descriptor, result}; +} + +function createRelationshipHandler( + clientFactories, + {accountsMap, endpointToIdMap}, + schema +) { + return async function (resource) { + const {descriptors, rootPath = '@.configuration'} = + schema.relationships; + + const [sdkDescriptors, standardDescriptors] = R.partition( + descriptor => descriptor.sdkClient != null, + descriptors + ); + + const sdkRels = await Promise.all( + sdkDescriptors.map(async descriptor => { + const {sdkClient} = descriptor; + const {credentials} = accountsMap.get(resource.accountId); + + const client = clientFactories[sdkClient.type]( + credentials, + resource.awsRegion + ); + const sdkResult = await client[sdkClient.method]( + ...sdkClient.argumentPaths.map(path => { + return jmesPath.search(resource, path); + }) + ); + + return { + result: jmesPath.search(sdkResult, descriptor.path), + descriptor, + }; + }) + ); + + const root = jmesPath.search(resource, rootPath); + const standardRels = standardDescriptors.map(descriptor => { + return { + result: jmesPath.search(root, descriptor.path), + descriptor + }; + }); + + const allRels = iterate([...sdkRels, ...standardRels]) + .map(({result, descriptor}) => mapEndpointToId(endpointToIdMap, {result, descriptor})) + .filter(({result}) => result != null) + .map(({result, descriptor}) => { + if(result == null) return []; + + if(Array.isArray(result)) { + // flattening the JMESPath query result allows us to handle results of arbitrarily nested depths + return R.flatten(result).filter(x => x != null).map(id => createRelationship(descriptor, id)); + } else { + return [createRelationship(descriptor, result)]; + } + }).flatten() + .toArray() + + resource.relationships.push(...allRels); + }; +} + +const schemaFiles = await fs + .readdir('./src/schemas/resourceTypes') + .then( + R.map(fileName => + import(`../../schemas/resourceTypes/${fileName}`, { + with: {type: 'json'}, + }) + ) + ) + .then(ps => Promise.all(ps)) + .then(R.map(({default: schema}) => schema)) + .then( + R.filter(schema => { + if (validate(schema)) { + return true; + } else { + logger.error( + `There was an error validating the ${schema.type} schema.`, + { + errors: validate.errors, + } + ); + return false; + } + }) + ); + +function createSchemaHandlers(awsClient, lookupMaps) { + const clientFactories = { + ecs: awsClient.createEcsClient, + elbV1: awsClient.createElbClient, + elbV2: awsClient.createElbV2Client + } + + return schemaFiles.reduce((acc, schema) => { + acc[schema.type] = createRelationshipHandler(clientFactories, lookupMaps, schema); + return acc; + }, {}); +} + +function createEcsEfsRelationships(volumes) { + return volumes.reduce((acc, {EfsVolumeConfiguration}) => { + if(EfsVolumeConfiguration != null) { + if(EfsVolumeConfiguration.AuthorizationConfig?.AccessPointId != null) { + acc.push(createAssociatedRelationship(AWS_EFS_ACCESS_POINT, {resourceId: EfsVolumeConfiguration.AuthorizationConfig.AccessPointId})); + } else { + acc.push(createAssociatedRelationship(AWS_EFS_FILE_SYSTEM, {resourceId: EfsVolumeConfiguration.FileSystemId})); + } + } + return acc; + }, []); +} + +function createEniRelationship({description, interfaceType, requesterId, awsRegion, accountId}) { + if(interfaceType === ENI_NAT_GATEWAY_INTERFACE_TYPE) { + //Every nat-gateway ENI has a `description` field like this: Interface for NAT Gateway + const {groups: {resourceId}} = R.match(/(?nat-[0-9a-fA-F]+)/, description); + return createAttachedRelationship(AWS_EC2_NAT_GATEWAY, {resourceId}); + } else if(description.startsWith(ENI_ALB_DESCRIPTION_PREFIX)) { + const [app, albGroup, linkedAlb] = description.replace(ENI_ELB_DESCRIPTION_PREFIX, '').split('/'); + const albArn = createArn( + {service: ELASTIC_LOAD_BALANCING, accountId, region: awsRegion, resource: `${LOAD_BALANCER}/${app}/${albGroup}/${linkedAlb}`} + ); + + return createArnRelationship(IS_ATTACHED_TO, albArn); + } else if(interfaceType === ENI_VPC_ENDPOINT_INTERFACE_TYPE) { + //Every VPC Endpoint ENI has a `description` field like this: VPC Endpoint Interface + const {groups: {resourceId}} = R.match(/(?vpce-[0-9a-fA-F]+)/, description) + return createAttachedRelationship(AWS_EC2_VPC_ENDPOINT, {resourceId}); + } else if(requesterId === ENI_SEARCH_REQUESTER_ID) { + // it's not possible to tell whether we have an OpenSearch or Elasticsearch cluster from the ENI + // so we must use an ARN instead as these both use the same format + const domainName = description.replace(ENI_SEARCH_DESCRIPTION_PREFIX, ''); + const arn = createArn({ + service: 'es', accountId, region: awsRegion, resource: `domain/${domainName}` + }); + + return createArnRelationship(IS_ATTACHED_TO, arn); + } else if(interfaceType === LAMBDA) { + // Every lambda ENI has a `description` field like this: AWS Lambda VPC ENI->-" + const resourceId = description + .replace('AWS Lambda VPC ENI-', '') + .replace(/-[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}$/i, ''); + + return createAttachedRelationship(AWS_LAMBDA_FUNCTION, {resourceId}); + } else { + return {resourceId: UNKNOWN} + } +} + +function createManagedPolicyRelationships(resourceMap, policies) { + return policies.reduce((acc, {policyArn}) => { + const {accountId} = parseArn(policyArn); + if(accountId === AWS) { + acc.push(createAttachedRelationship(AWS_IAM_AWS_MANAGED_POLICY, {arn: policyArn})); + } + return acc; + }, []); +} + +function createIndividualHandlers(lookUpMaps, awsClient) { + + const { + accountsMap, + endpointToIdMap, + resourceIdentifierToIdMap, + targetGroupToAsgMap, + elbDnsToResourceIdMap, + asgResourceNameToResourceIdMap, + envVarResourceIdentifierToIdMap, + eventBusRuleMap, + resourceMap + } = lookUpMaps; + + return { + [AWS_API_GATEWAY_METHOD]: async ({relationships, configuration: {methodIntegration}}) => { + const methodUri = methodIntegration?.uri ?? ''; + const lambdaArn = R.match(/arn.*\/functions\/(?.*)\/invocations/, methodUri).groups?.lambdaArn; + if(lambdaArn != null) { // not all API gateways use lambda + relationships.push(createAssociatedRelationship(AWS_LAMBDA_FUNCTION, {arn: lambdaArn})); + } + }, + [AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION]: async ({accountId, configuration: {applicationTag}, relationships}) => { + if(applicationTag == null) return; + + const tagResourceName = `${APPLICATION_TAG_NAME}=${applicationTag.awsApplication}`; + const applicationTagArn = createArn({ + service: TAGS, accountId, resource: `${TAG}/${tagResourceName}` + }); + + const tag = resourceMap.get(applicationTagArn); + + if(tag != null) { + relationships.push(...tag.relationships.map(rel => { + return { + ...rel, + relationshipName: CONTAINS + }; + })); + } + }, + [AWS_CLOUDFRONT_DISTRIBUTION]: async ({configuration: {distributionConfig}, relationships}) => { + relationships.forEach(relationship => { + const {resourceId, resourceType} = relationship; + if(resourceType === AWS_S3_BUCKET) { + relationship.arn = createArn({service: S3, resource: resourceId}); + } + }); + + const items = distributionConfig.origins?.items ?? []; + + relationships.push(...items.reduce((acc, {domainName}) => { + if(elbDnsToResourceIdMap.has(domainName)) { + const {resourceType, resourceId, awsRegion} = elbDnsToResourceIdMap.get(domainName) + acc.push(createAssociatedRelationship(resourceType, {resourceId, awsRegion})); + } + return acc; + }, [])); + }, + [AWS_CLOUDFRONT_STREAMING_DISTRIBUTION]: async ({relationships}) => { + relationships.forEach(relationship => { + const {resourceId, resourceType} = relationship; + if(resourceType === AWS_S3_BUCKET) { + relationship.arn = createArn({service: S3, resource: resourceId}); + } + }); + }, + [AWS_EC2_SECURITY_GROUP]: async ({configuration, relationships}) => { + const {ipPermissions, ipPermissionsEgress} = configuration; + const securityGroups = [...ipPermissions, ...ipPermissionsEgress].reduce((acc, {userIdGroupPairs = []}) => { + userIdGroupPairs.forEach(({groupId}) => { + if(groupId != null) acc.add(groupId); + }); + return acc; + }, new Set()); + + relationships.push(...Array.from(securityGroups).map(createAssociatedSecurityGroupRelationship)); + }, + [AWS_EC2_SUBNET]: async subnet => { + const {relationships, awsRegion, accountId, configuration: {subnetId}} = subnet; + + subnet.subnetId = subnetId; + + const routeTableRel = relationships.find(x => x.resourceType === AWS_EC2_ROUTE_TABLE); + if(routeTableRel != null) { + const {resourceId, resourceType} = routeTableRel; + const routeTableId = resourceIdentifierToIdMap.get(createResourceIdKey({resourceId, resourceType, accountId, awsRegion})); + const routes = resourceMap.get(routeTableId)?.configuration?.routes ?? []; + const natGateways = routes.filter(x => x.natGatewayId != null); + subnet.private = natGateways.length === 0; + } + }, + [AWS_ECS_TASK]: async task => { + const {accountId, awsRegion, configuration} = task; + const {clusterArn, overrides, attachments = [], taskDefinitionArn} = configuration; + + // running tasks can reference deregistered and/or deleted task definitions so we need to + // provide fallback values in case the definition no longer exists + const taskDefinition = resourceMap.get(taskDefinitionArn) ?? { + configuration: { + ContainerDefinitions: [], + Volumes: [] + } + }; + + task.relationships.push(createContainedInRelationship(AWS_ECS_CLUSTER, {arn: clusterArn})); + + const {taskRoleArn, executionRoleArn, containerOverrides = []} = overrides; + const roleRels = R.reject(R.isNil, [taskRoleArn, executionRoleArn]) + .map(arn => createAssociatedRelationship(AWS_IAM_ROLE, {arn})); + + if (R.isEmpty(roleRels)) { + const {configuration: {TaskRoleArn, ExecutionRoleArn}} = taskDefinition; + R.reject(R.isNil, [TaskRoleArn, ExecutionRoleArn]) + .forEach(arn => { + task.relationships.push(createAssociatedRelationship(AWS_IAM_ROLE, {arn})); + }); + } else { + task.relationships.push(...roleRels); + } + + const groupedDefinitions = R.groupBy(x => x.Name, taskDefinition.configuration.ContainerDefinitions); + const groupedOverrides = R.groupBy(x => x.name, containerOverrides); + + const environmentVariables = Object.entries(groupedDefinitions).map(([key, val]) => { + const Environment = R.head(val)?.Environment ?? []; + const environment = R.head(groupedOverrides[key] ?? [])?.environment ?? []; + + const envVarObj = Environment.reduce((acc, {Name, Value}) => { + acc[Name] = Value; + return acc + }, {}); + + const overridesObj = environment.reduce((acc, {name, value}) => { + acc[name] = value; + return acc + }, {}); + + return {...envVarObj, ...overridesObj}; + }, {}); + + environmentVariables.forEach( variables => { + task.relationships.push(...createEnvironmentVariableRelationships( + {resourceMap, envVarResourceIdentifierToIdMap, endpointToIdMap}, + {accountId, awsRegion}, + variables)); + }); + + task.relationships.push(...createEcsEfsRelationships(taskDefinition.configuration.Volumes)); + + attachments.forEach(({details}) => { + return details.forEach(({name, value}) => { + if(name === SUBNET_ID) { + const subnetArn = resourceIdentifierToIdMap.get(createResourceIdKey({resourceId: value, resourceType: AWS_EC2_SUBNET, accountId, awsRegion})); + const vpcId = resourceMap.get(subnetArn)?.configuration?.vpcId; // we may not have discovered the subnet + + if(vpcId != null) task.relationships.push(createContainedInVpcRelationship(vpcId)); + + task.relationships.push(createContainedInSubnetRelationship(value)); + } else if (name === NETWORK_INTERFACE_ID) { + const networkInterfaceId = resourceIdentifierToIdMap.get(createResourceIdKey({resourceId: value, resourceType: AWS_EC2_NETWORK_INTERFACE, accountId, awsRegion})); + // occasionally network interface information is stale, so we need to do null checks here + resourceMap.get(networkInterfaceId)?.relationships?.push(createAttachedRelationship(AWS_ECS_TASK, {resourceId: task.resourceId})); + } + }); + }); + }, + [AWS_ECS_TASK_DEFINITION]: async ({relationships, accountId, awsRegion, configuration}) => { + configuration.ContainerDefinitions.forEach(({Environment = []}) => { + const variables = Environment.reduce((acc, {Name, Value}) => { + acc[Name] = Value; + return acc + }, {}); + relationships.push(...createEnvironmentVariableRelationships( + {resourceMap, envVarResourceIdentifierToIdMap, endpointToIdMap}, + {accountId, awsRegion}, + variables)); + }); + }, + [AWS_EKS_NODE_GROUP]: async nodeGroup => { + const {accountId, awsRegion, relationships, configuration} = nodeGroup; + const autoScalingGroups = configuration.resources?.autoScalingGroups ?? []; + + relationships.push( + ...autoScalingGroups.map(({name}) => { + const rId = asgResourceNameToResourceIdMap.get(createResourceNameKey({ + resourceName: name, + accountId, + awsRegion + })); + return createAssociatedRelationship(AWS_AUTOSCALING_AUTOSCALING_GROUP, {resourceId: rId}); + }), + ); + }, + [AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER]: async ({relationships, configuration: {LoadBalancerArn, DefaultActions}}) => { + const {targetGroups, cognitoUserPools} = DefaultActions.reduce((acc, {AuthenticateCognitoConfig, TargetGroupArn, ForwardConfig}) => { + if(AuthenticateCognitoConfig != null) acc.cognitoUserPools.add(AuthenticateCognitoConfig.UserPoolArn); + if(TargetGroupArn != null) acc.targetGroups.add(TargetGroupArn); + if(ForwardConfig != null) { + const {TargetGroups = []} = ForwardConfig; + TargetGroups.forEach(x => acc.targetGroups.add(x.TargetGroupArn)) + } + return acc; + }, {cognitoUserPools: new Set(), targetGroups: new Set}); + + relationships.push( + createAssociatedRelationship(AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER, {resourceId: LoadBalancerArn}), + ...Array.from(targetGroups.values()).map(resourceId => createAssociatedRelationship(AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, {resourceId})), + ...Array.from(cognitoUserPools.values()).map(resourceId => createAssociatedRelationship(AWS_COGNITO_USER_POOL, {resourceId})) + ); + }, + [AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP]: async ({accountId, awsRegion, arn, configuration: {VpcId}, relationships}) => { + const {credentials} = accountsMap.get(accountId); + const elbClientV2 = awsClient.createElbV2Client(credentials, awsRegion); + + const {instances: asgInstances, arn: asgArn} = targetGroupToAsgMap.get(arn) ?? {instances: new Set()}; + + const targetHealthDescriptions = await elbClientV2.describeTargetHealth(arn); + + //TODO: use TargetHealth to label the link as to whether it's healthy or not + relationships.push(createContainedInVpcRelationship(VpcId), + ...targetHealthDescriptions.reduce((acc, {Target: {Id}, TargetHealth}) => { + // We don't want to include instances from ASGs as the direct link should be to the + // ASG not the instances therein + if(Id.startsWith('i-') && !asgInstances.has(Id)) { + acc.push(createAssociatedRelationship(AWS_EC2_INSTANCE, {resourceId:Id})); + } else if(Id.startsWith('arn:')) { + acc.push(createArnRelationship(IS_ASSOCIATED_WITH, Id)); + } + return acc; + }, [])); + + if(asgArn != null) { + relationships.push(createAssociatedRelationship(AWS_AUTOSCALING_AUTOSCALING_GROUP, {resourceId: asgArn})); + } + }, + [AWS_EVENT_EVENT_BUS]: async ({arn, relationships}) => { + relationships.push(...eventBusRuleMap.get(arn).map(createArnRelationship(IS_ASSOCIATED_WITH))); + }, + [AWS_IAM_ROLE]: async ({configuration: {attachedManagedPolicies}, relationships}) => { + relationships.push(...createManagedPolicyRelationships(resourceMap, attachedManagedPolicies)); + }, + [AWS_IAM_INLINE_POLICY]: ({configuration: {policyDocument}, relationships}) => { + const statement = Array.isArray(policyDocument.Statement) ? + policyDocument.Statement : [policyDocument.Statement]; + + relationships.push(...statement.flatMap(({Resource = []}) => { + // the Resource field, if it exists, can be an array or string + const resources = Array.isArray(Resource) ? Resource : [Resource]; + return resources.reduce((acc, resourceArn) => { + // Remove the trailing /* from ARNs to increase chance of finding + // a relationship, especially for S3 buckets. This will lead to + // duplicates, but they get deduped later on in the discovery + // process + const resource = resourceMap.get(resourceArn.replace(/\/?\*$/, '')); + if(resource != null) { + acc.push(createAttachedRelationship(resource.resourceType, { + arn: resource.arn + })); + } + return acc; + }, []); + })); + }, + [AWS_IAM_USER]: ({configuration: {attachedManagedPolicies}, relationships}) => { + relationships.push(...createManagedPolicyRelationships(resourceMap, attachedManagedPolicies)); + }, + [AWS_EC2_NETWORK_INTERFACE]: async eni => { + const {accountId, awsRegion, relationships, configuration} = eni; + const {interfaceType, description, requesterId} = configuration; + + const relationship = createEniRelationship({awsRegion, accountId, interfaceType, description, requesterId}); + if(relationship.resourceId !== UNKNOWN) { + relationships.push(relationship); + } + }, + [AWS_RDS_DB_INSTANCE]: async db => { + const {dBSubnetGroup, availabilityZone} = db.configuration; + + if(dBSubnetGroup != null) { + const {subnetIdentifier} = R.find(({subnetAvailabilityZone}) => subnetAvailabilityZone.name === availabilityZone, + dBSubnetGroup.subnets); + + db.relationships.push(...[ + createContainedInVpcRelationship(dBSubnetGroup.vpcId), + createContainedInSubnetRelationship(subnetIdentifier) + ]); + } + }, + [AWS_EC2_ROUTE_TABLE]: async ({configuration: {routes}, relationships}) => { + relationships.push(...routes.reduce((acc, {natGatewayId, gatewayId}) => { + if(natGatewayId != null) { + acc.push(createContainsRelationship(AWS_EC2_NAT_GATEWAY, {resourceId: natGatewayId})); + } else if(R.test(/vpce-[0-9a-fA-F]+/, gatewayId)) { + acc.push(createContainsRelationship(AWS_EC2_VPC_ENDPOINT, {resourceId: gatewayId})); + } else if(R.test(/igw-[0-9a-fA-F]+/, gatewayId)) { + acc.push(createContainsRelationship(AWS_EC2_INTERNET_GATEWAY, {resourceId: gatewayId})); + } + return acc; + }, [])); + } + } +} + +async function addIndividualRelationships(lookUpMaps, awsClient, resources) { + const handlers = createIndividualHandlers(lookUpMaps, awsClient); + const schemaHandlers = createSchemaHandlers(awsClient, lookUpMaps); + + const {errors} = await PromisePool + .withConcurrency(30) + .for(resources) + .process(async resource => { + const handler = handlers[resource.resourceType]; + const schemaHandler = schemaHandlers[resource.resourceType]; + + if(schemaHandler != null) await schemaHandler(resource); + if(handler != null) await handler(resource); + }); + + logger.error(`There were ${errors.length} errors when adding additional relationships.`); + logger.debug('Errors: ', {errors}); +} + +export default addIndividualRelationships; diff --git a/source/backend/discovery/src/lib/additionalRelationships/createEnvironmentVariableRelationships.mjs b/source/backend/discovery/src/lib/additionalRelationships/createEnvironmentVariableRelationships.mjs new file mode 100644 index 00000000..03cbe414 --- /dev/null +++ b/source/backend/discovery/src/lib/additionalRelationships/createEnvironmentVariableRelationships.mjs @@ -0,0 +1,38 @@ +import {createAssociatedRelationship, createResourceIdKey, createResourceNameKey} from '../utils.mjs'; +import {AWS_S3_ACCOUNT_PUBLIC_ACCESS_BLOCK} from '../constants.mjs'; + +function createEnvironmentVariableRelationships( + {resourceMap, envVarResourceIdentifierToIdMap, endpointToIdMap}, + {accountId, awsRegion}, + variables +) { + //TODO: add env var name as a property of the edge + return Object.values(variables).reduce((acc, val) => { + if (resourceMap.has(val)) { + const {resourceType, arn} = resourceMap.get(val); + acc.push(createAssociatedRelationship(resourceType, {arn})); + } else { + // this branch assumes all resources are in the same region + const resourceIdKey = createResourceIdKey({resourceId: val, accountId, awsRegion}); + const resourceNameKey = createResourceNameKey({resourceName: val, accountId, awsRegion}); + + const id = envVarResourceIdentifierToIdMap.get(resourceIdKey) + ?? envVarResourceIdentifierToIdMap.get(resourceNameKey) + ?? endpointToIdMap.get(val); + + if(resourceMap.has(id)) { + const {resourceType, resourceId} = resourceMap.get(id); + + // The resourceId of the AWS::S3::AccountPublicAccessBlock resource type is the accountId where it resides. + // We need to filter out environment variables that have AWS account IDs because otherwise we will create + // an erroneous relationship between the resource and the AWS::S3::AccountPublicAccessBlock + if(resourceId !== accountId && resourceType !== AWS_S3_ACCOUNT_PUBLIC_ACCESS_BLOCK) { + acc.push(createAssociatedRelationship(resourceType, {arn: id})); + } + } + } + return acc; + }, []); +} + +export default createEnvironmentVariableRelationships; \ No newline at end of file diff --git a/source/backend/discovery/src/lib/additionalRelationships/createLookUpMaps.mjs b/source/backend/discovery/src/lib/additionalRelationships/createLookUpMaps.mjs new file mode 100644 index 00000000..26483be7 --- /dev/null +++ b/source/backend/discovery/src/lib/additionalRelationships/createLookUpMaps.mjs @@ -0,0 +1,114 @@ +import * as R from 'ramda'; +import { + AWS_AUTOSCALING_AUTOSCALING_GROUP, + AWS_ELASTICSEARCH_DOMAIN, + AWS_OPENSEARCH_DOMAIN, + AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER, + AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER, + AWS_EVENT_RULE, + AWS_RDS_DB_CLUSTER, + EVENTS, + EVENT_BUS +} from '../constants.mjs'; +import {createResourceNameKey, createResourceIdKey, createArn} from '../utils.mjs'; + +function getEndpoint(configuration) { + const endpoint = configuration.endpoint ?? configuration.Endpoint; + return endpoint?.value ?? endpoint?.address ?? endpoint; +} + +function createResourceTypeLookUpMaps(resources) { + const targetGroupToAsgMap = new Map(); + const endpointToIdMap = new Map(); + const elbDnsToResourceIdMap = new Map(); + const asgResourceNameToResourceIdMap = new Map(); + const eventBusRuleMap = new Map(); + + const handlers = { + [AWS_AUTOSCALING_AUTOSCALING_GROUP]: resource => { + const {resourceId, resourceName, accountId, awsRegion, arn, configuration} = resource; + configuration.targetGroupARNs.forEach(tg => + targetGroupToAsgMap.set(tg, { + arn, + instances: new Set(configuration.instances.map(R.prop('instanceId'))) + })); + asgResourceNameToResourceIdMap.set( + createResourceNameKey( + {resourceName, accountId, awsRegion}), + resourceId); + }, + [AWS_ELASTICSEARCH_DOMAIN]: ({id, configuration: {endpoints = []}}) => { + Object.values(endpoints).forEach(endpoint => endpointToIdMap.set(endpoint, id)); + }, + [AWS_OPENSEARCH_DOMAIN]: ({id, configuration: {Endpoints = []}}) => { + Object.values(Endpoints).forEach(endpoint => endpointToIdMap.set(endpoint, id)); + }, + [AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER]: ({resourceId, resourceType, awsRegion, configuration}) => { + elbDnsToResourceIdMap.set(configuration.dnsname, {resourceId, resourceType, awsRegion}); + }, + [AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER]: ({resourceId, resourceType, awsRegion, configuration}) => { + elbDnsToResourceIdMap.set(configuration.dNSName, {resourceId, resourceType, awsRegion}); + }, + [AWS_EVENT_RULE]: ({id, accountId, awsRegion, configuration: {EventBusName}}) => { + const eventBusArn = EventBusName.startsWith('arn:') + ? EventBusName : createArn({ + service: EVENTS, accountId, region: awsRegion, resource: `${EVENT_BUS}/${EventBusName}`, + }); + if(!eventBusRuleMap.has(eventBusArn)) eventBusRuleMap.set(eventBusArn, []); + eventBusRuleMap.get(eventBusArn).push(id); + }, + [AWS_RDS_DB_CLUSTER]: ({id, configuration: {readerEndpoint}}) => { + if(readerEndpoint != null) endpointToIdMap.set(readerEndpoint, id); + } + }; + + for(let resource of resources) { + const {id, resourceType, configuration} = resource; + const endpoint = getEndpoint(configuration); + + if(endpoint != null) { + endpointToIdMap.set(endpoint, id); + } + + const handler = handlers[resourceType]; + if(handler != null) handler(resource); + } + + return { + endpointToIdMap, + targetGroupToAsgMap, + elbDnsToResourceIdMap, + asgResourceNameToResourceIdMap, + eventBusRuleMap + } +} + +function createLookUpMaps(resources) { + const resourceIdentifierToIdMap = new Map(); + // we can't reuse resourceIdentifierToIdMap because we don't know the resource type for env vars + const envVarResourceIdentifierToIdMap = new Map(); + + for(let resource of resources) { + const {id, resourceType, resourceId, resourceName, accountId, awsRegion} = resource; + + if(resourceName != null) { + envVarResourceIdentifierToIdMap.set(createResourceNameKey({resourceName, accountId, awsRegion}), id); + resourceIdentifierToIdMap.set( + createResourceNameKey({resourceName, resourceType, accountId, awsRegion}), + id); + } + + resourceIdentifierToIdMap.set( + createResourceIdKey({resourceId, resourceType, accountId, awsRegion}), + id); + envVarResourceIdentifierToIdMap.set(createResourceIdKey({resourceId, accountId, awsRegion}), id); + } + + return { + resourceIdentifierToIdMap, + envVarResourceIdentifierToIdMap, + ...createResourceTypeLookUpMaps(resources) + } +} + +export default createLookUpMaps; \ No newline at end of file diff --git a/source/backend/discovery/src/lib/additionalRelationships/index.mjs b/source/backend/discovery/src/lib/additionalRelationships/index.mjs new file mode 100644 index 00000000..3a7a5fb0 --- /dev/null +++ b/source/backend/discovery/src/lib/additionalRelationships/index.mjs @@ -0,0 +1,124 @@ +import * as R from 'ramda'; +import {iterate} from 'iterare'; +import addBatchedRelationships from './addBatchedRelationships.mjs'; +import addIndividualRelationships from './addIndividualRelationships.mjs'; +import createLookUpMaps from './createLookUpMaps.mjs'; +import { + EC2, + AWS_EC2_SUBNET, + AWS_TAGS_TAG, + AWS_CLOUDFORMATION_STACK, + AWS_CONFIG_RESOURCE_COMPLIANCE, + AWS_EC2_VPC, + VPC, + CONTAINS +} from '../constants.mjs'; +import { + createArn, + createContainedInVpcRelationship, + resourceTypesToNormalizeSet, + isQualifiedRelationshipName +} from '../utils.mjs'; + +function getSubnetInfo(resourceMap, accountId, awsRegion, subnetIds) { + const {availabilityZones, vpcId} = subnetIds.reduce((acc, subnetId) => { + const subnetArn = createArn({service: EC2, accountId, region: awsRegion, resource: `subnet/${subnetId}`}); + + // we may not have ingested the subnets + if(resourceMap.has(subnetArn)) { + const {configuration: {vpcId}, availabilityZone} = resourceMap.get(subnetArn); + if(acc.vpcId == null) acc.vpcId = vpcId; + acc.availabilityZones.add(availabilityZone); + } + + return acc; + }, {availabilityZones: new Set()}); + + return {vpcId, availabilityZones: Array.from(availabilityZones).sort()} +} + +function shouldNormaliseRelationship(rel) { + return resourceTypesToNormalizeSet.has(rel.resourceType) && !isQualifiedRelationshipName(rel.relationshipName); +} + +/** + * AWS Config qualifies some relationship names based on the resource type, e.g., the `Is contained in ` + * relationship becomes `Is contained in Subnet`. However, Config does not do this consistently, it will + * use `Is contained in Subnet` for EC2 instances but the unqualified `Is contained in ` for lambda + * functions. Note that the space at the end of the unqualified relationship name also comes from Config. + * This function aims to make the relationship names consistent across all resource types regardless of whether + * they originate from Config or Workload Discovery. + * */ +function normaliseRelationshipNames(resource) { + if (![AWS_TAGS_TAG, AWS_CONFIG_RESOURCE_COMPLIANCE].includes(resource.resourceType)) { + const {relationships} = resource; + + iterate(relationships) + .filter(shouldNormaliseRelationship) + .forEach(rel => { + const {resourceType, relationshipName} = rel; + + const [,, relSuffix] = resourceType.split('::'); + // VPC is in camelcase + if(!relationshipName.toLowerCase().includes(relSuffix.toLowerCase())) { + rel.relationshipName = relationshipName + (resourceType === AWS_EC2_VPC ? VPC : relSuffix); + } + }); + } + + return resource; +} + +const addVpcInfo = R.curry((resourceMap, resource) => { + if (![AWS_TAGS_TAG, AWS_CONFIG_RESOURCE_COMPLIANCE, AWS_CLOUDFORMATION_STACK].includes(resource.resourceType)) { + const {accountId, awsRegion, relationships} = resource; + + const vpcArray = relationships + .filter(x => x.resourceType === AWS_EC2_VPC) + .map(x => x.resourceId); + + const subnetIds = relationships + .filter(x => x.resourceType === AWS_EC2_SUBNET && !x.relationshipName.includes(CONTAINS)) + .map(x => x.resourceId) + .sort(); + + if (!R.isEmpty(vpcArray)) { + resource.vpcId = R.head(vpcArray); + } + + if(!R.isEmpty(subnetIds)) { + const {vpcId, availabilityZones} = getSubnetInfo(resourceMap, accountId, awsRegion, subnetIds); + if(R.isEmpty(vpcArray) && vpcId != null) { + relationships.push(createContainedInVpcRelationship(vpcId)); + resource.vpcId = vpcId; + } + if(!R.isEmpty(availabilityZones)) { + resource.availabilityZone = availabilityZones.join(','); + } + } + + if (subnetIds.length === 1) { + resource.subnetId = R.head(subnetIds); + } + } + + return resource; +}) + +// for performance reasons, each handler mutates the items in `resources` +export const addAdditionalRelationships = R.curry(async (accountsMap, awsClient, resources) => { + const resourceMap = new Map(resources.map(resource => ([resource.id, resource]))); + + const lookUpMaps = { + accountsMap, + ...createLookUpMaps(resources), + resourceMap + }; + + await addBatchedRelationships(lookUpMaps, awsClient); + + await addIndividualRelationships(lookUpMaps, awsClient, resources) + + return resources + .map(R.compose(addVpcInfo(resourceMap), normaliseRelationshipNames)); +}) diff --git a/source/backend/discovery/src/lib/aggregator/getAllConfigResources.mjs b/source/backend/discovery/src/lib/aggregator/getAllConfigResources.mjs new file mode 100644 index 00000000..7ba84125 --- /dev/null +++ b/source/backend/discovery/src/lib/aggregator/getAllConfigResources.mjs @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {PromisePool} from '@supercharge/promise-pool'; +import * as R from 'ramda'; +import logger from '../logger.mjs'; +import { + AWS_COGNITO_USER_POOL, + AWS_KINESIS_STREAM, + AWS_EKS_CLUSTER, + AWS_ECS_TASK_DEFINITION, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_IAM_INSTANCE_PROFILE, + AWS_MEDIA_CONNECT_FLOW_ENTITLEMENT, + AWS_OPENSEARCH_DOMAIN, + AWS_SSM_MANAGED_INSTANCE_INVENTORY +} from '../constants.mjs'; +import {createArnWithResourceType, isDate, isString, isObject, objToKeyNameArray} from '../utils.mjs'; + +const unsupportedAdvancedQueryResourceTypes = [ + AWS_COGNITO_USER_POOL, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_IAM_INSTANCE_PROFILE, + AWS_MEDIA_CONNECT_FLOW_ENTITLEMENT +]; + +const resourceTypesToExclude = [ + ...unsupportedAdvancedQueryResourceTypes, + // the configuration item that Config returns for OpenSearch is missing fields so we use the SDK instead + AWS_OPENSEARCH_DOMAIN +] + +async function getAdvancedQueryUnsupportedResources(configServiceClient, aggregatorName) { + const {results, errors} = await PromisePool + .withConcurrency(5) + .for(unsupportedAdvancedQueryResourceTypes) + .process(async resourceType => { + return configServiceClient.getAggregatorResources(aggregatorName, resourceType); + }); + + logger.error(`There were ${errors.length} errors when importing resource types unsupported by advanced query.`); + logger.debug('Errors: ', {errors}); + + return results.flat(); +} + +function normaliseConfigurationItem(resource) { + const { + arn, resourceType, accountId, awsRegion, resourceId, configuration = {}, + tags = [], configurationItemCaptureTime + } = resource; + resource.arn = arn ?? createArnWithResourceType({resourceType, accountId, awsRegion, resourceId}); + resource.id = resource.arn; + + switch (resource.resourceType) { + // resourceIds for these resource types are not unique per account + case AWS_ECS_TASK_DEFINITION: + case AWS_EKS_CLUSTER: + case AWS_KINESIS_STREAM: + case AWS_SSM_MANAGED_INSTANCE_INVENTORY: + resource.resourceId = resource.arn; + break; + default: + break; + } + + resource.configuration = isString(configuration) ? JSON.parse(configuration) : configuration; + resource.configurationItemCaptureTime = isDate(configurationItemCaptureTime) ? + configurationItemCaptureTime.toISOString() : configurationItemCaptureTime; + + // the return type for tags is not always consistent, sometimes it returns an object where the key/value + // pairs represent the tags names and values + resource.tags = isObject(tags) ? objToKeyNameArray(tags) : tags; + resource.relationships = resource.relationships ?? []; + return resource; +} + +async function getAllConfigResources(configServiceClient, configAggregatorName) { + logger.profile('Time to download resources from Config'); + logger.info('Retrieving resources from Config.'); + + return Promise.all([ + getAdvancedQueryUnsupportedResources(configServiceClient, configAggregatorName), + // We need to exclude the resources we get from querying the aggregator without the SQL + // API because it returns results in UpperCamelCase whereas the SQL API returns them in + // camelCase. If these resource types get added to the SQL API we would have duplicates + // with different casing schemes that could break downstream processing. + configServiceClient.getAllAggregatorResources(configAggregatorName, + {excludes: {resourceTypes: resourceTypesToExclude}} + ) + ]) + .then(R.chain(R.map(normaliseConfigurationItem))) + .then(R.tap(() => logger.profile('Time to download resources from Config'))) +} + +export default getAllConfigResources; \ No newline at end of file diff --git a/source/backend/discovery/src/lib/apiClient/appSync.mjs b/source/backend/discovery/src/lib/apiClient/appSync.mjs new file mode 100644 index 00000000..0aee6bbb --- /dev/null +++ b/source/backend/discovery/src/lib/apiClient/appSync.mjs @@ -0,0 +1,341 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import {request} from 'undici'; +import retry from 'async-retry'; +import aws4 from 'aws4'; +import logger from '../logger.mjs'; +import { + CONNECTION_CLOSED_PREMATURELY, + FUNCTION_RESPONSE_SIZE_TOO_LARGE, + RESOLVER_CODE_SIZE_ERROR +} from '../constants.mjs'; + +async function sendQuery(opts, name, {query, variables = {}}) { + const sigOptions = { + method: 'POST', + host: opts.host, + region: opts.region, + path: opts.path, + headers: { + 'x-amzn-workload-discovery-requester': 'discovery-process' + }, + body: JSON.stringify({ + query, + variables + }), + service: 'appsync' + }; + + const sig = aws4.sign(sigOptions, opts.creds); + + return retry(async bail => { + return request(opts.graphgQlUrl, { + method: 'POST', + headers: sig.headers, + body: sigOptions.body + }).catch(err => { + logger.error(`Error sending gql request, ensure query is not malformed: ${err.message}`) + bail(err); + }).then(({body}) => body.json()) + .then((body) => { + const {errors} = body; + if (errors != null) { + if(errors.length === 1) { + const {message} = R.head(errors); + // this transient error can happen due to a bug in the Gremlin client library + // that the appSync lambda uses, 1 retry is normally sufficient + if(message === CONNECTION_CLOSED_PREMATURELY) { + throw new Error(message); + } + if([RESOLVER_CODE_SIZE_ERROR, FUNCTION_RESPONSE_SIZE_TOO_LARGE].includes(message)) { + return bail(new Error(message)); + } + } + logger.error('Error executing gql request', {errors: body.errors, query, variables}) + return bail(new Error(JSON.stringify(errors))); + } + return body.data[name]; + }); + }, { + retries: 3, + onRetry: (err, count) => { + logger.error(`Retry attempt for ${name} no ${count}: ${err.message}`); + } + }); +} + +function createPaginator(operation, PAGE_SIZE) { + return async function*(args) { + let pageSize = PAGE_SIZE; + let start = 0; + let end = pageSize; + let resources = null; + + while(!R.isEmpty(resources)) { + try { + resources = await operation({pagination: {start, end}, ...args}); + yield resources + start = start + pageSize; + pageSize = PAGE_SIZE; + end = end + pageSize; + } catch(err) { + if([RESOLVER_CODE_SIZE_ERROR, FUNCTION_RESPONSE_SIZE_TOO_LARGE].includes(err.message)) { + pageSize = Math.floor(pageSize / 2); + logger.debug(`Lambda response size too large, reducing page size to ${pageSize}`); + end = start + pageSize; + } else { + throw err; + } + } + } + } +} + +const getAccounts = opts => async () => { + const name = 'getAccounts'; + const query = ` + query ${name} { + getAccounts { + accountId + lastCrawled + name + regions { + name + } + } + }`; + return sendQuery(opts, name, {query}); +}; + +const addRelationships = opts => async relationships => { + const name = 'addRelationships'; + const query = ` + mutation ${name}($relationships: [RelationshipInput]!) { + ${name}(relationships: $relationships) { + id + } + }`; + const variables = {relationships}; + return sendQuery(opts, name, {query, variables}); +}; + +const addResources = opts => async resources => { + const name = 'addResources'; + const query = ` + mutation ${name}($resources: [ResourceInput]!) { + ${name}(resources: $resources) { + id + label + } + }`; + const variables = {resources}; + return sendQuery(opts, name, {query, variables}); +}; + +const getResources = opts => async ({pagination, resourceTypes, accounts}) => { + const name = 'getResources'; + const query = ` + query ${name}( + $pagination: Pagination + $resourceTypes: [String] + $accounts: [AccountInput] + ) { + getResources( + pagination: $pagination + resourceTypes: $resourceTypes + accounts: $accounts + ) { + id + label + md5Hash + properties { + accountId + arn + availabilityZone + awsRegion + configuration + configurationItemCaptureTime + configurationStateId + configurationItemStatus + loggedInURL + loginURL + private + resourceCreationTime + resourceName + resourceId + resourceType + resourceValue + state + supplementaryConfiguration + subnetId + subnetIds + tags + title + version + vpcId + dBInstanceStatus + statement + instanceType + } + } + }`; + const variables = {pagination, resourceTypes, accounts}; + return sendQuery(opts, name, {query, variables}); +}; + +const getRelationships = opts => async ({pagination}) => { + const name = 'getRelationships'; + const query = ` + query ${name}($pagination: Pagination) { + getRelationships(pagination: $pagination) { + target { + id + label + } + id + label + source { + id + label + } + } +}`; + const variables = {pagination}; + return sendQuery(opts, name, {query, variables}); +}; + +const indexResources = opts => async resources => { + const name = 'indexResources'; + const query = ` + mutation ${name}($resources: [ResourceInput]!) { + ${name}(resources: $resources) { + unprocessedResources + } + }`; + const variables = {resources}; + return sendQuery(opts, name, {query, variables}); +}; + +const updateResources = opts => async resources => { + const name = 'updateResources'; + const query = ` + mutation ${name}($resources: [ResourceInput]!) { + ${name}(resources: $resources) { + id + } + }`; + const variables = {resources}; + return sendQuery(opts, name, {query, variables}); +}; + +const deleteRelationships = opts => async relationshipIds => { + const name = 'deleteRelationships'; + const query = ` + mutation ${name}($relationshipIds: [String]!) { + ${name}(relationshipIds: $relationshipIds) + }`; + const variables = {relationshipIds}; + return sendQuery(opts, name, {query, variables}); +}; + +const deleteResources = opts => async resourceIds => { + const name = 'deleteResources'; + const query = ` + mutation ${name}($resourceIds: [String]!) { + ${name}(resourceIds: $resourceIds) + }`; + const variables = {resourceIds}; + return sendQuery(opts, name, {query, variables}); +}; + +const deleteIndexedResources = opts => async resourceIds => { + const name = 'deleteIndexedResources'; + const query = ` + mutation ${name}($resourceIds: [String]!) { + ${name}(resourceIds: $resourceIds) { + unprocessedResources + } + }`; + const variables = {resourceIds}; + return sendQuery(opts, name, {query, variables}); +}; + +const updateIndexedResources = opts => async resources => { + const name = 'updateIndexedResources'; + const query = ` + mutation ${name}($resources: [ResourceInput]!) { + ${name}(resources: $resources) { + unprocessedResources + } + }`; + const variables = {resources}; + return sendQuery(opts, name, {query, variables}); +}; + +const addAccounts = opts => async accounts => { + const name = 'addAccounts'; + const query = ` + mutation ${name}($accounts: [AccountInput]!) { + addAccounts(accounts: $accounts) { + unprocessedAccounts + } + } +` + const variables = {accounts}; + return sendQuery(opts, name, {query, variables}); +} + +const updateAccount = opts => async (accountId, accountName, isIamRoleDeployed, lastCrawled, resourcesRegionMetadata) => { + const name = 'updateAccount'; + const query = ` + mutation ${name}($accountId: String!, $name: String, $isIamRoleDeployed: Boolean, $lastCrawled: AWSDateTime, $resourcesRegionMetadata: ResourcesRegionMetadataInput) { + ${name}(accountId: $accountId, name: $name, isIamRoleDeployed: $isIamRoleDeployed, lastCrawled: $lastCrawled, resourcesRegionMetadata: $resourcesRegionMetadata) { + accountId + lastCrawled + } + }`; + const variables = {accountId, name: accountName, lastCrawled, isIamRoleDeployed, resourcesRegionMetadata}; + return sendQuery(opts, name, {query, variables}); +}; + +const deleteAccounts = opts => async (accountIds) => { + const name = 'deleteAccounts'; + const query = ` + mutation ${name}($accountIds: [String]!) { + deleteAccounts(accountIds: $accountIds) { + unprocessedAccounts + } + }`; + const variables = {accountIds}; + return sendQuery(opts, name, {query, variables}); +}; + +export default function(config) { + const [host, path] = config.graphgQlUrl.replace('https://', '').split('/'); + + const opts = { + host, + path, + ...config + }; + + return { + addRelationships: addRelationships(opts), + addResources: addResources(opts), + deleteRelationships: deleteRelationships(opts), + deleteResources: deleteResources(opts), + indexResources: indexResources(opts), + addAccounts: addAccounts(opts), + deleteAccounts: deleteAccounts(opts), + getAccounts: getAccounts(opts), + updateAccount: updateAccount(opts), + updateResources: updateResources(opts), + deleteIndexedResources: deleteIndexedResources(opts), + updateIndexedResources: updateIndexedResources(opts), + getResources: getResources(opts), + getRelationships: getRelationships(opts), + createPaginator + }; +}; \ No newline at end of file diff --git a/source/backend/discovery/src/lib/apiClient/index.mjs b/source/backend/discovery/src/lib/apiClient/index.mjs new file mode 100644 index 00000000..901f8306 --- /dev/null +++ b/source/backend/discovery/src/lib/apiClient/index.mjs @@ -0,0 +1,245 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import {PromisePool, PromisePoolError} from '@supercharge/promise-pool'; +import {profileAsync, createArn} from '../utils.mjs'; +import {UnprocessedOpenSearchResourcesError} from '../errors.mjs' +import logger from '../logger.mjs'; +import {parse as parseArn} from '@aws-sdk/util-arn-parser'; +import { + ACCESS_DENIED, + IAM, + ROLE, + DISCOVERY_ROLE_NAME +} from '../constants.mjs'; + +function getDbResourcesMap(appSync) { + const {createPaginator, getResources} = appSync; + const getResourcesPaginator = createPaginator(getResources, 1000); + + return async () => { + const resourcesMap = new Map(); + + for await (const resources of getResourcesPaginator({})) { + resources.forEach(r => resourcesMap.set(r.id, { + id: r.id, + label: r.label, + md5Hash: r.md5Hash, + // gql will return `null` for missing properties which will break the hashing + // comparison for sdk discovered resources + properties: R.reject(R.isNil, r.properties) + })); + } + + return resourcesMap; + }; +} + +function getDbRelationshipsMap(appSync) { + const pageSize = 2500; + + function getDbRelationships(pagination, relationshipsMap= new Map()) { + return appSync.getRelationships({pagination}) + .then(relationships => { + if(R.isEmpty(relationships)) return relationshipsMap; + relationships.forEach(rel => { + const {id: source} = rel.source; + const {id: target} = rel.target; + const {label, id} = rel; + relationshipsMap.set(`${source}_${label}_${target}`, { + source, target, id, label + }) + }); + const {start, end} = pagination; + return getDbRelationships({start: start + pageSize, end: end + pageSize}, relationshipsMap); + }) + } + + return async () => getDbRelationships({start: 0, end: pageSize}); +} + +function process(processor) { + return async ({concurrency, batchSize}, resources) => { + const errors = []; + const {results} = await PromisePool + .withConcurrency(concurrency) + .for(R.splitEvery(batchSize, resources)) + .handleError(async (error, batch) => { + const failures = error instanceof UnprocessedOpenSearchResourcesError ? error.failures : batch; + errors.push(new PromisePoolError(error, failures)); + }) + .process(processor); + return {results, errors}; + } +} + +function createResourceProcessor(openSearchMutation, neptuneMutation, errorMsg) { + return async resources => { + const {unprocessedResources: unprocessedResourceArns} = await openSearchMutation(resources) + const unprocessedSet = new Set(unprocessedResourceArns); + const [unprocessedResources, processedResources] = R.partition(x => unprocessedSet.has(x.id ?? x), resources); + + await neptuneMutation(processedResources) + + if(!R.isEmpty(unprocessedResources)) { + logger.error(`${unprocessedResources.length} resources ${errorMsg}`, {unprocessedResources}); + throw new UnprocessedOpenSearchResourcesError(unprocessedResources); + } + } +} + +function updateCrawledAccounts(appSync) { + return async accounts => { + const {errors, results} = await PromisePool + .withConcurrency(10) // the reserved concurrency of the settings lambda is 10 + .for(accounts) + .process(async ({accountId, name, isIamRoleDeployed, lastCrawled, resourcesRegionMetadata}) => { + return appSync.updateAccount( + accountId, + name, + isIamRoleDeployed, isIamRoleDeployed ? new Date().toISOString() : lastCrawled, + resourcesRegionMetadata + ); + }); + + logger.error(`There were ${errors.length} errors when updating last crawled time for accounts.`); + logger.debug('Errors: ', {errors}); + + return {errors, results}; + } +} + +function addCrawledAccounts(appSync) { + return async accounts => { + return Promise.resolve(accounts) + // we must ensure we do not persist any temporary credentials to the db + .then(R.map(R.omit(['credentials', 'toDelete']))) + .then(R.map(({regions, isIamRoleDeployed, lastCrawled, ...props}) => { + return { + ...props, + isIamRoleDeployed, + regions: regions.map(name => ({name})), + lastCrawled: isIamRoleDeployed ? new Date().toISOString() : lastCrawled + } + })) + .then(appSync.addAccounts); + } +} + +async function getOrgAccounts( + {ec2Client, organizationsClient, configClient}, appSyncClient, {configAggregator, organizationUnitId} +) { + + const [dbAccounts, orgAccounts, {OrganizationAggregationSource}, regions] = await Promise.all([ + appSyncClient.getAccounts(), + organizationsClient.getAllActiveAccountsFromParent(organizationUnitId), + configClient.getConfigAggregator(configAggregator), + ec2Client.getAllRegions() + ]); + + logger.info(`Organization source info.`, {OrganizationAggregationSource}); + + const dbAccountsMap = new Map(dbAccounts.map(x => [x.accountId, x])); + + logger.info('Accounts from db.', {dbAccounts}); + + const orgAccountsMap = new Map(orgAccounts.map(x => [x.Id, x])); + + const deletedAccounts = dbAccounts.reduce((acc, account) => { + const {accountId} = account; + if(dbAccountsMap.has(accountId) && !orgAccountsMap.has(accountId)) { + acc.push({...account, toDelete: true}); + } + return acc; + }, []); + + return orgAccounts + .map(({Id, isManagementAccount, Name: name, Arn}) => { + const [, organizationId] = parseArn(Arn).resource.split('/'); + const lastCrawled = dbAccountsMap.get(Id)?.lastCrawled; + return { + accountId: Id, + organizationId, + name, + ...(isManagementAccount ? {isManagementAccount} : {}), + ...(lastCrawled != null ? {lastCrawled} : {}), + regions: OrganizationAggregationSource.AllAwsRegions + ? regions.map(x => x.name) : OrganizationAggregationSource.AwsRegions, + toDelete: dbAccountsMap.has(Id) && !orgAccountsMap.has(Id) + }; + }) + .concat(deletedAccounts); +} + +function createDiscoveryRoleArn(accountId, rootAccountId) { + return createArn({service: IAM, accountId, resource: `${ROLE}/${DISCOVERY_ROLE_NAME}-${rootAccountId}`}); +} + +const addAccountCredentials = R.curry(async ({stsClient}, rootAccountId, accounts) => { + const {errors, results} = await PromisePool + .withConcurrency(30) + .for(accounts) + .process(async ({accountId, organizationId, ...props}) => { + const roleArn = createDiscoveryRoleArn(accountId, rootAccountId); + const credentials = await stsClient.getCredentials(roleArn); + return { + ...props, + accountId, + isIamRoleDeployed: true, + ...(organizationId != null ? {organizationId} : {}), + credentials + }; + }); + + errors.forEach(({message, raw: error, item: {accountId, isManagementAccount}}) => { + const roleArn = createDiscoveryRoleArn(accountId, rootAccountId); + if (error.Code === ACCESS_DENIED) { + const errorMessage = `Access denied assuming role: ${roleArn}.`; + if(isManagementAccount) { + logger.error(`${errorMessage} This is the management account, ensure the global resources template has been deployed to the account.`); + } else { + logger.error(`${errorMessage} Ensure the global resources template has been deployed to account: ${accountId}. The discovery for this account will be skipped.`); + } + } else { + logger.error(`Error assuming role: ${roleArn}: ${message}`); + } + }); + + return [ + ...errors.filter(({raw}) => raw.Code === ACCESS_DENIED).map(({item}) => ({...item, isIamRoleDeployed: false})), + ...results, + ]; +}); + +export function createApiClient(awsClient, appSync, config) { + const ec2Client = awsClient.createEc2Client(); + const organizationsClient = awsClient.createOrganizationsClient(); + const configClient = awsClient.createConfigServiceClient(); + const stsClient = awsClient.createStsClient(); + + return { + getDbResourcesMap: profileAsync('Time to download resources from Neptune', getDbResourcesMap(appSync)), + getDbRelationshipsMap: profileAsync('Time to download relationships from Neptune', getDbRelationshipsMap(appSync)), + getAccounts: profileAsync('Time to get accounts', () => { + const accountsP = config.isUsingOrganizations + ? getOrgAccounts({ec2Client, organizationsClient, configClient}, appSync, config) + : appSync.getAccounts().then(R.map(R.evolve({regions: R.map(x => x.name)}))); + + return accountsP + .then(addAccountCredentials({stsClient}, config.rootAccountId)) + }), + addCrawledAccounts: addCrawledAccounts(appSync), + deleteAccounts: appSync.deleteAccounts, + storeResources: process(createResourceProcessor(appSync.indexResources, appSync.addResources, 'not written to OpenSearch')), + deleteResources: process(createResourceProcessor(appSync.deleteIndexedResources, appSync.deleteResources, 'not deleted from OpenSearch')), + updateResources: process(createResourceProcessor(appSync.updateIndexedResources, appSync.updateResources, 'not updated in OpenSearch')), + deleteRelationships: process(async ids => { + return appSync.deleteRelationships(ids); + }), + storeRelationships: process(async relationships => { + return appSync.addRelationships(relationships); + }), + updateCrawledAccounts: updateCrawledAccounts(appSync) + }; +} diff --git a/source/backend/discovery/src/lib/awsClient.mjs b/source/backend/discovery/src/lib/awsClient.mjs new file mode 100644 index 00000000..47a101b8 --- /dev/null +++ b/source/backend/discovery/src/lib/awsClient.mjs @@ -0,0 +1,771 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import logger from './logger.mjs'; +import pThrottle from 'p-throttle'; +import {ConfiguredRetryStrategy} from '@smithy/util-retry'; +import {customUserAgent} from './config.mjs'; +import { + ServiceCatalogAppRegistry, + ServiceCatalogAppRegistryClient, + paginateListApplications +} from '@aws-sdk/client-service-catalog-appregistry'; +import { + Organizations, + OrganizationsClient, + paginateListAccounts, + paginateListAccountsForParent, + paginateListOrganizationalUnitsForParent +} from "@aws-sdk/client-organizations"; +import {APIGateway, APIGatewayClient, paginateGetResources} from '@aws-sdk/client-api-gateway'; +import {AppSync} from '@aws-sdk/client-appsync'; +import {LambdaClient, paginateListFunctions, paginateListEventSourceMappings} from '@aws-sdk/client-lambda'; +import { + ECSClient, + ECS, + paginateListContainerInstances, + paginateListTasks +} from "@aws-sdk/client-ecs"; +import {EKSClient, EKS, paginateListNodegroups} from '@aws-sdk/client-eks'; +import { + EC2, + EC2Client, + paginateDescribeSpotInstanceRequests, + paginateDescribeSpotFleetRequests, + paginateDescribeTransitGatewayAttachments +} from '@aws-sdk/client-ec2' +import * as R from "ramda"; +import {ElasticLoadBalancing} from '@aws-sdk/client-elastic-load-balancing'; +import { + ElasticLoadBalancingV2, + ElasticLoadBalancingV2Client, + paginateDescribeTargetGroups +} from "@aws-sdk/client-elastic-load-balancing-v2"; +import {IAMClient, paginateListPolicies} from '@aws-sdk/client-iam'; +import {STS} from "@aws-sdk/client-sts"; +import {fromNodeProviderChain} from '@aws-sdk/credential-providers'; +import {AWS, OPENSEARCH, GLOBAL} from './constants.mjs'; +import { + ConfigServiceClient, + ConfigService, + paginateListAggregateDiscoveredResources, + paginateSelectAggregateResourceConfig, +} from '@aws-sdk/client-config-service'; +import { + MediaConnectClient, paginateListFlows +} from '@aws-sdk/client-mediaconnect'; +import { + OpenSearch +} from '@aws-sdk/client-opensearch'; +import { + DynamoDBStreams +} from '@aws-sdk/client-dynamodb-streams' +import {SNSClient, paginateListSubscriptions} from '@aws-sdk/client-sns'; +import {memoize} from './utils.mjs'; + +const RETRY_EXPONENTIAL_RATE = 2; + +// We want to share throttling limits across instances of clients so we memoize this +// function that each factory function calls to create its throttlers during +// instantiation. +const createThrottler = memoize((name, credentials, region, throttleParams) => { + return pThrottle(throttleParams); +}); + +export function throttledPaginator(throttler, paginator) { + const getPage = throttler(async () => paginator.next()); + + return (async function* () { + while(true) { + const {done, value} = await getPage(); + if(done) return {done}; + yield value; + } + })(); +} + +export function createServiceCatalogAppRegistryClient(credentials, region) { + const appRegistryClient = new ServiceCatalogAppRegistry({customUserAgent, region, credentials}); + + const paginatorConfig = { + pageSize: 20, + client: new ServiceCatalogAppRegistryClient({customUserAgent, region, credentials}) + }; + + const listApplicationsPaginatorThrottler = createThrottler('listApplicationsPaginated', credentials, region, { + limit: 5, + interval: 1000 + }); + + const getApplicationThrottler = createThrottler('getApplication', credentials, region, { + limit: 5, + interval: 1000 + }); + + const getApplication = getApplicationThrottler((application) => { + return appRegistryClient.getApplication({application}); + }); + + const listApplicationsPaginator = paginateListApplications(paginatorConfig, {}); + + return { + async getAllApplications() { + const applications = []; + + for await (const result of throttledPaginator(listApplicationsPaginatorThrottler, listApplicationsPaginator)) { + for(const {name} of result.applications) { + const application = await getApplication(name); + applications.push(application) + } + } + + return applications; + } + } +} + +export function createOrganizationsClient(credentials, region) { + const organizationsClient = new Organizations({customUserAgent, region, credentials}); + + const paginatorConfig = { + pageSize: 20, + client: new OrganizationsClient({customUserAgent, region, credentials}) + }; + + const getAllAccountsThrottler = createThrottler('getAllAccounts', credentials, region, { + limit: 1, + interval: 1000 + }); + + const getAllFromParentThrottler = createThrottler('getAllFromParent', credentials, region, { + limit: 1, + interval: 1000 + }); + + async function getAllAccounts() { + const listAccountsPaginator = paginateListAccounts(paginatorConfig, {}); + + const accounts = [] + + for await (const {Accounts} of throttledPaginator(getAllAccountsThrottler, listAccountsPaginator)) { + accounts.push(...Accounts); + } + + return accounts; + } + + async function getAllAccountsFromParent(ouId) { + const ouIds = [ouId]; + + // we will do these serially so as not to encounter rate limiting + for(const id of ouIds) { + const paginator = + throttledPaginator(getAllFromParentThrottler, paginateListOrganizationalUnitsForParent(paginatorConfig, {ParentId: id})); + for await (const {OrganizationalUnits} of paginator) { + ouIds.push(...OrganizationalUnits.map(x => x.Id)); + } + } + + const accounts = []; + + for(const id of ouIds) { + const paginator = + throttledPaginator(getAllFromParentThrottler, paginateListAccountsForParent(paginatorConfig, {ParentId: id})); + for await (const {Accounts} of paginator) { + accounts.push(...Accounts); + } + } + + return accounts; + } + + return { + async getAllActiveAccountsFromParent(ouId) { + const [{Roots}, {Organization}] = await Promise.all([ + organizationsClient.listRoots({}), + organizationsClient.describeOrganization({}) + ]); + const {Id: rootId} = Roots[0]; + const {MasterAccountId: managementAccountId} = Organization; + + const accounts = await (ouId === rootId ? getAllAccounts() : getAllAccountsFromParent(ouId)); + + const activeAccounts = accounts + .filter(account => account.Status === 'ACTIVE') + .map(account => { + if(account.Id === managementAccountId) { + account.isManagementAccount = true; + } + return account; + }); + + logger.info(`All active accounts from organization unit ${ouId} retrieved, ${activeAccounts.length} retrieved.`); + + return activeAccounts; + } + }; +} + +export function createOpenSearchClient(credentials, region) { + const OpenSearchClient = new OpenSearch({customUserAgent, region, credentials}); + + return { + async getAllOpenSearchDomains() { + const {DomainNames} = await OpenSearchClient.listDomainNames({EngineType: OPENSEARCH}); + + const domains = []; + + // The describeDomain API can only handle 5 domain names per request. Also, we send these + // requests serially to reduce the chance of any rate limiting. + for(const batch of R.splitEvery(5, DomainNames)) { + const {DomainStatusList} = await OpenSearchClient.describeDomains({DomainNames: batch.map(x => x.DomainName)}) + domains.push(...DomainStatusList); + } + + return domains; + } + }; +} + +export function createApiGatewayClient(credentials, region) { + const apiGatewayClient = new APIGateway({customUserAgent, region, credentials}); + + const apiGatewayPaginatorConfig = { + pageSize: 100, + client: new APIGatewayClient({customUserAgent, region, credentials}) + } + + // The API Gateway rate limits are _per account_ so we set the region to global + const getResourcesThrottler = createThrottler('apiGatewayGetResources', credentials, GLOBAL, { + limit: 5, + interval: 2000 + }); + + const totalOperationsThrottler = createThrottler('apiGatewayTotalOperations', credentials, GLOBAL, { + limit: 10, + interval: 1000 + }); + + return { + getResources: totalOperationsThrottler(getResourcesThrottler(async restApiId => { + const getResourcesPaginator = paginateGetResources(apiGatewayPaginatorConfig, {restApiId}); + + const apiResources = []; + for await (const {items} of getResourcesPaginator) { + apiResources.push(...items); + } + return apiResources; + })), + getMethod: totalOperationsThrottler(async (httpMethod, resourceId, restApiId) => { + return apiGatewayClient.getMethod({ + httpMethod, resourceId, restApiId + }); + }), + getAuthorizers: totalOperationsThrottler(async restApiId => { + return apiGatewayClient.getAuthorizers({restApiId}) + .then(R.prop('items')) + }) + }; +} + +export function createAppSyncClient(credentials, region) { + const appSyncClient = new AppSync({customUserAgent, credentials, region}); + const appSyncListThrottler = createThrottler('appSyncList', credentials, region, { + limit: 5, + interval: 1000 + }); + + const throttledListDataSources = appSyncListThrottler(({apiId, nextToken}) => appSyncClient.listDataSources({apiId, nextToken})); + const throttledListResolvers = appSyncListThrottler(({apiId, typeName, nextToken}) => appSyncClient.listResolvers({apiId, typeName, nextToken})); + + + return { + async listDataSources(apiId) { + const results = []; + + let nextToken = null; + do { + const {dataSources, nextToken: nt} = await throttledListDataSources({apiId, nextToken}) + results.push(...dataSources) + nextToken = nt + } while (nextToken != null) + + return results + }, + + async listResolvers(apiId, typeName){ + const results = []; + + let nextToken = null; + do { + const {resolvers, nextToken: nt} = await throttledListResolvers({apiId, typeName, nextToken}) + results.push(...resolvers) + nextToken = nt + } while (nextToken != null) + + return results + }, + } +} + +export function createConfigServiceClient(credentials, region) { + const configClient = new ConfigService({customUserAgent, credentials, region}); + + const paginatorConfig = { + client: new ConfigServiceClient({customUserAgent, credentials, region}), + pageSize: 100 + }; + + const selectAggregateResourceConfigThrottler = createThrottler( + 'selectAggregateResourceConfig', credentials, region, { + limit: 8, + interval: 1000 + } + ); + + const batchGetAggregateResourceConfigThrottler = createThrottler( + 'batchGetAggregateResourceConfig', credentials, region, { + limit: 15, + interval: 1000 + } + ); + + const batchGetAggregateResourceConfig = batchGetAggregateResourceConfigThrottler((ConfigurationAggregatorName, ResourceIdentifiers) => { + return configClient.batchGetAggregateResourceConfig({ConfigurationAggregatorName, ResourceIdentifiers}) + }) + + return { + async getConfigAggregator(aggregatorName) { + const {ConfigurationAggregators} = await configClient.describeConfigurationAggregators({ + ConfigurationAggregatorNames: [aggregatorName] + }); + return ConfigurationAggregators[0]; + }, + async getAllAggregatorResources(aggregatorName, {excludes: {resourceTypes: excludedResourceTypes = []}}) { + logger.info('Getting resources with advanced query'); + const excludedResourceTypesSqlList = excludedResourceTypes.map(rt => `'${rt}'`).join(','); + const excludesResourceTypesWhere = R.isEmpty(excludedResourceTypes) ? + '' : `WHERE resourceType NOT IN (${excludedResourceTypesSqlList})`; + + const Expression = `SELECT + *, + configuration, + configurationItemStatus, + relationships, + supplementaryConfiguration, + tags + ${excludesResourceTypesWhere} + ` + const MAX_RETRIES = 5; + + const paginator = paginateSelectAggregateResourceConfig({ + client: new ConfigServiceClient({ + customUserAgent, + credentials, + region, + // this code is a critical path so we use a lengthy exponential retry + // rate to give it as much chance to succeed in the face of any + // throttling errors: 0s -> 2s -> 6s -> 14s -> 30s -> Failure + retryStrategy: new ConfiguredRetryStrategy( + MAX_RETRIES, + attempt => 2000 * (RETRY_EXPONENTIAL_RATE ** attempt) + ) + }), + pageSize: 100 + }, { + ConfigurationAggregatorName: aggregatorName, Expression + }); + + const resources = [] + + for await (const page of throttledPaginator(selectAggregateResourceConfigThrottler, paginator)) { + resources.push(...R.map(JSON.parse, page.Results)); + } + + logger.info(`${resources.length} resources downloaded from Config advanced query`); + return resources; + }, + async getAggregatorResources(aggregatorName, resourceType) { + const resources = []; + + const paginator = paginateListAggregateDiscoveredResources(paginatorConfig,{ + ConfigurationAggregatorName: aggregatorName, + ResourceType: resourceType + }); + + for await (const {ResourceIdentifiers} of paginator) { + if(!R.isEmpty(ResourceIdentifiers)) { + const {BaseConfigurationItems} = await batchGetAggregateResourceConfig(aggregatorName, ResourceIdentifiers); + resources.push(...BaseConfigurationItems); + } + } + + return resources; + } + }; +} + +export function createLambdaClient(credentials, region) { + const lambdaPaginatorConfig = { + client: new LambdaClient({customUserAgent, region, credentials}), + pageSize: 100 + }; + + return { + async getAllFunctions() { + const functions = []; + const listFunctions = paginateListFunctions(lambdaPaginatorConfig, {}); + + for await (const {Functions} of listFunctions) { + functions.push(...Functions); + } + return functions; + }, + async listEventSourceMappings(arn) { + const mappings = []; + const listEventSourceMappingsPaginator = paginateListEventSourceMappings(lambdaPaginatorConfig, { + FunctionName: arn + }); + + for await (const {EventSourceMappings} of listEventSourceMappingsPaginator) { + mappings.push(...EventSourceMappings) + } + return mappings; + } + }; +} + +export function createEc2Client(credentials, region) { + const ec2Client = new EC2({customUserAgent, credentials, region}); + + const ec2PaginatorConfig = { + client: new EC2Client({customUserAgent, region, credentials}), + pageSize: 100 + }; + + return { + async getAllRegions() { + const { Regions } = await ec2Client.describeRegions({}); + return Regions.map(x => ({name: x.RegionName})); + }, + async getAllSpotInstanceRequests() { + const siPaginator = paginateDescribeSpotInstanceRequests(ec2PaginatorConfig, {}); + + const spotInstanceRequests = []; + for await (const {SpotInstanceRequests} of siPaginator) { + spotInstanceRequests.push(...SpotInstanceRequests); + } + return spotInstanceRequests; + }, + async getAllSpotFleetRequests() { + const sfPaginator = paginateDescribeSpotFleetRequests(ec2PaginatorConfig, {}); + + const spotFleetRequests = []; + + for await (const {SpotFleetRequestConfigs} of sfPaginator) { + spotFleetRequests.push(...SpotFleetRequestConfigs); + } + return spotFleetRequests; + }, + async getAllTransitGatewayAttachments(Filters) { + const paginator = paginateDescribeTransitGatewayAttachments(ec2PaginatorConfig, {Filters}); + const attachments = []; + for await (const {TransitGatewayAttachments} of paginator) { + attachments.push(...TransitGatewayAttachments); + } + return attachments; + } + } +} + +export function createEcsClient(credentials, region) { + const ecsClient = new ECS({customUserAgent, region, credentials}); + + const ecsPaginatorConfig = { + client: new ECSClient({customUserAgent, region, credentials}), + pageSize: 100 + }; + + // describeContainerInstances, describeTasks and listTasks share the same throttling bucket + const ecsClusterResourceReadThrottler = createThrottler('ecsClusterResourceReadThrottler', credentials, region, { + limit: 20, + interval: 1000 + }); + + const describeContainerInstances = ecsClusterResourceReadThrottler((cluster, containerInstances) => { + return ecsClient.describeContainerInstances({cluster, containerInstances}); + }) + + const describeTasks = ecsClusterResourceReadThrottler((cluster, tasks) => { + return ecsClient.describeTasks({cluster, tasks, include: ['TAGS']}); + }) + + return { + async getAllClusterInstances(clusterArn) { + const listContainerInstancesPaginator = paginateListContainerInstances(ecsPaginatorConfig, { + cluster: clusterArn + }); + + const instances = []; + + for await (const {containerInstanceArns} of throttledPaginator(ecsClusterResourceReadThrottler, listContainerInstancesPaginator)) { + if(!R.isEmpty(containerInstanceArns)) { + const {containerInstances} = await describeContainerInstances(clusterArn, containerInstanceArns); + instances.push(...containerInstances.map(x => x.ec2InstanceId)) + } + } + return instances; + }, + async getAllServiceTasks(cluster, serviceName) { + const serviceTasks = [] + const listTaskPaginator = paginateListTasks(ecsPaginatorConfig, { + cluster, serviceName + }); + + for await (const {taskArns} of throttledPaginator(ecsClusterResourceReadThrottler, listTaskPaginator)) { + if(!R.isEmpty(taskArns)) { + const {tasks} = await describeTasks(cluster, taskArns); + serviceTasks.push(...tasks); + } + } + + return serviceTasks; + }, + async getAllClusterTasks(cluster) { + const clusterTasks = [] + const listTaskPaginator = paginateListTasks(ecsPaginatorConfig, { + cluster, include: ['TAGS'] + }); + + for await (const {taskArns} of throttledPaginator(ecsClusterResourceReadThrottler, listTaskPaginator)) { + if(!R.isEmpty(taskArns)) { + const {tasks} = await describeTasks(cluster, taskArns); + clusterTasks.push(...tasks); + } + } + + return clusterTasks; + } + }; +} + +export function createEksClient(credentials, region) { + const eksClient = new EKS({customUserAgent, region, credentials}); + + const eksPaginatorConfig = { + client: new EKSClient({customUserAgent, region, credentials}), + pageSize: 100 + }; + // this API only has a TPS of 10 so we set it artificially low to avoid rate limiting + const describeNodegroupThrottler = createThrottler('eksDescribeNodegroup', credentials, region, { + limit: 5, + interval: 1000 + }); + + return { + async listNodeGroups(clusterName) { + const ngs = []; + const listNodegroupsPaginator = paginateListNodegroups(eksPaginatorConfig, { + clusterName + }); + + for await (const {nodegroups} of listNodegroupsPaginator) { + const result = await Promise.all(nodegroups.map(describeNodegroupThrottler(async nodegroupName => { + const {nodegroup} = await eksClient.describeNodegroup({ + nodegroupName, clusterName + }); + return nodegroup; + }))); + ngs.push(...result); + } + + return ngs; + } + } + +} + +export function createElbClient(credentials, region) { + const elbClient = new ElasticLoadBalancing({customUserAgent, credentials, region}); + + // ELB rate limits for describe* calls are shared amongst all LB types + const elbDescribeThrottler = createThrottler('elbDescribe', credentials, region, { + limit: 10, + interval: 1000 + }); + + return { + getLoadBalancerInstances: elbDescribeThrottler(async resourceId => { + const lb = await elbClient.describeLoadBalancers({ + LoadBalancerNames: [resourceId], + }); + + const instances = lb.LoadBalancerDescriptions[0]?.Instances ?? []; + + return instances.map(x => x.InstanceId); + }) + }; +} + +export function createElbV2Client(credentials, region) { + const elbClientV2 = new ElasticLoadBalancingV2({customUserAgent, credentials, region}); + const elbV2PaginatorConfig = { + client: new ElasticLoadBalancingV2Client({customUserAgent, region, credentials}), + pageSize: 100 + }; + + // ELB rate limits for describe* calls are shared amongst all LB types + const elbDescribeThrottler = createThrottler('elbDescribe', credentials, region, { + limit: 10, + interval: 1000 + }); + + return { + describeTargetHealth: elbDescribeThrottler(async arn => { + const {TargetHealthDescriptions = []} = await elbClientV2.describeTargetHealth({ + TargetGroupArn: arn + }); + return TargetHealthDescriptions; + }), + getAllTargetGroups: elbDescribeThrottler(async () => { + const tgPaginator = paginateDescribeTargetGroups(elbV2PaginatorConfig, {}); + + const targetGroups = []; + for await (const {TargetGroups} of tgPaginator) { + targetGroups.push(...TargetGroups); + } + + return targetGroups; + }), + }; +} + +export function createIamClient(credentials, region) { + const iamPaginatorConfig = { + client: new IAMClient({customUserAgent, region, credentials}), + pageSize: 100 + }; + + return { + async getAllAttachedAwsManagedPolices() { + const listPoliciesPaginator = paginateListPolicies(iamPaginatorConfig, { + Scope: AWS.toUpperCase(), OnlyAttached: true}); + + const managedPolices = []; + for await (const {Policies} of listPoliciesPaginator) { + managedPolices.push(...Policies); + } + + return managedPolices; + } + }; +} + +export function createMediaConnectClient(credentials, region) { + const listFlowsPaginatorConfig = { + client: new MediaConnectClient({customUserAgent, credentials, region}), + pageSize: 20 + } + + const listFlowsPaginatorThrottler = createThrottler('mediaConnectListThrottler', credentials, region, { + limit: 5, + interval: 1000 + }); + + return { + async getAllFlows() { + const listFlowsPaginator = paginateListFlows(listFlowsPaginatorConfig, {}); + + const flows = []; + + for await (const {Flows} of throttledPaginator(listFlowsPaginatorThrottler, listFlowsPaginator)) { + flows.push(...Flows); + } + + return flows; + } + }; +} + +export function createSnsClient(credentials, region) { + const snsPaginatorConfig = { + client: new SNSClient({customUserAgent, credentials, region}), + pageSize: 100 + } + + return { + async getAllSubscriptions() { + const listSubscriptionsPaginator = paginateListSubscriptions(snsPaginatorConfig, {}); + + const subscriptions = []; + for await (const {Subscriptions} of listSubscriptionsPaginator) { + subscriptions.push(...Subscriptions); + } + + return subscriptions; + } + } +} + +export function createStsClient(credentials, region) { + const params = (credentials == null && region == null) ? {} : {credentials, region} + const sts = new STS({...params, customUserAgent}); + + const CredentialsProvider = fromNodeProviderChain(); + + return { + async getCredentials(RoleArn) { + const {Credentials} = await sts.assumeRole({ + RoleArn, + RoleSessionName: 'discovery' + } + ); + + return {accessKeyId: Credentials.AccessKeyId, secretAccessKey: Credentials.SecretAccessKey, sessionToken: Credentials.SessionToken}; + }, + async getCurrentCredentials() { + return CredentialsProvider(); + } + }; +} + +export function createDynamoDBStreamsClient(credentials, region) { + const dynamoDBStreamsClient = new DynamoDBStreams({customUserAgent, region, credentials}); + + // this API only has a TPS of 10 so we set it artificially low to avoid rate limiting + const describeStreamThrottler = createThrottler('dynamoDbDescribeStream', credentials, region, { + limit: 8, + interval: 1000 + }); + + const describeStream = describeStreamThrottler(streamArn => dynamoDBStreamsClient.describeStream({StreamArn: streamArn})); + + return { + async describeStream(streamArn) { + const {StreamDescription} = await describeStream(streamArn); + return StreamDescription; + } + } +} + +export function createAwsClient() { + return { + createServiceCatalogAppRegistryClient, + createOrganizationsClient, + createApiGatewayClient, + createAppSyncClient, + createConfigServiceClient, + createDynamoDBStreamsClient, + createEc2Client, + createEcsClient, + createEksClient, + createLambdaClient, + createElbClient, + createElbV2Client, + createIamClient, + createMediaConnectClient, + createStsClient, + createOpenSearchClient, + createSnsClient + } +}; \ No newline at end of file diff --git a/source/backend/discovery/src/lib/config.mjs b/source/backend/discovery/src/lib/config.mjs new file mode 100644 index 00000000..b6dbe72c --- /dev/null +++ b/source/backend/discovery/src/lib/config.mjs @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AWS_ORGANIZATIONS } from "./constants.mjs"; + +export const cluster = process.env.CLUSTER; +export const configAggregator = process.env.CONFIG_AGGREGATOR; +export const crossAccountDiscovery = process.env.CROSS_ACCOUNT_DISCOVERY; +export const customUserAgent = process.env.CUSTOM_USER_AGENT; +export const graphgQlUrl = process.env.GRAPHQL_API_URL; +export const isUsingOrganizations = process.env.CROSS_ACCOUNT_DISCOVERY === AWS_ORGANIZATIONS; +export const organizationUnitId = process.env.ORGANIZATION_UNIT_ID; +export const region = process.env.AWS_REGION; +export const rootAccountId = process.env.AWS_ACCOUNT_ID; +export const rootAccountRole = process.env.DISCOVERY_ROLE; diff --git a/source/backend/discovery/src/lib/constants.mjs b/source/backend/discovery/src/lib/constants.mjs new file mode 100644 index 00000000..ce1180df --- /dev/null +++ b/source/backend/discovery/src/lib/constants.mjs @@ -0,0 +1,164 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const IS_ASSOCIATED_WITH = 'Is associated with '; +export const CONTAINS = 'Contains '; +export const IS_CONTAINED_IN = 'Is contained in '; +export const IS_ATTACHED_TO = 'Is attached to '; +export const ACCESS_DENIED = 'AccessDenied'; +export const AWS = 'aws'; +export const AWS_API_GATEWAY_AUTHORIZER = 'AWS::ApiGateway::Authorizer'; +export const AWS_API_GATEWAY_METHOD = 'AWS::ApiGateway::Method'; +export const AWS_API_GATEWAY_REST_API = 'AWS::ApiGateway::RestApi'; +export const AWS_API_GATEWAY_RESOURCE = 'AWS::ApiGateway::Resource'; +export const AWS_APPSYNC_GRAPHQLAPI = 'AWS::AppSync::GraphQLApi'; +export const AWS_APPSYNC_DATASOURCE = 'AWS::AppSync::DataSource'; +export const AWS_APPSYNC_RESOLVER = 'AWS::AppSync::Resolver'; +export const AWS_CLOUDFORMATION_STACK = 'AWS::CloudFormation::Stack'; +export const AWS_CLOUDFRONT_DISTRIBUTION = 'AWS::CloudFront::Distribution'; +export const AWS_CLOUDFRONT_STREAMING_DISTRIBUTION = 'AWS::CloudFront::StreamingDistribution'; +export const AWS_COGNITO_USER_POOL = 'AWS::Cognito::UserPool'; +export const AWS_CONFIG_RESOURCE_COMPLIANCE = 'AWS::Config::ResourceCompliance'; +export const AWS_DYNAMODB_STREAM = 'AWS::DynamoDB::Stream'; +export const AWS_DYNAMODB_TABLE = 'AWS::DynamoDB::Table'; +export const AWS_EC2_INSTANCE = 'AWS::EC2::Instance'; +export const AWS_EC2_INTERNET_GATEWAY = 'AWS::EC2::InternetGateway'; +export const AWS_EC2_LAUNCH_TEMPLATE = 'AWS::EC2::LaunchTemplate'; +export const AWS_EC2_NAT_GATEWAY = 'AWS::EC2::NatGateway'; +export const AWS_EC2_NETWORK_ACL = 'AWS::EC2::NetworkAcl'; +export const AWS_EC2_NETWORK_INTERFACE = 'AWS::EC2::NetworkInterface'; +export const AWS_EC2_ROUTE_TABLE = 'AWS::EC2::RouteTable'; +export const AWS_EC2_SPOT = 'AWS::EC2::Spot'; +export const AWS_EC2_SPOT_FLEET = 'AWS::EC2::SpotFleet'; +export const AWS_EC2_SUBNET = 'AWS::EC2::Subnet'; +export const AWS_EC2_SECURITY_GROUP = 'AWS::EC2::SecurityGroup'; +export const AWS_EC2_TRANSIT_GATEWAY = 'AWS::EC2::TransitGateway'; +export const AWS_EC2_TRANSIT_GATEWAY_ATTACHMENT = 'AWS::EC2::TransitGatewayAttachment'; +export const AWS_EC2_TRANSIT_GATEWAY_ROUTE_TABLE = 'AWS::EC2::TransitGatewayRouteTable'; +export const AWS_EC2_VOLUME = 'AWS::EC2::Volume'; +export const AWS_EC2_VPC = 'AWS::EC2::VPC'; +export const AWS_EC2_VPC_ENDPOINT = 'AWS::EC2::VPCEndpoint'; +export const AWS_ECR_REPOSITORY = 'AWS::ECR::Repository'; +export const AWS_ECS_CLUSTER = 'AWS::ECS::Cluster'; +export const AWS_ECS_SERVICE = 'AWS::ECS::Service'; +export const AWS_ECS_TASK = 'AWS::ECS::Task'; +export const AWS_ECS_TASK_DEFINITION = 'AWS::ECS::TaskDefinition'; +export const AWS_ELASTICSEARCH_DOMAIN = 'AWS::Elasticsearch::Domain'; +export const AWS_EVENT_EVENT_BUS = 'AWS::Events::EventBus'; +export const AWS_EVENT_RULE = 'AWS::Events::Rule'; +export const AWS_KMS_KEY = 'AWS::KMS::Key'; +export const AWS_OPENSEARCH_DOMAIN = 'AWS::OpenSearch::Domain'; +export const AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER = 'AWS::ElasticLoadBalancing::LoadBalancer'; +export const AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER = 'AWS::ElasticLoadBalancingV2::LoadBalancer'; +export const AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP = 'AWS::ElasticLoadBalancingV2::TargetGroup'; +export const AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER = 'AWS::ElasticLoadBalancingV2::Listener'; +export const AWS_LAMBDA_FUNCTION = 'AWS::Lambda::Function'; +export const AWS_RDS_DB_SUBNET_GROUP = 'AWS::RDS::DBSubnetGroup'; +export const AWS_RDS_DB_CLUSTER = 'AWS::RDS::DBCluster'; +export const AWS_RDS_DB_INSTANCE = 'AWS::RDS::DBInstance'; +export const AWS_IAM_GROUP = 'AWS::IAM::Group'; +export const AWS_IAM_ROLE = 'AWS::IAM::Role'; +export const AWS_IAM_USER = 'AWS::IAM::User'; +export const AWS_IAM_AWS_MANAGED_POLICY = 'AWS::IAM::AWSManagedPolicy'; +export const AWS_IAM_INLINE_POLICY = 'AWS::IAM::InlinePolicy'; +export const AWS_IAM_INSTANCE_PROFILE = 'AWS::IAM::InstanceProfile'; +export const AWS_IAM_POLICY = 'AWS::IAM::Policy'; +export const AWS_CODEBUILD_PROJECT = 'AWS::CodeBuild::Project'; +export const AWS_CODE_PIPELINE_PIPELINE = 'AWS::CodePipeline::Pipeline'; +export const AWS_EC2_EIP = 'AWS::EC2::EIP'; +export const AWS_EFS_FILE_SYSTEM = 'AWS::EFS::FileSystem'; +export const AWS_EFS_ACCESS_POINT = 'AWS::EFS::AccessPoint'; +export const AWS_ELASTIC_BEANSTALK_APPLICATION_VERSION = 'AWS::ElasticBeanstalk::ApplicationVersion'; +export const AWS_EKS_CLUSTER = 'AWS::EKS::Cluster'; +export const AWS_EKS_NODE_GROUP = 'AWS::EKS::Nodegroup'; +export const AWS_AUTOSCALING_AUTOSCALING_GROUP = 'AWS::AutoScaling::AutoScalingGroup'; +export const AWS_AUTOSCALING_SCALING_POLICY = 'AWS::AutoScaling::ScalingPolicy'; +export const AWS_AUTOSCALING_LAUNCH_CONFIGURATION = 'AWS::AutoScaling::LaunchConfiguration'; +export const AWS_AUTOSCALING_WARM_POOL = 'AWS::AutoScaling::WarmPool'; +export const AWS_KINESIS_STREAM = 'AWS::Kinesis::Stream'; +export const AWS_MEDIA_CONNECT_FLOW = 'AWS::MediaConnect::Flow'; +export const AWS_MEDIA_CONNECT_FLOW_ENTITLEMENT = 'AWS::MediaConnect::FlowEntitlement'; +export const AWS_MEDIA_CONNECT_FLOW_SOURCE = 'AWS::MediaConnect::FlowSource'; +export const AWS_MEDIA_CONNECT_FLOW_VPC_INTERFACE = 'AWS::MediaConnect::FlowVpcInterface'; +export const AWS_MEDIA_PACKAGE_PACKAGING_CONFIGURATION = 'AWS::MediaPackage::PackagingConfiguration'; +export const AWS_MEDIA_PACKAGE_PACKAGING_GROUP = 'AWS::MediaPackage::PackagingGroup'; +export const AWS_MEDIA_TAILOR_FLOW_ENTITLEMENT = 'AWS::MediaTailor::PlaybackConfiguration'; +export const AWS_MSK_CLUSTER = 'AWS::MSK::Cluster'; +export const AWS_REDSHIFT_CLUSTER = 'AWS::Redshift::Cluster'; +export const AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION = 'AWS::ServiceCatalogAppRegistry::Application'; +export const AWS_S3_BUCKET = 'AWS::S3::Bucket'; +export const AWS_S3_ACCOUNT_PUBLIC_ACCESS_BLOCK = 'AWS::S3::AccountPublicAccessBlock'; +export const AWS_SNS_TOPIC = 'AWS::SNS::Topic'; +export const AWS_SQS_QUEUE = 'AWS::SQS::Queue'; +export const AWS_SSM_MANAGED_INSTANCE_INVENTORY = 'AWS::SSM::ManagedInstanceInventory'; +export const AWS_TAGS_TAG = 'AWS::Tags::Tag'; +export const APPLICATION_TAG_NAME = 'awsApplication'; +export const AWS_ORGANIZATIONS = 'AWS_ORGANIZATIONS'; +export const DISCOVERY_ROLE_NAME = 'WorkloadDiscoveryRole'; +export const ECS = 'ecs'; +export const ELASTIC_LOAD_BALANCING = 'elasticloadbalancing'; +export const LOAD_BALANCER = 'loadbalancer'; +export const ENI_NAT_GATEWAY_INTERFACE_TYPE = 'nat_gateway'; +export const ENI_ALB_DESCRIPTION_PREFIX = 'ELB app'; +export const ENI_ELB_DESCRIPTION_PREFIX = 'ELB '; +export const ENI_VPC_ENDPOINT_INTERFACE_TYPE = 'vpc_endpoint'; +export const ENI_SEARCH_DESCRIPTION_PREFIX = 'ES '; // this value is the same for both Opensearch and ES ENIs +export const ENI_SEARCH_REQUESTER_ID = 'amazon-elasticsearch'; // this value is the same for both Opensearch and ES ENIs +export const IAM = 'iam'; +export const ROLE = 'role'; +export const LAMBDA = 'lambda'; +export const GLOBAL = 'global'; +export const REGION = 'region'; +export const REGIONAL = 'regional'; +export const NETWORK_INTERFACE = 'NetworkInterface'; +export const NETWORK_INTERFACE_ID = 'networkInterfaceId'; +export const NOT_APPLICABLE = 'Not Applicable'; +export const MULTIPLE_AVAILABILITY_ZONES = 'Multiple Availability Zones'; +export const SPOT_FLEET_REQUEST_ID_TAG = 'aws:ec2spot:fleet-request-id'; +export const SUBNET_ID = 'subnetId'; +export const GET = 'GET'; +export const POST = 'POST'; +export const PUT = 'PUT'; +export const DELETE = 'DELETE'; +export const SUBNET = 'Subnet'; +export const OPENSEARCH = 'OpenSearch'; +export const SECURITY_GROUP = 'SecurityGroup'; +export const RESOURCE_DISCOVERED = 'ResourceDiscovered'; +export const RESOURCE_NOT_RECORDED = 'ResourceNotRecorded'; +export const EC2 = 'ec2'; +export const SPOT_FLEET_REQUEST = 'spot-fleet-request'; +export const SPOT_INSTANCE_REQUEST = 'spot-instance-request'; +export const INLINE_POLICY = 'inlinePolicy'; +export const TAG = 'tag'; +export const TAGS = 'tags'; +export const VPC = 'Vpc'; +export const APIGATEWAY = 'apigateway'; +export const RESTAPIS = 'restapis'; +export const RESOURCES = 'resources'; +export const METHODS = 'methods'; +export const AUTHORIZERS = 'authorizers'; +export const EVENTS = 'events'; +export const EVENT_BUS = 'event-bus'; +export const NAME = 'Name'; +export const NOT_FOUND_EXCEPTION = 'NotFoundException'; +export const CN_NORTH_1 = 'cn-north-1'; +export const CN_NORTHWEST_1 = 'cn-northwest-1'; +export const US_GOV_EAST_1 = 'us-gov-east-1'; +export const US_GOV_WEST_1 = 'us-gov-west-1'; +export const AWS_CN = 'aws-cn'; +export const AWS_US_GOV = 'aws-us-gov'; +export const CONNECTION_CLOSED_PREMATURELY = 'Connection closed prematurely'; +export const RESOLVER_CODE_SIZE_ERROR = 'Reached evaluated resolver code size limit.'; +export const PERSPECTIVE = 'perspective'; +export const TASK_DEFINITION = 'task-definition'; +export const TRANSIT_GATEWAY_ATTACHMENT = 'transit-gateway-attachment'; +export const UNKNOWN = 'unknown'; +export const DISCOVERY_PROCESS_RUNNING = 'Discovery process ECS task is already running in cluster.'; +export const CONSOLE = 'console'; +export const SIGN_IN = 'signin'; +export const AWS_AMAZON_COM = 'aws.amazon.com'; +export const S3 = 's3'; +export const HOME = 'home'; +export const FULFILLED = 'fulfilled'; +export const FUNCTION_RESPONSE_SIZE_TOO_LARGE = 'Response payload size exceeded maximum allowed payload size (6291556 bytes).'; +export const WORKLOAD_DISCOVERY_TASKGROUP = 'workload-discovery-taskgroup'; diff --git a/source/backend/discovery/src/lib/createResourceAndRelationshipDeltas.mjs b/source/backend/discovery/src/lib/createResourceAndRelationshipDeltas.mjs new file mode 100644 index 00000000..3081acab --- /dev/null +++ b/source/backend/discovery/src/lib/createResourceAndRelationshipDeltas.mjs @@ -0,0 +1,180 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import {iterate} from 'iterare'; +import { + GLOBAL, + AWS_IAM_AWS_MANAGED_POLICY, + AWS, + AWS_IAM_INLINE_POLICY, + AWS_IAM_USER, + AWS_IAM_ROLE, + AWS_IAM_POLICY, + AWS_IAM_GROUP, + AWS_TAGS_TAG, + UNKNOWN +} from './constants.mjs'; +import {resourceTypesToHash} from './utils.mjs'; + +function createLookUpMaps(resources) { + const resourcesMap = new Map(); + const resourceIdentifierToIdMap = new Map(); + + for(let resource of resources) { + const {id, resourceId, resourceType, resourceName, accountId, awsRegion} = resource; + + if(resourceName != null) { + resourceIdentifierToIdMap.set( + createResourceNameKey({resourceType, resourceName, accountId, awsRegion}), + id); + } + resourceIdentifierToIdMap.set( + createResourceIdKey({resourceType, resourceId, accountId, awsRegion}), + id); + + resourcesMap.set(id, resource); + } + + return { + resourcesMap, + resourceIdentifierToIdMap + } +} + +function createResourceNameKey({resourceName, resourceType, accountId, awsRegion}) { + return `${resourceType}_${resourceName}_${accountId}_${awsRegion}`; +} + +function createResourceIdKey({resourceId, resourceType, accountId, awsRegion}) { + return `${resourceType}_${resourceId}_${accountId}_${awsRegion}`; +} + +const globalResourceTypes = new Set([ + AWS_IAM_INLINE_POLICY, + AWS_IAM_USER, + AWS_IAM_ROLE, + AWS_IAM_POLICY, + AWS_IAM_GROUP, + AWS_IAM_AWS_MANAGED_POLICY +]); + +function isGlobalResourceType(resourceType) { + return globalResourceTypes.has(resourceType); +} + +const createLinksFromRelationships = R.curry((resourceIdentifierToIdMap, resourcesMap, resource) => { + const {id: source, accountId: sourceAccountId, awsRegion: sourceRegion, relationships = []} = resource; + + return relationships.map(({arn, resourceId, resourceType, resourceName, relationshipName, awsRegion: targetRegion, accountId: targetAccountId}) => { + const awsRegion = targetRegion ?? (isGlobalResourceType(resourceType) ? GLOBAL : sourceRegion); + const accountId = resourceType === AWS_IAM_AWS_MANAGED_POLICY ? AWS : (targetAccountId ?? sourceAccountId); + + const findId = arn ?? (resourceId == null ? + resourceIdentifierToIdMap.get(createResourceNameKey({resourceType, resourceName, accountId, awsRegion})) : + resourceIdentifierToIdMap.get(createResourceIdKey({resourceType, resourceId, accountId, awsRegion}))); + const {id: target} = resourcesMap.get(findId) ?? {id: UNKNOWN}; + + return { + source, + target, + label: relationshipName.trim().toUpperCase().replace(/ /g, '_') + } + }); +}); + +function getLinkChanges(configLinks, dbLinks) { + const linksToAdd = iterate(configLinks.values()) + .filter(({source, label, target}) => target !== UNKNOWN && !dbLinks.has(`${source}_${label}_${target}`)) + .toArray(); + + const linksToDelete = iterate(dbLinks.values()) + .filter(({source, label, target}) => target !== UNKNOWN && !configLinks.has(`${source}_${label}_${target}`)) + .map(x => x.id) + .toArray(); + + return {linksToAdd, linksToDelete}; +} + +function createUpdate(dbResourcesMap) { + return ({id, md5Hash, properties}) => { + const {properties: dbProperties} = dbResourcesMap.get(id); + return { + id, + md5Hash, + properties: Object.entries(properties).reduce((acc, [k, v]) => { + if(dbProperties[k] !== v) acc[k] = v; + return acc; + }, {}) + } + } +} + +function createStore({id, resourceType, md5Hash, properties}) { + return { + id, + md5Hash, + label: resourceType.replace(/::/g, "_"), + properties + } +} + +function getResourceChanges(resourcesMap, dbResourcesMap) { + const resources = Array.from(resourcesMap.values()); + const dbResources = Array.from(dbResourcesMap.values()); + + const resourcesToStore = iterate(resources) + .filter(x => !dbResourcesMap.has(x.id)) + .map(createStore) + .toArray(); + + const resourcesToUpdate = iterate(resources) + .filter(resource => { + const {id} = resource; + if(!dbResourcesMap.has(id)) return false; + + const dbResource = dbResourcesMap.get(id); + if(resourceTypesToHash.has(resource.resourceType)) { + return resource.md5Hash !== dbResource.md5Hash; + } + + // we previously did not ingest the supplementaryConfiguration field so cannot rely on the + // AWS Config configurationItemCaptureTime timestamp to ascertain if a resource has changed + if(dbResource.properties.supplementaryConfiguration == null && resource.properties.supplementaryConfiguration != null) { + return true; + } + + return resource.resourceType !== AWS_TAGS_TAG && resource.properties.configurationItemCaptureTime !== dbResource.properties.configurationItemCaptureTime; + }) + .map(createUpdate(dbResourcesMap)) + .toArray(); + + const resourceIdsToDelete = iterate(dbResources) + .filter(x => !resourcesMap.has(x.id)) + .map(x => x.id) + .toArray(); + + return { + resourcesToStore, + resourceIdsToDelete, + resourcesToUpdate + } +} + +function createResourceAndRelationshipDeltas(dbResourcesMap, dbLinksMap, resources) { + const {resourceIdentifierToIdMap, resourcesMap} = createLookUpMaps(resources); + + const links = resources.flatMap(createLinksFromRelationships(resourceIdentifierToIdMap, resourcesMap)); + const configLinksMap = new Map(links.map(x => [`${x.source}_${x.label}_${x.target}`, x])); + + const {linksToAdd, linksToDelete} = getLinkChanges(configLinksMap, dbLinksMap); + + const {resourceIdsToDelete, resourcesToStore, resourcesToUpdate} = getResourceChanges(resourcesMap, dbResourcesMap); + + return { + resourceIdsToDelete, resourcesToStore, resourcesToUpdate, + linksToAdd, linksToDelete + } +} + +export default R.curry(createResourceAndRelationshipDeltas); diff --git a/source/backend/discovery/src/lib/errors.mjs b/source/backend/discovery/src/lib/errors.mjs new file mode 100644 index 00000000..5ac2c55f --- /dev/null +++ b/source/backend/discovery/src/lib/errors.mjs @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export class UnprocessedOpenSearchResourcesError extends Error { + constructor(failures) { + super('Error processing resources.'); + this.name = 'UnprocessedOpenSearchResourcesError'; + this.failures = failures; + } +} + +export class AggregatorNotFoundError extends Error { + constructor(aggregatorName) { + super(`Aggregator ${aggregatorName} was not found`); + this.name = 'AggregatorValidationError'; + this.aggregatorName = aggregatorName; + } +} + +export class OrgAggregatorValidationError extends Error { + constructor(aggregator) { + super('Config aggregator is not an organization wide aggregator'); + this.name = 'AggregatorValidationError'; + this.aggregator = aggregator; + } +} diff --git a/source/backend/discovery/src/lib/index.mjs b/source/backend/discovery/src/lib/index.mjs new file mode 100644 index 00000000..eae7df87 --- /dev/null +++ b/source/backend/discovery/src/lib/index.mjs @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import logger from './logger.mjs'; +import {initialise} from './intialisation.mjs'; +import getAllConfigResources from './aggregator/getAllConfigResources.mjs'; +import {getAllSdkResources} from './sdkResources/index.mjs'; +import {addAdditionalRelationships} from './additionalRelationships/index.mjs'; +import createResourceAndRelationshipDeltas from './createResourceAndRelationshipDeltas.mjs'; +import {createSaveObject, createResourcesRegionMetadata} from './persistence/transformers.mjs'; +import {persistResourcesAndRelationships, persistAccounts, processPersistenceFailures} from './persistence/index.mjs'; +import {GLOBAL, RESOURCE_NOT_RECORDED} from "./constants.mjs"; + +const shouldDiscoverResource = R.curry((accountsMap, resource) => { + const {accountId, awsRegion, configurationItemStatus} = resource; + + if(configurationItemStatus === RESOURCE_NOT_RECORDED) { + return false; + } + // resources from removed accounts/regions can take a while to be deleted from the Config aggregator + const regions = accountsMap.get(accountId)?.regions ?? []; + return (accountsMap.has(accountId) && awsRegion === GLOBAL) || regions.includes(awsRegion); +}); + +export async function discoverResources(appSync, awsClient, config) { + logger.info('Beginning discovery of resources'); + const {apiClient, configServiceClient} = await initialise(awsClient, appSync, config); + + const [accounts, dbLinksMap, dbResourcesMap, configResources] = await Promise.all([ + apiClient.getAccounts(), + apiClient.getDbRelationshipsMap(), + apiClient.getDbResourcesMap(), + getAllConfigResources(configServiceClient, config.configAggregator) + ]); + + const accountsMap = new Map(accounts.filter(x => x.isIamRoleDeployed && !x.toDelete).map(x => [x.accountId, x])); + + const resources = await Promise.resolve(configResources) + .then(R.filter(shouldDiscoverResource(accountsMap))) + .then(getAllSdkResources(accountsMap, awsClient)) + .then(addAdditionalRelationships(accountsMap, awsClient)) + .then(R.map(createSaveObject)); + + return Promise.resolve(resources) + .then(createResourceAndRelationshipDeltas(dbResourcesMap, dbLinksMap)) + .then(persistResourcesAndRelationships(apiClient)) + .then(processPersistenceFailures(dbResourcesMap, resources)) + .then(createResourcesRegionMetadata) + .then(persistAccounts(config, apiClient, accounts)); +} diff --git a/source/backend/discovery/src/lib/intialisation.mjs b/source/backend/discovery/src/lib/intialisation.mjs new file mode 100644 index 00000000..1209e385 --- /dev/null +++ b/source/backend/discovery/src/lib/intialisation.mjs @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import logger from './logger.mjs'; +import {createApiClient} from "./apiClient/index.mjs"; +import {AggregatorNotFoundError, OrgAggregatorValidationError} from './errors.mjs'; +import { + AWS_ORGANIZATIONS, + ECS, + WORKLOAD_DISCOVERY_TASKGROUP, + TASK_DEFINITION, + DISCOVERY_PROCESS_RUNNING, +} from './constants.mjs' +import {createArn, profileAsync} from './utils.mjs'; + +async function isDiscoveryEcsTaskRunning (ecsClient, taskDefinitionArn, {cluster}) { + const tasks = await ecsClient.getAllClusterTasks(cluster) + .then(R.filter(task => { + // The number after the last colon in the ARN is the version of the task definition. We strip it out + // as we can't know what number it will be. Furthermore, it's not relevant as we just need to know if + // there's another discovery task potentially writing to the DB. + return task.taskDefinitionArn.slice(0, task.taskDefinitionArn.lastIndexOf(':')) === taskDefinitionArn; + })); + + logger.debug('Discovery ECS tasks currently running:', {tasks}); + + return tasks.length > 1; +} + +async function validateOrgAggregator(configServiceClient, aggregatorName) { + return configServiceClient.getConfigAggregator(aggregatorName) + .catch(err => { + if(err.name === 'NoSuchConfigurationAggregatorException') { + throw new AggregatorNotFoundError(aggregatorName) + } + throw err; + }) + .then(aggregator => { + if(aggregator.OrganizationAggregationSource == null) throw new OrgAggregatorValidationError(aggregator); + }); +} + +export async function initialise(awsClient, appSync, config) { + logger.info('Initialising discovery process'); + const {region, rootAccountId, configAggregator: configAggregatorName, crossAccountDiscovery} = config; + + const stsClient = awsClient.createStsClient(); + + const credentials = await stsClient.getCurrentCredentials(); + + const ecsClient = awsClient.createEcsClient(credentials, region); + const taskDefinitionArn = createArn({service: ECS, region, accountId: rootAccountId, resource: `${TASK_DEFINITION}/${WORKLOAD_DISCOVERY_TASKGROUP}`}); + + if (await isDiscoveryEcsTaskRunning(ecsClient, taskDefinitionArn, config)) { + throw new Error(DISCOVERY_PROCESS_RUNNING); + } + + const configServiceClient = awsClient.createConfigServiceClient(credentials, region); + + if(crossAccountDiscovery === AWS_ORGANIZATIONS) { + await validateOrgAggregator(configServiceClient, configAggregatorName); + } + + const appSyncClient = appSync({...config, creds: credentials}); + const apiClient = createApiClient(awsClient, appSyncClient, config); + + return { + apiClient, + configServiceClient + }; +} diff --git a/source/backend/discovery/src/lib/logger.mjs b/source/backend/discovery/src/lib/logger.mjs new file mode 100644 index 00000000..3e803798 --- /dev/null +++ b/source/backend/discovery/src/lib/logger.mjs @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import winston from 'winston'; + +const {transports, createLogger, format} = winston; + +const level = R.defaultTo('info', process.env.LOG_LEVEL).toLowerCase(); + +const logger = createLogger({ + format: format.combine( + format.timestamp(), + format.json() + ), + transports: [ + new transports.Console({level}) + ] +}); + +export default logger; diff --git a/source/backend/discovery/src/lib/persistence/index.mjs b/source/backend/discovery/src/lib/persistence/index.mjs new file mode 100644 index 00000000..268ac3c6 --- /dev/null +++ b/source/backend/discovery/src/lib/persistence/index.mjs @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import logger from '../logger.mjs'; + +export const persistResourcesAndRelationships = R.curry(async (apiClient, deltas) => { + const { + resourceIdsToDelete, resourcesToStore, resourcesToUpdate, + linksToAdd, linksToDelete + } = deltas; + + logger.info(`Deleting ${resourceIdsToDelete.length} resources...`); + logger.profile('Total time to upload'); + const {errors: deleteResourcesErrors} = await apiClient.deleteResources({concurrency: 5, batchSize: 50}, resourceIdsToDelete); + + logger.info(`Updating ${resourcesToUpdate.length} resources...`); + await apiClient.updateResources({concurrency: 10, batchSize: 10}, resourcesToUpdate); + + logger.info(`Storing ${resourcesToStore.length} resources...`); + const {errors: storeResourcesErrors} = await apiClient.storeResources({concurrency: 10, batchSize: 10}, resourcesToStore); + + logger.info(`Deleting ${linksToDelete.length} relationships...`); + await apiClient.deleteRelationships({concurrency: 5, batchSize: 50}, linksToDelete); + + logger.info(`Storing ${linksToAdd.length} relationships...`); + await apiClient.storeRelationships({concurrency: 10, batchSize: 20}, linksToAdd); + + logger.profile('Total time to upload'); + + return { + failedDeletes: deleteResourcesErrors.flatMap(x => x.item), + failedStores: storeResourcesErrors.flatMap(x => x.item.map(x => x.id)) + }; +}); + +export const persistAccounts = R.curry(async ({isUsingOrganizations}, apiClient, accounts, resourcesRegionMetadata) => { + const accountsWithMetadata = accounts.map(({accountId, ...props}) => { + return { + accountId, + ...props, + resourcesRegionMetadata: resourcesRegionMetadata.get(accountId) + } + }); + + if(isUsingOrganizations) { + const [accountsToDelete, accountsToStore] = R.partition(account => account.toDelete, accountsWithMetadata); + const [accountsToAdd, accountsToUpdate] = R.partition(account => account.lastCrawled == null, accountsToStore); + + logger.info(`Adding ${accountsToAdd.length} accounts...`); + logger.info(`Updating ${accountsToUpdate.length} accounts...`); + logger.info(`Deleting ${accountsToDelete.length} accounts...`); + + const results = await Promise.allSettled([ + apiClient.addCrawledAccounts(accountsToAdd), + apiClient.updateCrawledAccounts(accountsToUpdate), + apiClient.deleteAccounts(accountsToDelete.map(x => x.accountId)) + ]); + + results.filter(x => x.status === 'rejected').forEach(res => { + logger.error('Error', {reason: {message: res.reason.message, stack: res.reason.stack}}); + }); + } else { + logger.info(`Updating ${accountsWithMetadata.length} accounts...`); + return apiClient.updateCrawledAccounts(accountsWithMetadata); + } +}); + +export const processPersistenceFailures = R.curry((dbResourcesMap, resources, {failedDeletes, failedStores}) => { + const resourceMap = new Map(resources.map(x => [x.id, x])); + failedStores.forEach(id => resourceMap.delete(id)); + failedDeletes.forEach(id => resourceMap.set(id, dbResourcesMap.get(id))); + return Array.from(resourceMap.values()); +}); diff --git a/source/backend/discovery/src/lib/persistence/transformers.mjs b/source/backend/discovery/src/lib/persistence/transformers.mjs new file mode 100644 index 00000000..56a4c4bb --- /dev/null +++ b/source/backend/discovery/src/lib/persistence/transformers.mjs @@ -0,0 +1,260 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import { + NAME, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_AUTOSCALING_AUTOSCALING_GROUP, + AWS_API_GATEWAY_METHOD, + AWS_API_GATEWAY_RESOURCE, + AWS_EC2_VPC, + AWS_EC2_NETWORK_INTERFACE, + AWS_EC2_INSTANCE, + AWS_EC2_VOLUME, + AWS_EC2_SUBNET, + AWS_EC2_SECURITY_GROUP, + AWS_EC2_ROUTE_TABLE, + AWS_EC2_INTERNET_GATEWAY, + AWS_EC2_NETWORK_ACL, + AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER, + AWS_EC2_EIP, + AWS_API_GATEWAY_REST_API, + AWS_LAMBDA_FUNCTION, + AWS_IAM_ROLE, + AWS_IAM_GROUP, + AWS_IAM_USER, + AWS_IAM_POLICY, + AWS_S3_BUCKET, + APIGATEWAY, + EC2, + IAM, + VPC, + SIGN_IN, + CONSOLE, + AWS_AMAZON_COM, + S3, + LAMBDA, + HOME, + REGION +} from '../constants.mjs'; +import {hash, resourceTypesToHash} from '../utils.mjs'; +import logger from '../logger.mjs'; + +const defaultUrlMappings = { + [AWS_EC2_VPC]: { url: 'vpcs:sort=VpcId', type: VPC.toLowerCase()}, + [AWS_EC2_NETWORK_INTERFACE]: { url: 'NIC:sort=description', type: EC2}, + [AWS_EC2_INSTANCE]: { url: 'Instances:sort=instanceId', type: EC2}, + [AWS_EC2_VOLUME]: { url: 'Volumes:sort=desc:name', type: EC2}, + [AWS_EC2_SUBNET]: { url: 'subnets:sort=SubnetId', type: VPC.toLowerCase()}, + [AWS_EC2_SECURITY_GROUP]: { url: 'SecurityGroups:sort=groupId', type: EC2}, + [AWS_EC2_ROUTE_TABLE]: { url: 'RouteTables:sort=routeTableId', type: VPC.toLowerCase()}, + [AWS_EC2_INTERNET_GATEWAY]: { url: 'igws:sort=internetGatewayId', type: VPC.toLowerCase()}, + [AWS_EC2_NETWORK_ACL]: { url: 'acls:sort=networkAclId', type: VPC.toLowerCase()}, + [AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER]: { url: 'LoadBalancers:', type: EC2}, + [AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP]: { url: 'TargetGroups:', type: EC2}, + [AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER]: { url: 'LoadBalancers:', type: EC2}, + [AWS_EC2_EIP]: { url: 'Addresses:sort=PublicIp', type: EC2}, +}; + +const iamUrlMappings = { + [AWS_IAM_USER]: { url: "/users", type: IAM}, + [AWS_IAM_ROLE]: { url: "/roles", type: IAM}, + [AWS_IAM_POLICY]: { url: "/policies", type: IAM}, + [AWS_IAM_GROUP]: { url: "/groups", type: IAM}, +}; + +function createSignInHostname(accountId, service) { + return `https://${accountId}.${SIGN_IN}.${AWS_AMAZON_COM}/${CONSOLE}/${service}` +} +function createLoggedInHostname(awsRegion, service) { + return `https://${awsRegion}.${CONSOLE}.${AWS_AMAZON_COM}/${service}/${HOME}`; +} + +function createConsoleUrls(resource) { + const {resourceType, resourceName, accountId, awsRegion, configuration} = resource; + + switch(resourceType) { + case AWS_API_GATEWAY_REST_API: + return { + loginURL: `${createSignInHostname(accountId, APIGATEWAY)}?${REGION}=${awsRegion}#/apis/${configuration.id}/resources`, + loggedInURL: `${createLoggedInHostname(awsRegion, APIGATEWAY)}?${REGION}=${awsRegion}#/apis/${configuration.id}/resources` + } + case AWS_API_GATEWAY_RESOURCE: + return { + loginURL: `${createSignInHostname(accountId, APIGATEWAY)}?${REGION}=${awsRegion}#/apis/${configuration.RestApiId}/resources/${configuration.id}`, + loggedInURL: `${createLoggedInHostname(awsRegion, APIGATEWAY)}?${REGION}=${awsRegion}#/apis/${configuration.RestApiId}/resources/${configuration.id}` + } + case AWS_API_GATEWAY_METHOD: + const {httpMethod} = configuration; + return { + loginURL: `${createSignInHostname(accountId, APIGATEWAY)}?${REGION}=${awsRegion}#/apis/${configuration.RestApiId}/resources/${configuration.ResourceId}/${httpMethod}`, + loggedInURL: `${createLoggedInHostname(awsRegion, APIGATEWAY)}?${REGION}=${awsRegion}#/apis/${configuration.RestApiId}/resources/${configuration.ResourceId}/${httpMethod}` + } + case AWS_AUTOSCALING_AUTOSCALING_GROUP: + return { + loginURL: `${createSignInHostname(accountId, EC2)}/autoscaling/home?${REGION}=${awsRegion}#AutoScalingGroups:id=${resourceName};view=details`, + loggedInURL: `${createLoggedInHostname(awsRegion, EC2)}/autoscaling/home?${REGION}=${awsRegion}#AutoScalingGroups:id=${resourceName};view=details` + } + case AWS_LAMBDA_FUNCTION: + return { + loginURL: `${createSignInHostname(accountId, LAMBDA)}?${REGION}=${awsRegion}#/functions/${resourceName}?tab=graph`, + loggedInURL: `${createLoggedInHostname(awsRegion, LAMBDA)}?${REGION}=${awsRegion}#/functions/${resourceName}?tab=graph` + } + case AWS_IAM_ROLE: + case AWS_IAM_GROUP: + case AWS_IAM_USER: + case AWS_IAM_POLICY: + const {url, type} = iamUrlMappings[resourceType]; + return { + loginURL: `${createSignInHostname(accountId, type)}?${HOME}?#${url}`, + loggedInURL: `https://${CONSOLE}.${AWS_AMAZON_COM}/${type}/${HOME}?#${url}`, + } + case AWS_S3_BUCKET: + return { + loginURL: `${createSignInHostname(accountId, S3)}?bucket=${resourceName}`, + loggedInURL: `https://${S3}.${CONSOLE}.${AWS_AMAZON_COM}/${S3}/buckets/${resourceName}/?${REGION}=${awsRegion}` + } + default: + if(defaultUrlMappings[resourceType] != null) { + const {url, type} = defaultUrlMappings[resourceType]; + const v2Type = `${type}/v2` + return { + loginURL: `${createSignInHostname(accountId, type)}?${REGION}=${awsRegion}#${url}`, + loggedInURL: `${createLoggedInHostname(awsRegion, v2Type)}?${REGION}=${awsRegion}#${url}` + } + } + return {}; + } +} + +function createTitle({resourceId, resourceName, arn, resourceType, tags}) { + const name = tags.find(tag => tag.key === NAME); + if(name != null) return name.value; + + switch (resourceType) { + case AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP: + case AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER: + return R.last(arn.split(":")); + case AWS_AUTOSCALING_AUTOSCALING_GROUP: + const parsedAsg = R.last(arn.split(":")); + return R.last(parsedAsg.split("/")); + default: + return resourceName == null ? resourceId : resourceName; + } +} + +const propertiesToKeep = new Set([ + 'accountId', 'arn', 'availabilityZone', 'awsRegion', 'configuration', 'configurationItemCaptureTime', + 'configurationItemStatus', 'configurationStateId', 'resourceCreationTime', 'resourceId', + 'resourceName', 'resourceType', 'supplementaryConfiguration', 'tags', 'version', 'vpcId', 'subnetId', 'subnetIds', + 'resourceValue', 'state', 'private', 'dBInstanceStatus', 'statement', 'instanceType']); + +const propertiesToJsonStringify = new Set(['configuration', 'supplementaryConfiguration', 'tags', 'state']) + +/** + * Neptune cannot store nested properties. Therefore, this function extracts the + * specified and adds them to the main object. It also converts nested fields + * into JSON. + * @param {*} node + */ +function createProperties(resource) { + const properties = Object.entries(resource).reduce((acc, [key, value]) => { + if (propertiesToKeep.has(key)) { + if(propertiesToJsonStringify.has(key)) { + acc[key] = JSON.stringify(value); + } else { + acc[key] = value; + } + } + return acc; + }, {}); + + const logins = createConsoleUrls(resource) + + if(!R.isEmpty(logins)) { + properties.loginURL = logins.loginURL; + properties.loggedInURL = logins.loggedInURL; + } + + properties.title = createTitle(resource); + + return properties; +} + +export function createSaveObject(resource) { + const {id, resourceId, resourceName, resourceType, accountId, arn, awsRegion, relationships = [], tags = []} = resource; + + const properties = createProperties(resource); + + return { + id, + md5Hash: resourceTypesToHash.has(resourceType) ? hash(properties) : '', + resourceId, + resourceName, + resourceType, + accountId, + arn, + awsRegion, + relationships, + properties, + tags + }; +} + +export function createResourcesRegionMetadata(resources) { + logger.profile('Time to createResourcesRegionMetadata'); + + const grouped = R.groupBy(({properties}) => { + const {accountId, awsRegion, resourceType} = properties; + return `${accountId}__${awsRegion}__${resourceType}`; + }, resources); + + const regionsObj = Object.entries(grouped) + .reduce((acc, [key, resources]) => { + const [accountId, awsRegion, resourceType] = key.split('__'); + + const regionKey = `${accountId}__${awsRegion}`; + + if(acc[regionKey] == null) { + acc[regionKey] = { + count: 0, + resourceTypes: [] + }; + } + + acc[regionKey].count = acc[regionKey].count + resources.length; + acc[regionKey].name = awsRegion; + acc[regionKey].resourceTypes.push({ + count: resources.length, + type: resourceType + }); + + return acc; + }, {}); + + const metadata = Object.entries(regionsObj) + .reduce((acc, [key, resourceTypes]) => { + const [accountId] = key.split('__'); + + if(!acc.has(accountId)) { + acc.set(accountId, { + accountId, + count: 0, + regions: [] + }); + } + + const account = acc.get(accountId) + + account.count = account.count + resourceTypes.count; + account.regions.push(resourceTypes); + + return acc; + }, new Map()); + + logger.profile('Time to createResourcesRegionMetadata'); + + return metadata; +} diff --git a/source/backend/discovery/src/lib/sdkResources/createAllBatchResources.mjs b/source/backend/discovery/src/lib/sdkResources/createAllBatchResources.mjs new file mode 100644 index 00000000..cbbb8551 --- /dev/null +++ b/source/backend/discovery/src/lib/sdkResources/createAllBatchResources.mjs @@ -0,0 +1,211 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import { + AWS, NOT_APPLICABLE, + AWS_IAM_AWS_MANAGED_POLICY, + MULTIPLE_AVAILABILITY_ZONES, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + SPOT_FLEET_REQUEST_ID_TAG, + EC2, + SPOT_FLEET_REQUEST, + AWS_EC2_SPOT_FLEET, + AWS_EC2_INSTANCE, + SPOT_INSTANCE_REQUEST, + AWS_EC2_SPOT, + AWS_MEDIA_CONNECT_FLOW, + AWS_OPENSEARCH_DOMAIN, + GLOBAL, + REGIONAL +} from '../constants.mjs'; +import { + createArn, + createAssociatedRelationship, + createConfigObject +} from '../utils.mjs'; +import logger from '../logger.mjs'; + +async function createApplications(awsClient, credentials, accountId, region) { + const appRegistryClient = awsClient.createServiceCatalogAppRegistryClient(credentials, region); + + const applications = await appRegistryClient.getAllApplications(); + + return applications.map(application => { + return createConfigObject({ + arn: application.arn, + accountId, + awsRegion: region, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + resourceId: application.arn, + resourceName: application.name + }, application) + }); +} + +async function createMediaConnectFlows(awsClient, credentials, accountId, region) { + const mediaConnectClient = awsClient.createMediaConnectClient(credentials, region); + + const flows = await mediaConnectClient.getAllFlows(); + + return flows.map(flow => { + return createConfigObject({ + arn: flow.FlowArn, + accountId: accountId, + awsRegion: region, + availabilityZone: flow.AvailabilityZone, + resourceType: AWS_MEDIA_CONNECT_FLOW, + resourceId: flow.FlowArn, + resourceName: flow.Name + }, flow); + }); +} + +async function createAttachedAwsManagedPolices(awsClient, credentials, accountId, region) { + const iamClient = awsClient.createIamClient(credentials, region) + + const managedPolices = await iamClient.getAllAttachedAwsManagedPolices(); + + return managedPolices.map(policy => { + return createConfigObject({ + arn: policy.Arn, + accountId: AWS, + awsRegion: region, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_IAM_AWS_MANAGED_POLICY, + resourceId: policy.Arn, + resourceName: policy.PolicyName + }, policy); + }); +} + +async function createTargetGroups(awsClient, credentials, accountId, region) { + const elbV2Client = awsClient.createElbV2Client(credentials, region); + + const targetGroups = await elbV2Client.getAllTargetGroups(); + + return targetGroups.map(targetGroup => { + return createConfigObject({ + arn: targetGroup.TargetGroupArn, + accountId, + awsRegion: region, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + resourceId: targetGroup.TargetGroupArn, + resourceName: targetGroup.TargetGroupArn + }, targetGroup); + }) +} + +async function createSpotResources(awsClient, credentials, accountId, region) { + const ec2Client = awsClient.createEc2Client(credentials, region); + + const spotInstanceRequests = await ec2Client.getAllSpotInstanceRequests(); + + const groupedReqs = R.groupBy(x => { + const sfReqId = x.Tags.find(x => x.Key === SPOT_FLEET_REQUEST_ID_TAG); + return sfReqId == null ? 'spotInstanceRequests' : sfReqId.Value; + }, spotInstanceRequests); + + const spotFleetRequests = (await ec2Client.getAllSpotFleetRequests()).map((request) => { + const arn = createArn({ + service: EC2, region, accountId, resource: `${SPOT_FLEET_REQUEST}/${request.SpotFleetRequestId}` + }); + return createConfigObject({ + arn, + accountId, + awsRegion: region, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + resourceType: AWS_EC2_SPOT_FLEET, + resourceId: arn, + resourceName: arn, + relationships: groupedReqs[request.SpotFleetRequestId].map(({InstanceId}) => { + return createAssociatedRelationship(AWS_EC2_INSTANCE, {resourceId: InstanceId}); + }) + }, request); + }); + + + const spotInstanceRequestObjs = (groupedReqs.spotInstanceRequests ?? []).map(spiReq => { + const arn = createArn({ + service: EC2, region, accountId, resource: `${SPOT_INSTANCE_REQUEST}/${spiReq.SpotInstanceRequestId}` + }); + return createConfigObject({ + arn, + accountId, + awsRegion: region, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + resourceType: AWS_EC2_SPOT, + resourceId: arn, + resourceName: arn, + relationships: [ + createAssociatedRelationship(AWS_EC2_INSTANCE, {resourceId: spiReq.InstanceId}) + ] + }, spiReq); + }); + + return [...spotFleetRequests, ...spotInstanceRequestObjs]; +} + +async function createOpenSearchDomains(awsClient, credentials, accountId, region) { + const openSearchClient = awsClient.createOpenSearchClient(credentials, region) + + const domains = await openSearchClient.getAllOpenSearchDomains(); + + return domains.map(domain => { + return createConfigObject({ + arn: domain.ARN, + accountId, + awsRegion: region, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + resourceType: AWS_OPENSEARCH_DOMAIN, + resourceId: domain.DomainName, + resourceName: domain.DomainName + }, domain); + }); +} + +const handleError = R.curry((handlerName, accountId, region, error) => { + return { + item: {handlerName, accountId, region}, + raw: error, + message: error.message + } +}); + +async function createAllBatchResources(credentialsTuples, awsClient) { + const handlers = [ + [GLOBAL, createAttachedAwsManagedPolices], + [REGIONAL, createApplications], + [REGIONAL, createMediaConnectFlows], + [REGIONAL, createTargetGroups], + [REGIONAL, createOpenSearchDomains], + [REGIONAL, createSpotResources] + ]; + + const {results, errors} = await Promise.all(handlers.flatMap(([serviceRegion, handler]) => { + return credentialsTuples + .flatMap(([accountId, {regions, credentials}]) => { + const errorHandler = handleError(handler.name, accountId); + return serviceRegion === GLOBAL + ? handler(awsClient, credentials, accountId, GLOBAL).catch(errorHandler(GLOBAL)) + : regions.map(region => handler(awsClient, credentials, accountId, region).catch(errorHandler(region))); + }); + })).then(R.reduce((acc, item) => { + if (item.raw != null) { + acc.errors.push(item); + } else { + acc.results.push(...item); + } + return acc; + }, {results: [], errors: []})); + + logger.error(`There were ${errors.length} errors when adding batch SDK resources.`); + logger.debug('Errors: ', {errors: errors}); + + return results; +} + +export default createAllBatchResources; diff --git a/source/backend/discovery/src/lib/sdkResources/firstOrderHandlers.mjs b/source/backend/discovery/src/lib/sdkResources/firstOrderHandlers.mjs new file mode 100644 index 00000000..f8f5c4ab --- /dev/null +++ b/source/backend/discovery/src/lib/sdkResources/firstOrderHandlers.mjs @@ -0,0 +1,239 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import { + AWS_API_GATEWAY_REST_API, + APIGATEWAY, + RESTAPIS, + RESOURCES, + AWS_API_GATEWAY_RESOURCE, + AUTHORIZERS, + AWS_API_GATEWAY_AUTHORIZER, + AWS_DYNAMODB_STREAM, + AWS_DYNAMODB_TABLE, + AWS_ECS_SERVICE, + AWS_ECS_TASK, + AWS_EKS_CLUSTER, + MULTIPLE_AVAILABILITY_ZONES, + AWS_EKS_NODE_GROUP, + AWS_IAM_ROLE, + AWS_IAM_USER, + INLINE_POLICY, + IS_ASSOCIATED_WITH, + GLOBAL, + NOT_APPLICABLE, + AWS_IAM_INLINE_POLICY, + AWS_APPSYNC_DATASOURCE, + AWS_APPSYNC_GRAPHQLAPI, + AWS_APPSYNC_RESOLVER +} from '../constants.mjs'; +import { + createArn, createConfigObject, createContainedInRelationship, createAssociatedRelationship, createArnRelationship +} from '../utils.mjs'; + +const createInlinePolicy = R.curry(({arn, resourceName, accountId, resourceType}, policy) => { + const policyArn = `${arn}/${INLINE_POLICY}/${policy.policyName}`; + const inlinePolicy = { + policyName: policy.policyName, + policyDocument: JSON.parse(decodeURIComponent(policy.policyDocument)) + }; + + return createConfigObject({ + arn: policyArn, + accountId: accountId, + awsRegion: GLOBAL, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_IAM_INLINE_POLICY, + resourceId: policyArn, + resourceName: policyArn, + relationships: [ + createAssociatedRelationship(resourceType, {resourceName}) + ] + }, inlinePolicy); +}); + +export function createFirstOrderHandlers(accountsMap, awsClient) { + return { + [AWS_API_GATEWAY_REST_API]: async ({awsRegion, accountId, availabilityZone, resourceId, configuration}) => { + const {id: RestApiId} = configuration; + const {credentials} = accountsMap.get(accountId); + + const apiGatewayClient = awsClient.createApiGatewayClient(credentials, awsRegion); + + const apiGatewayResources = [] + + const apiResources = await apiGatewayClient.getResources(RestApiId); + + apiGatewayResources.push(...apiResources.map(item => { + const arn = createArn({ + service: APIGATEWAY, + region: awsRegion, + resource: `/${RESTAPIS}/${RestApiId}/${RESOURCES}/${item.id}` + }); + return createConfigObject({ + arn, + accountId, + awsRegion, + availabilityZone, + resourceType: AWS_API_GATEWAY_RESOURCE, + resourceId: arn, + resourceName: arn, + relationships: [ + createContainedInRelationship(AWS_API_GATEWAY_REST_API, {resourceId}) + ] + }, {RestApiId, ...item}); + })); + + const authorizers = await apiGatewayClient.getAuthorizers(RestApiId); + apiGatewayResources.push(...authorizers.map(authorizer => { + const arn = createArn({ + service: APIGATEWAY, + region: awsRegion, + resource: `/${RESTAPIS}/${RestApiId}/${AUTHORIZERS}/${authorizer.id}` + }); + return createConfigObject({ + arn, + accountId, + awsRegion, + availabilityZone, + resourceType: AWS_API_GATEWAY_AUTHORIZER, + resourceId: arn, + resourceName: arn, + relationships: [ + createContainedInRelationship(AWS_API_GATEWAY_REST_API, {resourceId}), + ...(authorizer.providerARNs ?? []).map(createArnRelationship(IS_ASSOCIATED_WITH)) + ] + }, {RestApiId, ...authorizer}); + })); + + return apiGatewayResources; + }, + + [AWS_APPSYNC_GRAPHQLAPI]: async ({accountId, awsRegion, resourceId, resourceName}) => { + const {credentials} = accountsMap.get(accountId); + const appSyncClient = awsClient.createAppSyncClient(credentials, awsRegion); + + const dataSources = appSyncClient.listDataSources(resourceId).then(data => data.map(dataSource => { + return createConfigObject({ + arn: dataSource.dataSourceArn, + accountId, + awsRegion, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_APPSYNC_DATASOURCE, + resourceId: dataSource.dataSourceArn, + resourceName: dataSource.name, + relationships: [] + }, {...dataSource, apiId: resourceId}); + })) + + const queryResolvers = appSyncClient.listResolvers(resourceId, "Query").then(data => data.map(resolver => { + return createConfigObject({ + arn: resolver.resolverArn, + accountId, + awsRegion, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_APPSYNC_RESOLVER, + resourceId: resolver.resolverArn, + resourceName: resolver.fieldName, + relationships: [ + createContainedInRelationship(AWS_APPSYNC_GRAPHQLAPI, {resourceId}), + createAssociatedRelationship(AWS_APPSYNC_DATASOURCE, {resourceName: resolver.dataSourceName}) + ] + }, {...resolver, apiId: resourceId}); + })) + + const mutationResolvers = appSyncClient.listResolvers(resourceId, "Mutation").then(data => data.map(resolver => { + return createConfigObject({ + arn: resolver.resolverArn, + accountId, + awsRegion, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_APPSYNC_RESOLVER, + resourceId: resolver.resolverArn, + resourceName: resolver.fieldName, + relationships: [ + createContainedInRelationship(AWS_APPSYNC_GRAPHQLAPI, {resourceId}), + createAssociatedRelationship(AWS_APPSYNC_DATASOURCE, {resourceName: resolver.dataSourceName}) + ] + }, {...resolver, apiId: resourceId}); + })) + return Promise.allSettled([dataSources, queryResolvers, mutationResolvers]) + .then(results => results + .flatMap(({status, value}) => status === "fulfilled" ? value : []) + ) + + }, + [AWS_DYNAMODB_TABLE]: async ({awsRegion, accountId, configuration}) => { + if (configuration.latestStreamArn == null) { + return [] + } + + const {credentials} = accountsMap.get(accountId); + + const dynamoDBStreamsClient = awsClient.createDynamoDBStreamsClient(credentials, awsRegion); + + const stream = await dynamoDBStreamsClient.describeStream(configuration.latestStreamArn); + + return [createConfigObject({ + arn: stream.StreamArn, + accountId, + awsRegion, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_DYNAMODB_STREAM, + resourceId: stream.StreamArn, + resourceName: stream.StreamArn, + relationships: [] + }, stream)]; + }, + [AWS_ECS_SERVICE]: async ({awsRegion, resourceId, resourceName, accountId, configuration: {Cluster}}) => { + const {credentials} = accountsMap.get(accountId); + const ecsClient = awsClient.createEcsClient(credentials, awsRegion); + + const tasks = await ecsClient.getAllServiceTasks(Cluster, resourceName); + + return tasks.map(task => { + return createConfigObject({ + arn: task.taskArn, + accountId, + awsRegion, + availabilityZone: task.availabilityZone, + resourceType: AWS_ECS_TASK, + resourceId: task.taskArn, + resourceName: task.taskArn, + relationships: [ + createAssociatedRelationship(AWS_ECS_SERVICE, {resourceId}) + ] + }, task); + }); + }, + [AWS_EKS_CLUSTER]: async ({accountId, awsRegion, resourceId, resourceName}) => { + const {credentials} = accountsMap.get(accountId); + + const eksClient = awsClient.createEksClient(credentials, awsRegion); + + const nodeGroups = await eksClient.listNodeGroups(resourceName); + + return nodeGroups.map(nodeGroup => { + return createConfigObject({ + arn: nodeGroup.nodegroupArn, + accountId, + awsRegion, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + resourceType: AWS_EKS_NODE_GROUP, + resourceId: nodeGroup.nodegroupArn, + resourceName: nodeGroup.nodegroupName, + relationships: [ + createContainedInRelationship(AWS_EKS_CLUSTER, {resourceId}) + ] + }, nodeGroup); + }); + }, + [AWS_IAM_ROLE]: async ({arn, resourceName, accountId, resourceType, configuration: {rolePolicyList = []}}) => { + return rolePolicyList.map(createInlinePolicy({arn, resourceName, resourceType, accountId})); + }, + [AWS_IAM_USER]: ({arn, resourceName, resourceType, accountId, configuration: {userPolicyList = []}}) => { + return userPolicyList.map(createInlinePolicy({arn, resourceName, accountId, resourceType})); + } + } +} diff --git a/source/backend/discovery/src/lib/sdkResources/index.mjs b/source/backend/discovery/src/lib/sdkResources/index.mjs new file mode 100644 index 00000000..9b41137e --- /dev/null +++ b/source/backend/discovery/src/lib/sdkResources/index.mjs @@ -0,0 +1,121 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import {PromisePool} from '@supercharge/promise-pool'; +import { + createArn, + createConfigObject +} from '../utils.mjs'; +import { + AWS_TAGS_TAG, + GLOBAL, + NOT_APPLICABLE, + TAG, + TAGS, + IS_ASSOCIATED_WITH +} from '../constants.mjs'; +import logger from '../logger.mjs'; +import createAllBatchResources from './createAllBatchResources.mjs'; +import {createFirstOrderHandlers} from './firstOrderHandlers.mjs'; +import {createSecondOrderHandlers} from './secondOrderHandlers.mjs'; + +const createTag = R.curry((accountId, {key, value}) => { + const resourceName = `${key}=${value}`; + const arn = createArn({ + service: TAGS, accountId, resource: `${TAG}/${resourceName}` + }); + return createConfigObject({ + arn, + accountId, + awsRegion: GLOBAL, + availabilityZone: NOT_APPLICABLE, + resourceType: AWS_TAGS_TAG, + resourceId: arn, + resourceName + }, {}); +}); + +function createTags(resources) { + const resourceMap = resources.reduce((acc, {accountId, awsRegion, resourceId, resourceName, resourceType, tags = []}) => { + tags + .map(createTag(accountId)) + .forEach(tag => { + const {id, relationships} = tag; + if (!acc.has(id)) { + relationships.push({ + relationshipName: IS_ASSOCIATED_WITH, + resourceId, + resourceName, + resourceType, + awsRegion + }) + acc.set(id, tag); + } else { + acc.get(id).relationships.push({ + relationshipName: IS_ASSOCIATED_WITH, + resourceId, + resourceName, + resourceType, + awsRegion + }); + } + }) + return acc; + }, new Map()); + + return Array.from(resourceMap.values()); +} + +export const getAllSdkResources = R.curry(async (accountsMap, awsClient, resources) => { + logger.profile('Time to get all resources from AWS SDK'); + const resourcesCopy = [...resources]; + + const credentialsTuples = Array.from(accountsMap.entries()); + + const batchResources = await createAllBatchResources(credentialsTuples, awsClient); + + batchResources.forEach(resource => resourcesCopy.push(resource)); + + const firstOrderHandlers = createFirstOrderHandlers(accountsMap, awsClient); + + const secondOrderHandlers = createSecondOrderHandlers(accountsMap, awsClient); + + const firstOrderResourceTypes = new Set(R.keys(firstOrderHandlers)); + + const {results: firstResults, errors: firstErrors} = await PromisePool + .withConcurrency(15) + .for(resourcesCopy.filter(({resourceType}) => firstOrderResourceTypes.has(resourceType))) + .process(async resource => { + const handler = firstOrderHandlers[resource.resourceType]; + return handler(resource); + }); + + logger.error(`There were ${firstErrors.length} errors when adding first order SDK resources.`); + logger.debug('Errors: ', {firstErrors}); + + firstResults.flat().forEach(resource => resourcesCopy.push(resource) ); + + const secondOrderResourceTypes = new Set(R.keys(secondOrderHandlers)); + + const {results: secondResults, errors: secondErrors} = await PromisePool + .withConcurrency(10) + .for(firstResults.flat().filter(({resourceType}) => secondOrderResourceTypes.has(resourceType))) + .process(async resource => { + const handler = secondOrderHandlers[resource.resourceType]; + return handler(resource); + }); + + logger.error(`There were ${secondErrors.length} errors when adding second order SDK resources.`); + logger.debug('Errors: ', {secondErrors}); + + secondResults.flat().forEach(resource => resourcesCopy.push(resource)); + + const tags = createTags(resourcesCopy); + + tags.forEach(tag => resourcesCopy.push(tag)) + + logger.profile('Time to get all resources from AWS SDK'); + + return resourcesCopy; +}); \ No newline at end of file diff --git a/source/backend/discovery/src/lib/sdkResources/secondOrderHandlers.mjs b/source/backend/discovery/src/lib/sdkResources/secondOrderHandlers.mjs new file mode 100644 index 00000000..76de4e5f --- /dev/null +++ b/source/backend/discovery/src/lib/sdkResources/secondOrderHandlers.mjs @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + APIGATEWAY, + RESTAPIS, + RESOURCES, + AWS_API_GATEWAY_RESOURCE, + POST, + GET, + PUT, + DELETE, + NOT_FOUND_EXCEPTION, + METHODS, + AWS_API_GATEWAY_METHOD +} from '../constants.mjs'; +import {createArn, createConfigObject, createContainedInRelationship} from '../utils.mjs'; +import logger from '../logger.mjs'; + +export function createSecondOrderHandlers(accountsMap, awsClient) { + return { + [AWS_API_GATEWAY_RESOURCE]: async ({resourceId, accountId, availabilityZone, awsRegion, arn: apiResourceArn, configuration}) => { + // don't confuse ResourceId which is the id that API Gateway assigns to this resource with + // the camel case version, which is the id AWS Config would assign it. We create this in + // ths first order handlers to have a uniform shape to all the data. + const {RestApiId, id: ResourceId} = configuration; + + const {credentials} = accountsMap.get(accountId); + + const apiGatewayClient = awsClient.createApiGatewayClient(credentials, awsRegion); + + const results = await Promise.allSettled([ + apiGatewayClient.getMethod(POST, ResourceId, RestApiId), + apiGatewayClient.getMethod(GET, ResourceId, RestApiId), + apiGatewayClient.getMethod(PUT, ResourceId, RestApiId), + apiGatewayClient.getMethod(DELETE, ResourceId, RestApiId), + ]); + + results.forEach(({status, reason}) => { + if(status === 'rejected' && reason.name !== NOT_FOUND_EXCEPTION) { + logger.error(`Error discovering API Gateway integration for resource: ${apiResourceArn}`, {error: reason}); + } + }); + + return results.filter(x => x.status === 'fulfilled').map(({value: item}) => { + const arn = createArn({ + service: APIGATEWAY, region: awsRegion, resource: `/${RESTAPIS}/${RestApiId}/${RESOURCES}/${ResourceId}/${METHODS}/${item.httpMethod}` + }); + return createConfigObject({ + arn, + accountId, + awsRegion, + availabilityZone, + resourceType: AWS_API_GATEWAY_METHOD, + resourceId: arn, + resourceName: arn, + relationships: [ + createContainedInRelationship(AWS_API_GATEWAY_RESOURCE, {resourceId}), + ] + }, {RestApiId, ResourceId, ...item}); + }); + } + }; +} diff --git a/source/backend/discovery/src/lib/utils.mjs b/source/backend/discovery/src/lib/utils.mjs new file mode 100644 index 00000000..a7160c5b --- /dev/null +++ b/source/backend/discovery/src/lib/utils.mjs @@ -0,0 +1,240 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import {build as buildArn} from '@aws-sdk/util-arn-parser'; +import logger from './logger.mjs'; +import { + AWS, + AWS_CN, + AWS_US_GOV, + CONTAINS, + AWS_EC2_SECURITY_GROUP, + IS_ASSOCIATED_WITH, + IS_ATTACHED_TO, + IS_CONTAINED_IN, + SUBNET, + VPC, + AWS_EC2_VPC, + AWS_EC2_SUBNET, + CN_NORTH_1, + CN_NORTHWEST_1, + US_GOV_EAST_1, + US_GOV_WEST_1, + RESOURCE_DISCOVERED, + SECURITY_GROUP, + AWS_API_GATEWAY_METHOD, + AWS_API_GATEWAY_RESOURCE, + AWS_COGNITO_USER_POOL, + AWS_ECS_TASK, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_EKS_NODE_GROUP, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_IAM_AWS_MANAGED_POLICY, + AWS_DYNAMODB_STREAM, + AWS_EC2_SPOT, + AWS_EC2_SPOT_FLEET, + AWS_IAM_INLINE_POLICY, + AWS_OPENSEARCH_DOMAIN, + AWS_EC2_INSTANCE, + AWS_EC2_NETWORK_INTERFACE, + AWS_EC2_VOLUME, + AWS_IAM_ROLE +} from './constants.mjs'; +import crypto from 'crypto'; + +export function hash(data) { + const algo = 'md5'; + let shasum = crypto.createHash(algo).update(JSON.stringify(data)); //NOSONAR - hashing algorithm is only used for comparing two JS objects + return "" + shasum.digest('hex'); +} + +export const createRelationship = R.curry((relationshipName, resourceType, {arn, relNameSuffix, resourceName, resourceId, awsRegion, accountId}) => { + const relationship = {relationshipName} + if(arn != null) { + relationship.arn = arn; + } + if(resourceType != null) { + relationship.resourceType = resourceType; + } + if(resourceName != null) { + relationship.resourceName = resourceName; + } + if(relNameSuffix != null) { + relationship.relationshipName = relationshipName + relNameSuffix; + } + if(resourceId != null) { + relationship.resourceId = resourceId; + } + if(accountId != null) { + relationship.accountId = accountId; + } + if(awsRegion != null) { + relationship.awsRegion = awsRegion; + } + return relationship; +}); + +export const createContainsRelationship = createRelationship(CONTAINS); + +export const createAssociatedRelationship = createRelationship(IS_ASSOCIATED_WITH); + +export const createAttachedRelationship = createRelationship(IS_ATTACHED_TO); + +export const createContainedInRelationship = createRelationship(IS_CONTAINED_IN); + +export function createContainedInVpcRelationship(resourceId) { + return createRelationship(IS_CONTAINED_IN + VPC, AWS_EC2_VPC, {resourceId}); +} + +export function createContainedInSubnetRelationship(resourceId) { + return createRelationship(IS_CONTAINED_IN + SUBNET, AWS_EC2_SUBNET, {resourceId}); +} + +export function createAssociatedSecurityGroupRelationship(resourceId) { + return createRelationship(IS_ASSOCIATED_WITH + SECURITY_GROUP, AWS_EC2_SECURITY_GROUP, {resourceId}) +} + +export const createArnRelationship = R.curry((relationshipName, arn) => { + return createRelationship(relationshipName, null, {arn}); +}); + +const chinaRegions = new Map([[CN_NORTH_1, AWS_CN], [CN_NORTHWEST_1, AWS_CN]]); +const govRegions = new Map([[US_GOV_EAST_1, AWS_US_GOV], [US_GOV_WEST_1, AWS_US_GOV]]); + +export function createArn({service, accountId = '', region = '', resource}) { + const partition = chinaRegions.get(region) ?? govRegions.get(region) ?? AWS; + return buildArn({ service, partition, region, accountId, resource}); +} + +export function createArnWithResourceType({resourceType, accountId = '', awsRegion: region = '', resourceId}) { + const [, service, resource] = resourceType.toLowerCase().split('::'); + return createArn({ service, region, accountId, resource: `${resource}/${resourceId}`}); +} + +export function isObject(val) { + return typeof val === 'object' && !Array.isArray(val) && val !== null; +} + +function objKeysToCamelCase(obj) { + return Object.entries(obj).reduce((acc, [k, v]) => { + acc[k.replace(/^./, k[0].toLowerCase())] = v; + return acc + }, {}); +} + +export function objToKeyNameArray(obj) { + return Object.entries(obj).map(([key, value]) => { + return { + key, + value + } + }); +} + +export function normaliseTags(tags = []) { + return isObject(tags) ? objToKeyNameArray(tags) : tags.map(objKeysToCamelCase); +} + +export function createConfigObject({arn, accountId, awsRegion, availabilityZone, resourceType, resourceId, resourceName, relationships = []}, configuration) { + const tags = normaliseTags(configuration.Tags ?? configuration.tags); + + return { + id: arn, + accountId, + arn: arn ?? createArn({resourceType, accountId, awsRegion, resourceId}), + availabilityZone, + awsRegion, + configuration: configuration, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId, + resourceName, + resourceType, + tags, + relationships + } +} + +export function isString(value) { + return typeof value === 'string' && Object.prototype.toString.call(value) === "[object String]" +} + +export function isDate(date) { + return date && Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date); +} + +export function createResourceNameKey({resourceName, resourceType, accountId, awsRegion}) { + const first = resourceType == null ? '' : `${resourceType}_`; + return `${first}${resourceName}_${accountId}_${awsRegion}`; +} + +export function createResourceIdKey({resourceId, resourceType, accountId, awsRegion}) { + const first = resourceType == null ? '' : `${resourceType}_`; + return `${first}${resourceId}_${accountId}_${awsRegion}`; +} + +export const safeForEach = R.curry((f, xs) => { + const errors = []; + + xs.forEach(item => { + try { + f(item); + } catch(error) { + errors.push({ + error, + item + }) + } + }); + + return {errors}; +}); + +export const profileAsync = R.curry((message, f) => { + return async (...args) => { + logger.profile(message); + const result = await f(...args); + logger.profile(message); + return result; + } +}); + +export const memoize = R.memoizeWith((...args) => JSON.stringify(args)); + +export const resourceTypesToHash = new Set([ + AWS_API_GATEWAY_METHOD, + AWS_API_GATEWAY_RESOURCE, + AWS_DYNAMODB_STREAM, + AWS_ECS_TASK, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_EKS_NODE_GROUP, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_IAM_AWS_MANAGED_POLICY, + AWS_EC2_SPOT, + AWS_EC2_SPOT_FLEET, + AWS_IAM_INLINE_POLICY, + AWS_COGNITO_USER_POOL, + AWS_OPENSEARCH_DOMAIN + ] +); + +export const resourceTypesToNormalize = [ + AWS_EC2_INSTANCE, + AWS_EC2_NETWORK_INTERFACE, + AWS_EC2_SECURITY_GROUP, + AWS_EC2_SUBNET, + AWS_EC2_VOLUME, + AWS_EC2_VPC, + AWS_IAM_ROLE +]; + +export const resourceTypesToNormalizeSet = new Set(resourceTypesToNormalize); + +const normalizedSuffixSet = new Set(resourceTypesToNormalize.map(resourceType => { + const [,, relSuffix] = resourceType.split('::'); + return relSuffix.toLowerCase(); +})); + +export function isQualifiedRelationshipName(relationshipName) { + return normalizedSuffixSet.has(relationshipName.split(' ').at(-1).toLowerCase()); +} diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::AppSync::DataSource.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::AppSync::DataSource.json new file mode 100644 index 00000000..4bc5a43c --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::AppSync::DataSource.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "type": "AWS::AppSync::DataSource", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "lambdaConfig.lambdaFunctionArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "dynamodbConfig.tableName", + "identifierType": "resourceName", + "resourceType": "AWS::DynamoDB::Table" + }, + { + "relationshipName": "Is associated with", + "path": "eventBridgeConfig.eventBusArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "relationalDatabaseConfig.rdsHttpEndpointConfig.dbClusterIdentifier", + "identifierType": "resourceId", + "resourceType":"AWS::RDS::DBCluster" + }, + { + "relationshipName": "Is associated with", + "path": "elasticsearchConfig.endpoint", + "identifierType": "endpoint" + }, + { + "relationshipName": "Is associated with", + "path": "openSearchServiceConfig.endpoint", + "identifierType": "endpoint" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::AutoScalingGroup.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::AutoScalingGroup.json new file mode 100644 index 00000000..bbd2e625 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::AutoScalingGroup.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "type": "AWS::AutoScaling::AutoScalingGroup", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::LaunchTemplate", + "path": "launchTemplate.launchTemplateId", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::WarmPool.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::WarmPool.json new file mode 100644 index 00000000..691166fe --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::AutoScaling::WarmPool.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "type": "AWS::AutoScaling::WarmPool", + "relationships": { + "descriptors": [ + { + "resourceType": "AWS::AutoScaling::AutoScalingGroup", + "relationshipName": "Is associated with", + "path": "AutoScalingGroupName", + "identifierType": "resourceName" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::CodeBuild::Project.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::CodeBuild::Project.json new file mode 100644 index 00000000..2be78274 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::CodeBuild::Project.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "type": "AWS::CodeBuild::Project", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with Role", + "path": "serviceRole", + "identifierType": "arn" + }, + { + "relationshipName": "Is contained in", + "resourceType": "AWS::EC2::Subnet", + "path": "vpcConfig.subnets", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "vpcConfig.securityGroupIds", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::DynamoDB::Table.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::DynamoDB::Table.json new file mode 100644 index 00000000..84f1fb2b --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::DynamoDB::Table.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "type": "AWS::DynamoDB::Table", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "latestStreamArn", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::Instance.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::Instance.json new file mode 100644 index 00000000..9d665d4b --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::Instance.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "type": "AWS::EC2::Instance", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "iamInstanceProfile.arn", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::SpotFleet.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::SpotFleet.json new file mode 100644 index 00000000..c8a58b49 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::SpotFleet.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "type": "AWS::EC2::SpotFleet", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "SpotFleetRequestConfig.LoadBalancersConfig.ClassicLoadBalancersConfig.ClassicLoadBalancers[*].Name", + "identifierType": "resourceId", + "resourceType": "AWS::ElasticLoadBalancing::LoadBalancer" + }, + { + "relationshipName": "Is associated with", + "path": "SpotFleetRequestConfig.LoadBalancersConfig.TargetGroupsConfig.TargetGroups[*].Arn", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::TransitGateway.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::TransitGateway.json new file mode 100644 index 00000000..d417e3fe --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::EC2::TransitGateway.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "type": "AWS::EC2::TransitGateway", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is contained in", + "resourceType": "AWS::EC2::TransitGatewayRouteTable", + "path": "AssociationDefaultRouteTableId", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is contained in", + "resourceType": "AWS::EC2::TransitGatewayRouteTable", + "path": "PropagationDefaultRouteTableId", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Cluster.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Cluster.json new file mode 100644 index 00000000..8b62e5e9 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Cluster.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "type": "AWS::ECS::Cluster", + "relationships": { + "descriptors": [ + { + "resourceType": "AWS::S3::Bucket", + "relationshipName": "Is associated with", + "path": "LogConfiguration.S3BucketName", + "identifierType": "resourceId" + }, + { + "resourceType": "AWS::EC2::Instance", + "relationshipName": "Contains", + "sdkClient": { + "type": "ecs", + "method": "getAllClusterInstances", + "argumentPaths": ["@.arn"] + }, + "path": "@", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Service.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Service.json new file mode 100644 index 00000000..0f468b27 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::Service.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "type": "AWS::ECS::Service", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is contained in", + "path": "Cluster", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "TaskDefinition", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "LoadBalancers[].TargetGroupArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with Role", + "path": "Role", + "identifierType": "arn" + }, + { + "relationshipName": "Is contained in Subnet", + "resourceType": "AWS::EC2::Subnet", + "path": "NetworkConfiguration.AwsvpcConfiguration.Subnets", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with SecurityGroup", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "NetworkConfiguration.AwsvpcConfiguration.SecurityGroups", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::TaskDefinition.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::TaskDefinition.json new file mode 100644 index 00000000..392db93c --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::ECS::TaskDefinition.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "type": "AWS::ECS::TaskDefinition", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with Role", + "path": "ExecutionRoleArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with Role", + "path": "TaskRoleArn", + "identifierType": "arn" + }, + { + "resourceType": "AWS::EFS::AccessPoint", + "relationshipName": "Is associated with", + "path": "Volumes[].EfsVolumeConfiguration.AuthorizationConfig.AccessPointId", + "identifierType": "resourceId" + }, + { + "resourceType": "AWS::EFS::FileSystem", + "relationshipName": "Is associated with", + "path": "Volumes[].EfsVolumeConfiguration.FileSystemId", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::AccessPoint.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::AccessPoint.json new file mode 100644 index 00000000..6770c389 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::AccessPoint.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "type": "AWS::EFS::AccessPoint", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is attached to", + "path": "FileSystemId", + "identifierType": "resourceId", + "resourceType": "AWS::EFS::FileSystem" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::FileSystem.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::FileSystem.json new file mode 100644 index 00000000..90d72a6a --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::EFS::FileSystem.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "type": "AWS::EFS::FileSystem", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "KmsKeyId", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Cluster.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Cluster.json new file mode 100644 index 00000000..308e7a4b --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Cluster.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "type": "AWS::EKS::Cluster", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with Role", + "path": "RoleArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is contained in", + "resourceType": "AWS::EC2::Subnet", + "path": "ResourcesVpcConfig.SubnetIds", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "ResourcesVpcConfig.SecurityGroupIds", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "ClusterSecurityGroupId", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Nodegroup.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Nodegroup.json new file mode 100644 index 00000000..ae8c0678 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::EKS::Nodegroup.json @@ -0,0 +1,37 @@ +{ + "version": 1, + "type": "AWS::EKS::Nodegroup", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with Role", + "path": "nodeRole", + "identifierType": "arn" + }, + { + "relationshipName": "Is contained in", + "resourceType": "AWS::EC2::Subnet", + "path": "subnets", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "resources.remoteAccessSecurityGroup", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "remoteAccess.sourceSecurityGroups", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::LaunchTemplate", + "path": "launchTemplate.id", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::ElasticLoadBalancing::LoadBalancer.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::ElasticLoadBalancing::LoadBalancer.json new file mode 100644 index 00000000..1c03e179 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::ElasticLoadBalancing::LoadBalancer.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "type": "AWS::ElasticLoadBalancing::LoadBalancer", + "relationships": { + "descriptors": [ + { + "resourceType": "AWS::EC2::Instance", + "relationshipName": "Is associated with", + "sdkClient": { + "type": "elbV1", + "method": "getLoadBalancerInstances", + "argumentPaths": ["@"] + }, + "path": "@", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::Events::Rule.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::Events::Rule.json new file mode 100644 index 00000000..e8003a63 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::Events::Rule.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "type": "AWS::Events::Rule", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with Role", + "path": "Targets[*].RoleArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "Targets[*].[Arn, EcsParameters.TaskDefinitionArn]", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::IAM::InstanceProfile.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::IAM::InstanceProfile.json new file mode 100644 index 00000000..9381a4c6 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::IAM::InstanceProfile.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "type": "AWS::IAM::InstanceProfile", + "relationships": { + "descriptors": [ + { + "resourceType": "AWS::IAM::Role", + "relationshipName": "Is associated with Role", + "path": "Roles", + "identifierType": "resourceName" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::Lambda::Function.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::Lambda::Function.json new file mode 100644 index 00000000..60869651 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::Lambda::Function.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "type": "AWS::Lambda::Function", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "deadLetterConfig.targetArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "kmsKeyArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "fileSystemConfigs[*].arn", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::MSK::Cluster.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::MSK::Cluster.json new file mode 100644 index 00000000..d3c91023 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::MSK::Cluster.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "type": "AWS::MSK::Cluster", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is contained in", + "resourceType": "AWS::EC2::Subnet", + "path": "BrokerNodeGroupInfo.ClientSubnets", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "BrokerNodeGroupInfo.SecurityGroups", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowEntitlement.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowEntitlement.json new file mode 100644 index 00000000..e07ea6d2 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowEntitlement.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "type": "AWS::MediaConnect::FlowEntitlement", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "FlowArn", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowSource.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowSource.json new file mode 100644 index 00000000..2e97d2ba --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowSource.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "type": "AWS::MediaConnect::FlowSource", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "FlowArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "EntitlementArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::MediaConnect::FlowVpcInterface", + "path": "VpcInterfaceName", + "identifierType": "resourceName" + }, + { + "relationshipName": "Is associated with Role", + "path": "Decryption.RoleArn", + "identifierType": "arn" + }, + { + "relationshipName": "Is associated with", + "path": "Decryption.SecretArn", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowVpcInterface.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowVpcInterface.json new file mode 100644 index 00000000..3a831418 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaConnect::FlowVpcInterface.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "type": "AWS::MediaConnect::FlowVpcInterface", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is contained in Subnet", + "resourceType": "AWS::EC2::Subnet", + "path": "SubnetId", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with SecurityGroup", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "SecurityGroupIds", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is attached to NetworkInterface", + "resourceType": "AWS::EC2::NetworkInterface", + "path": "NetworkInterfaceIds", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingConfiguration.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingConfiguration.json new file mode 100644 index 00000000..317700a0 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingConfiguration.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "type": "AWS::MediaPackage::PackagingConfiguration", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "PackagingGroupId", + "identifierType": "resourceId", + "resourceType":"AWS::MediaPackage::PackagingGroup" + }, + { + "relationshipName": "Is associated with", + "path": "*.Encryption.SpekeKeyProvider.RoleArn", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingGroup.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingGroup.json new file mode 100644 index 00000000..347eb01a --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::MediaPackage::PackagingGroup.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "type": "AWS::MediaPackage::PackagingGroup", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "Authorization.[CdnIdentifierSecret, SecretsRoleArn]", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::OpenSearch::Domain.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::OpenSearch::Domain.json new file mode 100644 index 00000000..5c7e203e --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::OpenSearch::Domain.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "type": "AWS::OpenSearch::Domain", + "relationships": { + "descriptors": [ + { + "relationshipName": "Is contained in", + "path": "VPCOptions.VPCId", + "resourceType": "AWS::EC2::VPC", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is contained in", + "resourceType": "AWS::EC2::Subnet", + "path": "VPCOptions.SubnetIds", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "resourceType": "AWS::EC2::SecurityGroup", + "path": "VPCOptions.SecurityGroupIds", + "identifierType": "resourceId" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/resourceTypes/AWS::S3::Bucket.json b/source/backend/discovery/src/schemas/resourceTypes/AWS::S3::Bucket.json new file mode 100644 index 00000000..ce7b65b7 --- /dev/null +++ b/source/backend/discovery/src/schemas/resourceTypes/AWS::S3::Bucket.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "type": "AWS::S3::Bucket", + "relationships": { + "rootPath": "@", + "descriptors": [ + { + "relationshipName": "Is associated with", + "path": "supplementaryConfiguration.BucketLoggingConfiguration.destinationBucketName", + "resourceType": "AWS::S3::Bucket", + "identifierType": "resourceId" + }, + { + "relationshipName": "Is associated with", + "path": "supplementaryConfiguration.BucketNotificationConfiguration.configurations.*.[functionARN, topicARN, queueARN]", + "identifierType": "arn" + } + ] + } +} \ No newline at end of file diff --git a/source/backend/discovery/src/schemas/schema.json b/source/backend/discovery/src/schemas/schema.json new file mode 100644 index 00000000..de9963ba --- /dev/null +++ b/source/backend/discovery/src/schemas/schema.json @@ -0,0 +1,114 @@ +{ + "type": "object", + "properties": { + "version": { + "type": "integer" + }, + "rootPath": { + "type": "string" + }, + "type": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$" + }, + "relationships": { + "type": "object", + "properties": { + "descriptors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "pattern": "^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$" + }, + "relationshipName": { + "anyOf": [ + { + "type": "string", + "enum": [ + "Is associated with", + "Is contained in", + "Contains", + "Is attached to" + ] + }, + { + "type": "string", + "pattern": "^Is associated with(\\s(Vpc|Subnet|NetworkInterface|SecurityGroup|Role|Volume))?$" + }, + { + "type": "string", + "pattern": "^Is contained in(\\s(Vpc|Subnet|NetworkInterface|SecurityGroup|Role|Volume))?$" + }, + { + "type": "string", + "pattern": "^Contains(\\s(Vpc|Subnet|NetworkInterface|SecurityGroup|Role|Volume))?$" + }, + { + "type": "string", + "pattern": "^Is attached to(\\s(Vpc|Subnet|NetworkInterface|SecurityGroup|Role|Volume))?$" + } + ] + }, + "sdkClient": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ecs", + "elbV1", + "elbV2" + ] + }, + "method": { + "type": "string" + }, + "argumentPaths": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "method", + "argumentPaths" + ] + }, + "path": { + "type": "string" + }, + "identifierType": { + "type": "string", + "enum": [ + "arn", + "resourceId", + "resourceName", + "endpoint" + + ] + } + }, + "required": [ + "relationshipName", + "path", + "identifierType" + ] + } + } + }, + "required": [ + "descriptors" + ] + } + }, + "required": [ + "version", + "type", + "relationships" + ] +} \ No newline at end of file diff --git a/source/backend/discovery/test/additionalRelationships.test.mjs b/source/backend/discovery/test/additionalRelationships.test.mjs new file mode 100644 index 00000000..e07fff30 --- /dev/null +++ b/source/backend/discovery/test/additionalRelationships.test.mjs @@ -0,0 +1,2967 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import { + AWS_API_GATEWAY_METHOD, + AWS_DYNAMODB_TABLE, + AWS_EC2_NETWORK_INTERFACE, + AWS_EC2_VPC, + AWS_ECS_CLUSTER, + AWS_ECS_SERVICE, + AWS_ECS_TASK, + AWS_ECS_TASK_DEFINITION, + AWS_ELASTICSEARCH_DOMAIN, + AWS_LAMBDA_FUNCTION, + AWS_IAM_ROLE, + AWS_IAM_USER, + IS_ASSOCIATED_WITH, + IS_ATTACHED_TO, + IS_CONTAINED_IN, + AWS_CODEBUILD_PROJECT, + AWS_EC2_LAUNCH_TEMPLATE, + AWS_EC2_NAT_GATEWAY, + AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER, + AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_EC2_ROUTE_TABLE, + AWS_EC2_SUBNET, + AWS_EC2_SECURITY_GROUP, + AWS_EC2_TRANSIT_GATEWAY, + AWS_EC2_TRANSIT_GATEWAY_ATTACHMENT, + AWS_EC2_TRANSIT_GATEWAY_ROUTE_TABLE, + CONTAINS, + AWS_EC2_INTERNET_GATEWAY, + AWS_EC2_VPC_ENDPOINT, + AWS_RDS_DB_INSTANCE, + AWS_EFS_ACCESS_POINT, + AWS_KINESIS_STREAM, + AWS_EC2_INSTANCE, + AWS_CLOUDFRONT_DISTRIBUTION, + AWS_CLOUDFRONT_STREAMING_DISTRIBUTION, + AWS_EKS_CLUSTER, + AWS_EKS_NODE_GROUP, + AWS_AUTOSCALING_AUTOSCALING_GROUP, + AWS_AUTOSCALING_WARM_POOL, + AWS_EFS_FILE_SYSTEM, + AWS_IAM_AWS_MANAGED_POLICY, + AWS_S3_BUCKET, + AWS_OPENSEARCH_DOMAIN, + AWS_RDS_DB_CLUSTER, + AWS_REDSHIFT_CLUSTER, + SUBNET, + VPC, + AWS_EC2_SPOT_FLEET, + AWS_COGNITO_USER_POOL, + AWS_MSK_CLUSTER, + AWS_SNS_TOPIC, + AWS_SQS_QUEUE, + SECURITY_GROUP, + AWS_IAM_INLINE_POLICY, + MULTIPLE_AVAILABILITY_ZONES, + AWS_EVENT_EVENT_BUS, + AWS_EVENT_RULE, + AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + AWS_IAM_INSTANCE_PROFILE, + AWS_APPSYNC_DATASOURCE, + AWS_MEDIA_CONNECT_FLOW_ENTITLEMENT, + AWS_MEDIA_CONNECT_FLOW_SOURCE, + AWS_MEDIA_CONNECT_FLOW_VPC_INTERFACE, + AWS_MEDIA_PACKAGE_PACKAGING_CONFIGURATION, + AWS_MEDIA_PACKAGE_PACKAGING_GROUP, + NETWORK_INTERFACE, +} from '../src/lib/constants.mjs'; +import {generate} from './generator.mjs'; +import * as additionalRelationships from '../src/lib/additionalRelationships/index.mjs'; + +const ROLE = 'Role'; +const INSTANCE = 'Instance'; + +describe('additionalRelationships', () => { + + const credentials = {accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken'}; + + const defaultMockAwsClient = { + createAppSyncClient() { + return { + listDataSources: async apiId => [], + } + }, + createLambdaClient() { + return { + getAllFunctions: async arn => [], + listEventSourceMappings: async arn => [] + } + }, + createEcsClient() { + return { + getAllClusterInstances: async arn => [] + } + }, + createEksClient() { + return { + getAllNodeGroups: async arn => [] + } + }, + createElbClient() { + return { + getLoadBalancerInstances: async resourceId => [] + } + }, + createElbV2Client() { + return { + describeTargetHealth: async arn => [] + } + }, + createSnsClient() { + return { + getAllSubscriptions: async () => [] + } + }, + createEc2Client() { + return { + getAllTransitGatewayAttachments: async () => [] + } + } + }; + + describe('addAdditionalRelationships', () => { + const addAdditionalRelationships = additionalRelationships.addAdditionalRelationships(new Map( + [[ + 'xxxxxxxxxxxx', + { + credentials, + regions: [ + 'eu-west-2', + 'us-east-1', + 'us-east-2' + ] + } + ]] + )); + + describe(AWS_API_GATEWAY_METHOD, () => { + + it('should ignore non-lambda relationships', async () => { + const {default: schema} = await import('./fixtures/relationships/apigateway/method/noLambda.json', {with: {type: 'json' }}); + const {method} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [method]); + const {relationships} = rels.find(r => r.resourceId === method.resourceId); + + assert.deepEqual(relationships, []); + }); + + it('should handle no method integration', async () => { + const {default: schema} = await import('./fixtures/relationships/apigateway/method/noMethodIntegration.json', {with: {type: 'json' }}); + const {method} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [method]); + const {relationships} = rels.find(r => r.resourceId === method.resourceId); + + assert.deepEqual(relationships, []); + }); + + it('should handle no method integration uri', async () => { + const {default: schema} = await import('./fixtures/relationships/apigateway/method/noMethodIntegrationUri.json', {with: {type: 'json' }}); + const {method} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [method]); + const {relationships} = rels.find(r => r.resourceId === method.resourceId); + + assert.deepEqual(relationships, []); + }); + + it('should add relationships for lambdas', async () => { + const {default: schema} = await import('./fixtures/relationships/apigateway/method/lambda.json', {with: {type: 'json' }}); + const {lambda, method} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [lambda, method]); + const {relationships} = rels.find(r => r.resourceId === method.resourceId); + + assert.deepEqual(relationships, [ + { + relationshipName: IS_ASSOCIATED_WITH, + arn: lambda.arn, + resourceType: AWS_LAMBDA_FUNCTION + } + ]); + }); + + }); + + describe(AWS_AUTOSCALING_AUTOSCALING_GROUP, () => { + + it('should add launch configuration relationship', async () => { + const {default: schema} = await import('./fixtures/relationships/asg/launchTemplate.json', {with: {type: 'json' }}); + const {asg, subnet, launchTemplate} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet, asg]); + + const {relationships} = rels.find(r => r.resourceId === asg.resourceId); + const actualLaunchTemplateRel = relationships.find(x => x.resourceId === launchTemplate.resourceId); + + assert.deepEqual(actualLaunchTemplateRel, { + resourceId: launchTemplate.resourceId, + relationshipName: IS_ASSOCIATED_WITH, + resourceType: AWS_EC2_LAUNCH_TEMPLATE + }); + }); + + it('should add networking relationship', async () => { + const {default: schema} = await import('./fixtures/relationships/asg/networking.json', {with: {type: 'json' }}); + const {vpc, asg, subnet1, subnet2} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet1, subnet2, asg]); + + const actualAsg = rels.find(r => r.resourceId === asg.resourceId); + const actualVpcRel = actualAsg.relationships.find(x => x.resourceId === vpc.resourceId); + + assert.strictEqual(actualAsg.vpcId, vpc.resourceId); + assert.strictEqual(actualAsg.availabilityZone, 'eu-west-2a,eu-west-2b'); + + assert.deepEqual(actualVpcRel, { + resourceId: vpc.resourceId, + relationshipName: IS_CONTAINED_IN + VPC, + resourceType: AWS_EC2_VPC + }); + }); + + it('should handle networking relationship when subnet has not been ingested', async () => { + const {default: schema} = await import('./fixtures/relationships/asg/networking.json', {with: {type: 'json' }}); + const {vpc, asg} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [asg]); + + const actualAsg = rels.find(r => r.resourceId === asg.resourceId); + const actualVpcRel = actualAsg.relationships.find(x => x.resourceId === vpc.resourceId); + + assert.notExists(actualAsg.vpcId); + assert.strictEqual(actualAsg.availabilityZone, MULTIPLE_AVAILABILITY_ZONES); + + assert.deepEqual(actualVpcRel); + }); + + }); + + describe(AWS_AUTOSCALING_WARM_POOL, () => { + + it('should add relationship to autoscaling group', async () => { + const {default: schema} = await import('./fixtures/relationships/asg/warmPool/configuration.json', {with: {type: 'json' }}); + const {warmPool, asg} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [warmPool]); + + const {relationships} = rels.find(r => r.arn === warmPool.arn); + const actualAsgRel = relationships.find(x => x.resourceName === asg.resourceName); + + assert.deepEqual(actualAsgRel, { + resourceName: asg.resourceName, + relationshipName: IS_ASSOCIATED_WITH, + resourceType: AWS_AUTOSCALING_AUTOSCALING_GROUP + }); + }); + + }); + + describe(AWS_CLOUDFRONT_DISTRIBUTION, () => { + + it('should add regiun for s3 buckets', async () => { + const {default: schema} = await import('./fixtures/relationships/cloudfront/distribution/s3.json', {with: {type: 'json' }}); + const {cfDistro, s3} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [cfDistro, s3]); + const {relationships} = rels.find(r => r.resourceId === cfDistro.resourceId); + + assert.deepEqual(relationships, [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: s3.resourceId, + resourceType: AWS_S3_BUCKET, + arn: s3.arn + } + ]); + }); + + it('should add relationships to ELBs', async () => { + const {default: schema} = await import('./fixtures/relationships/cloudfront/distribution/elb.json', {with: {type: 'json' }}); + const {cfDistro, elb} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [cfDistro, elb]); + const {relationships} = rels.find(r => r.resourceId === cfDistro.resourceId); + + assert.deepEqual(relationships, [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: elb.resourceId, + resourceType: AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER, + awsRegion: elb.awsRegion + } + ]); + }); + + it('should add relationships to ALBs/NLBs', async () => { + const {default: schema} = await import('./fixtures/relationships/cloudfront/distribution/alb.json', {with: {type: 'json' }}); + const {cfDistro, alb} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [cfDistro, alb]); + const {relationships} = rels.find(r => r.resourceId === cfDistro.resourceId); + + assert.deepEqual(relationships, [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: alb.resourceId, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER, + awsRegion: alb.awsRegion + } + ]); + }); + + }); + + describe(AWS_CLOUDFRONT_STREAMING_DISTRIBUTION, () => { + + it('should add region for s3 buckets', async () => { + const {default: schema} = await import('./fixtures/relationships/cloudfrontStreamingDistribution/s3.json', {with: {type: 'json' }}); + const {cfStreamingDistro, s3} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [cfStreamingDistro, s3]); + const {relationships} = rels.find(r => r.resourceId === cfStreamingDistro.resourceId); + + assert.deepEqual(relationships, [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: s3.resourceId, + resourceType: AWS_S3_BUCKET, + arn: s3.arn + } + ]); + }); + + }); + + describe(AWS_DYNAMODB_TABLE, () => { + + it('should add relationship from table to stream', async () => { + const {default: schema} = await import('./fixtures/relationships/dynamodb/table.json', {with: {type: 'json' }}); + const {table} = generate(schema); + const rels = await addAdditionalRelationships(defaultMockAwsClient, [table]); + const actual = rels.find(r => r.resourceType === AWS_DYNAMODB_TABLE); + + assert.deepEqual(actual.relationships, [ + { + relationshipName: IS_ASSOCIATED_WITH, + arn: table.configuration.latestStreamArn + } + ]); + }); + + }); + + describe(AWS_EC2_NETWORK_INTERFACE, () => { + + it('should add vpc information', async () => { + const {default: schema} = await import('./fixtures/relationships/eni/vpcInfo.json', {with: {type: 'json' }}); + const {vpc, subnet, eni} = generate(schema); + + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eni]); + const actual = rels.find(r => r.resourceType === AWS_EC2_NETWORK_INTERFACE); + + assert.strictEqual(actual.vpcId, vpc.resourceId); + assert.strictEqual(actual.subnetId, subnet.resourceId); + }); + + it('should add eni relationships for Opensearch clusters', async () => { + const {default: schema} = await import('./fixtures/relationships/eni/opensearch.json', {with: {type: 'json' }}); + const {eni, opensearch} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eni]); + const actual = rels[0]; + const actualOpensearchRel = actual.relationships.find(r => r.arn === opensearch.arn); + + assert.deepEqual(actualOpensearchRel, { + relationshipName: IS_ATTACHED_TO, + arn: opensearch.arn + }) + }); + + it('should add eni relationships for nat gateways', async () => { + const {default: schema} = await import('./fixtures/relationships/eni/natGateway.json', {with: {type: 'json' }}); + const {eni} = generate(schema); + + const expectedNatGatewayResourceId = 'nat-01234567890abcdef'; + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eni]); + const actual = rels[0]; + const actualNatGatewayRel = actual.relationships.find(r => r.resourceId === expectedNatGatewayResourceId); + + assert.deepEqual(actualNatGatewayRel, { + relationshipName: IS_ATTACHED_TO, + resourceId: expectedNatGatewayResourceId, + resourceType: AWS_EC2_NAT_GATEWAY + }) + }); + + + it('should add eni relationships for vpc endpoints', async () => { + const {default: schema} = await import('./fixtures/relationships/eni/vpcEndpoint.json', {with: {type: 'json' }}); + const {eni, vpcEndpoint} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eni]); + const actual = rels.find(x => x.resourceId === eni.resourceId); + const actualNatGatewayRel = actual.relationships.find(r => r.resourceId === vpcEndpoint.resourceId); + + assert.deepEqual(actualNatGatewayRel, { + relationshipName: IS_ATTACHED_TO, + resourceId: vpcEndpoint.resourceId, + resourceType: AWS_EC2_VPC_ENDPOINT + }) + }); + + it('should add eni relationships for ALBs', async () => { + const {default: schema} = await import('./fixtures/relationships/eni/alb.json', {with: {type: 'json' }}); + const {eni, alb} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eni]); + const actual = rels[0]; + const actualAlbRel = actual.relationships.find(r => r.arn === alb.arn); + + assert.deepEqual(actualAlbRel, { + relationshipName: IS_ATTACHED_TO, + arn: alb.arn + }) + }); + + it('should add eni relationships for lambda functions', async () => { + const {default: schema} = await import('./fixtures/relationships/eni/lambda.json', {with: {type: 'json' }}); + const {eni} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eni]); + const actual = rels[0]; + + const expectedLambdaResourceId = 'testLambda'; + + const actualLambdaRel = actual.relationships.find(r => r.resourceId === expectedLambdaResourceId); + + assert.deepEqual(actualLambdaRel, { + relationshipName: IS_ATTACHED_TO, + resourceId: expectedLambdaResourceId, + resourceType: AWS_LAMBDA_FUNCTION + }) + }); + + }); + + describe(AWS_EC2_ROUTE_TABLE, () => { + + it('should ni relationships for vpc endpoints, nat gateways and Internet gateways', async () => { + const {default: schema} = await import('./fixtures/relationships/routeTable/allRelationships.json', {with: {type: 'json' }}); + const {routeTable} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [routeTable]); + + const {relationships} = rels[0]; + + assert.deepEqual(relationships[0], { + relationshipName: CONTAINS, + resourceId: routeTable.configuration.routes[0].gatewayId, + resourceType: AWS_EC2_INTERNET_GATEWAY + }); + + assert.deepEqual(relationships[1], { + relationshipName: CONTAINS, + resourceId: routeTable.configuration.routes[1].gatewayId, + resourceType: AWS_EC2_VPC_ENDPOINT + }); + + assert.deepEqual(relationships[2], { + relationshipName: CONTAINS, + resourceId: routeTable.configuration.routes[2].natGatewayId, + resourceType: AWS_EC2_NAT_GATEWAY + }); + + }); + + }); + + describe(AWS_EC2_SECURITY_GROUP, () => { + + it('should add relationships for security group in ingress', async () => { + const {default: schema} = await import('./fixtures/relationships/securityGroup/ingress.json', {with: {type: 'json' }}); + const {inSecurityGroup1, inSecurityGroup2, securityGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [securityGroup]); + const {relationships} = rels.find(r => r.resourceId === securityGroup.resourceId); + + assert.deepEqual(relationships, [ + { + resourceId: inSecurityGroup1.resourceId, + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceType: AWS_EC2_SECURITY_GROUP + }, { + resourceId: inSecurityGroup2.resourceId, + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceType: AWS_EC2_SECURITY_GROUP + } + ]); + }); + + it('should add relationships for security group in egress', async () => { + const {default: schema} = await import('./fixtures/relationships/securityGroup/egress.json', {with: {type: 'json' }}); + const {outSecurityGroup1, outSecurityGroup2, securityGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [securityGroup]); + const {relationships} = rels.find(r => r.resourceId === securityGroup.resourceId); + + assert.deepEqual(relationships, [ + { + resourceId: outSecurityGroup1.resourceId, + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceType: AWS_EC2_SECURITY_GROUP + }, { + resourceId: outSecurityGroup2.resourceId, + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceType: AWS_EC2_SECURITY_GROUP + } + ]); + }); + + }); + + describe(AWS_EC2_SUBNET, () => { + + it('should add vpc information', async () => { + const {default: schema} = await import('./fixtures/relationships/subnet/vpcInfo.json', {with: {type: 'json' }}); + const {subnet, vpc, routeTable} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet, routeTable]); + const actualSubnet = rels.find(x => x.resourceId === subnet.resourceId); + + assert.strictEqual(actualSubnet.vpcId, vpc.resourceId); + assert.strictEqual(actualSubnet.subnetId, subnet.resourceId); + }); + + it('should identify public subnets', async () => { + const {default: schema} = await import('./fixtures/relationships/subnet/public.json', {with: {type: 'json' }}); + const {subnet, routeTable} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet, routeTable]); + const actualSubnet = rels.find(x => x.resourceId === subnet.resourceId); + + assert.strictEqual(actualSubnet.private, false); + }); + + it('should identify private subnets', async () => { + const {default: schema} = await import('./fixtures/relationships/subnet/private.json', {with: {type: 'json' }}); + const {subnet, routeTable} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet, routeTable]); + const actualSubnet = rels.find(x => x.resourceId === subnet.resourceId); + + assert.strictEqual(actualSubnet.private, true); + }); + + }); + + describe(AWS_EC2_TRANSIT_GATEWAY, () => { + + it('should add relationships to routetables', async () => { + const {default: schema} = await import('./fixtures/relationships/transitgateway/routetables.json', {with: {type: 'json' }}); + const {tgw, tgwRouteTable1, tgwRouteTable2} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [tgw]); + + const {relationships} = rels.find(x => x.resourceId === tgw.resourceId); + + const actualTgwRouteTableRel1 = relationships.find(x => x.resourceId === tgwRouteTable1.resourceId); + const actualTgwRouteTableRel2 = relationships.find(x => x.resourceId === tgwRouteTable2.resourceId); + + assert.deepEqual(actualTgwRouteTableRel1, { + relationshipName: IS_CONTAINED_IN, + resourceId: tgwRouteTable1.resourceId, + resourceType: AWS_EC2_TRANSIT_GATEWAY_ROUTE_TABLE + }); + + assert.deepEqual(actualTgwRouteTableRel2, { + relationshipName: IS_CONTAINED_IN, + resourceId: tgwRouteTable2.resourceId, + resourceType: AWS_EC2_TRANSIT_GATEWAY_ROUTE_TABLE + }); + }); + + }); + + describe(AWS_EC2_TRANSIT_GATEWAY_ATTACHMENT, () => { + const accountIdX = 'xxxxxxxxxxxx'; + const accountIdZ = 'zzzzzzzzzzzz'; + const euWest2 = 'eu-west-2'; + + const addAdditionalRelationships = additionalRelationships.addAdditionalRelationships(new Map( + [[ + accountIdX, + { + credentials, + regions: [ + 'eu-west-2' + ] + } + ], [ + accountIdZ, + { + credentials, + regions: [ + 'eu-west-2' + ] + } + ]] + )); + + it('should add vpc relationships to transit gateway attachments', async () => { + const {default: schema} = await import('./fixtures/relationships/transitgateway/attachments/vpc.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, subnet3, tgw, tgwAttachment, tgwAttachmentApi} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createEc2Client(credentials, region) { + return { + getAllTransitGatewayAttachments: async arn => { + return credentials[0] = 'zzzzzzzzzzzz' && region === tgwAttachment.awsRegion ? [ + tgwAttachmentApi + ] : [] + } + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [tgwAttachment]); + + const {relationships} = rels.find(x => x.resourceId === tgwAttachment.resourceId); + + const actualTgwRel = relationships.find(x => x.resourceId === tgw.resourceId); + const actualVpcRel = relationships.find(x => x.resourceId === vpc.resourceId); + const actualSubnet1Rel = relationships.find(x => x.resourceId === subnet1.resourceId); + const actualSubnet2Rel = relationships.find(x => x.resourceId === subnet2.resourceId); + const actualSubnet3Rel = relationships.find(x => x.resourceId === subnet3.resourceId); + + assert.deepEqual(actualTgwRel, { + relationshipName: IS_ATTACHED_TO, + resourceId: tgw.resourceId, + resourceType: AWS_EC2_TRANSIT_GATEWAY, + awsRegion: euWest2, + accountId: accountIdX + }); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_ASSOCIATED_WITH + `${VPC}`, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC, + awsRegion: euWest2, + accountId: accountIdZ + }); + + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_ASSOCIATED_WITH + 'Subnet', + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET, + awsRegion: euWest2, + accountId: accountIdZ + }); + + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_ASSOCIATED_WITH + 'Subnet', + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET, + awsRegion: euWest2, + accountId: accountIdZ + }); + + assert.deepEqual(actualSubnet3Rel, { + relationshipName: IS_ASSOCIATED_WITH + 'Subnet', + resourceId: subnet3.resourceId, + resourceType: AWS_EC2_SUBNET, + awsRegion: euWest2, + accountId: accountIdZ + }); + + }); + + }); + + describe(AWS_EVENT_EVENT_BUS, () => { + + it('should add relationships for event bus rules', async () => { + const {default: schema} = await import('./fixtures/relationships/events/eventBus/bus.json', {with: {type: 'json' }}); + const {eventBus1, eventBus2, eventRule1, eventRule2} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eventBus1, eventBus2, eventRule1, eventRule2]); + const {relationships: eventBus1Rel} = rels.find(r => r.arn === eventBus1.arn); + const {relationships: eventBus2Rel} = rels.find(r => r.arn === eventBus2.arn); + + assert.deepEqual(eventBus1Rel, [ + { + arn: 'eventRuleArn1', + relationshipName: IS_ASSOCIATED_WITH, + } + ]); + + assert.deepEqual(eventBus2Rel, [ + { + arn: 'eventRuleArn2', + relationshipName: IS_ASSOCIATED_WITH, + } + ]); + }); + }); + + describe(AWS_EVENT_RULE, () => { + + it('should add relationships for event bus rules', async () => { + const {default: schema} = await import('./fixtures/relationships/events/rule/rules.json', {with: {type: 'json' }}); + const {eventRule1, eventRule2} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eventRule1, eventRule2]); + const {relationships: eventRule1Rel} = rels.find(r => r.arn === eventRule1.arn); + const {relationships: eventRule2Rel} = rels.find(r => r.arn === eventRule2.arn); + + const ruleTarget1Rel = eventRule1Rel.find(r => r.arn === 'ruleTargetArn1'); + const ruleTarget1RoleRel = eventRule1Rel.find(r => r.arn === 'roleArn1'); + const ruleTarget2Rel = eventRule2Rel.find(r => r.arn === 'clusterArn'); + const ruleTaskTarget2Rel = eventRule2Rel.find(r => r.arn === 'taskDefinitionArn'); + const ruleTarget2RoleRel = eventRule2Rel.find(r => r.arn === 'roleArn2'); + + assert.deepEqual(ruleTarget1Rel, { + arn: 'ruleTargetArn1', + relationshipName: IS_ASSOCIATED_WITH, + }); + + assert.deepEqual(ruleTarget1RoleRel, { + arn: 'roleArn1', + relationshipName: IS_ASSOCIATED_WITH + ROLE + }); + + assert.deepEqual(ruleTarget2Rel, { + arn: 'clusterArn', + relationshipName: IS_ASSOCIATED_WITH, + }); + + assert.deepEqual(ruleTaskTarget2Rel, { + arn: 'taskDefinitionArn', + relationshipName: IS_ASSOCIATED_WITH, + }); + + assert.deepEqual(ruleTarget2RoleRel, { + arn: 'roleArn2', + relationshipName: IS_ASSOCIATED_WITH + ROLE + }); + }); + }) + + describe(AWS_LAMBDA_FUNCTION, () => { + + it('should not add additional relationships for Lambda functions with no vpc', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/noVpc.json', {with: {type: 'json' }}); + const {lambda} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [lambda]); + + const actual = rels.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + + assert.deepEqual(actual.relationships, []); + }); + + it('should add all relationships contained in lambda configuration field', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/configuration.json', {with: {type: 'json' }}); + const {lambda} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [lambda]); + + const {relationships} = rels.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + + const actualDlq = relationships.find(r => r.arn === lambda.configuration.deadLetterConfig.targetArn); + const actualKms = relationships.find(r => r.arn === lambda.configuration.kmsKeyArn); + + assert.deepEqual(actualDlq, { + relationshipName: IS_ASSOCIATED_WITH, + arn: lambda.configuration.deadLetterConfig.targetArn + }); + assert.deepEqual(actualKms, { + relationshipName: IS_ASSOCIATED_WITH, + arn: lambda.configuration.kmsKeyArn + }); + }); + + it('should add VPC relationships for Lambda functions', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/vpc.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, lambda} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet1, subnet2, lambda]); + + const actual = rels.find(r => r.resourceId === lambda.resourceId); + const actualVpcRel = actual.relationships.find(r => r.resourceId === vpc.resourceId); + const actualSubnet1Rel = actual.relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSubnet2Rel = actual.relationships.find(r => r.resourceId === subnet2.resourceId); + + assert.strictEqual(actual.availabilityZone, `${subnet1.availabilityZone},${subnet2.availabilityZone}`); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET + }); + }); + + it('should handle VPC relationships for Lambda functions when subnets have not been ingested', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/vpc.json', {with: {type: 'json' }}); + const {vpc, lambda} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [lambda]); + + const actual = rels.find(r => r.resourceId === lambda.resourceId); + const actualVpcRel = actual.relationships.find(r => r.resourceId === vpc.resourceId); + + assert.strictEqual(actual.availabilityZone, 'Not Applicable'); + + assert.notExists(actualVpcRel); + }); + + it('should add VPC relationships for Lambda functions with efs', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/efs.json', {with: {type: 'json' }}); + const {subnet1, subnet2, lambda, efs1, efs2} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet1, subnet2, lambda, efs1, efs2]); + + const actual = rels.find(r => r.resourceId === lambda.resourceId); + const actualEfsRel1 = actual.relationships.find(r => r.arn === efs1.arn); + const actualEfsRel2 = actual.relationships.find(r => r.arn === efs2.arn); + + assert.deepEqual(actualEfsRel1, { + relationshipName: IS_ASSOCIATED_WITH, + arn: efs1.arn + }); + assert.deepEqual(actualEfsRel2, { + relationshipName: IS_ASSOCIATED_WITH, + arn: efs2.arn + }); + }); + + it('should return additional relationships for Lambda functions with event mappings', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/eventMappings.json', {with: {type: 'json' }}); + const {lambda, kinesis} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createLambdaClient(_, region) { + return { + getAllFunctions: async arn => [], + listEventSourceMappings: async arn => { + return region === lambda.awsRegion ? [{ + EventSourceArn: kinesis.arn, FunctionArn: lambda.arn + }] : [] + } + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [lambda, kinesis]); + + const actual = rels.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + + assert.deepEqual(actual.relationships, [{ + relationshipName: IS_ASSOCIATED_WITH, + arn: kinesis.arn, + resourceType: AWS_KINESIS_STREAM + }]); + }); + + it('should handle errors when encrypted environment variables are present', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/encryptedEnvVar.json', {with: {type: 'json' }}); + const {lambda} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createLambdaClient(_, region) { + return { + getAllFunctions: async arn => { + if(region === lambda.awsRegion) { + return [{ + FunctionArn: lambda.arn, + Environment: { + Error: { + ErrorCode: 'AccessDeniedException', + Message: 'Error' + } + } + }] + } + return []; + }, + listEventSourceMappings: async arn => [] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [lambda]); + + const {relationships} = rels.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + + assert.lengthOf(relationships, 0); + }); + + it('should handle when Environment field is set to null', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/envVar.json', {with: {type: 'json' }}); + const {resourceIdResource, resourceNameResource, arnResource, lambda} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createLambdaClient(_, region) { + return { + getAllFunctions: async arn => { + if(region === lambda.awsRegion) { + return [{ + FunctionArn: lambda.arn, + Environment: { + Variables: { + resourceIdVar: resourceIdResource.resourceId, + resourceNameVar: resourceNameResource.resourceName, + arnVar: arnResource.arn + } + } + }, { + FunctionArn: lambda.arn, + Environment: null + } + ] + } + return []; + }, + listEventSourceMappings: async arn => [] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [resourceIdResource, resourceNameResource, arnResource, lambda]); + + const {relationships} = rels.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + const actualResourceIdResourceRel = relationships.find(r => r.arn === resourceIdResource.arn); + const actualResourceNameResourceRel = relationships.find(r => r.arn === resourceNameResource.arn); + const actualArnResourceRel = relationships.find(r => r.arn === arnResource.arn); + + assert.deepEqual(actualResourceIdResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: resourceIdResource.arn, + resourceType: AWS_S3_BUCKET + }); + assert.deepEqual(actualResourceNameResourceRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: resourceNameResource.arn, + resourceType: AWS_IAM_ROLE + }); + assert.deepEqual(actualArnResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: arnResource.arn, + resourceType: AWS_S3_BUCKET + }); + }); + + it('should return additional non-db relationships for Lambda functions with environment variables', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/envVar.json', {with: {type: 'json' }}); + const {resourceIdResource, resourceNameResource, arnResource, lambda} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createLambdaClient(_, region) { + return { + getAllFunctions: async arn => { + if(region === lambda.awsRegion) { + return [{ + FunctionArn: lambda.arn, + Environment: { + Variables: { + resourceIdVar: resourceIdResource.resourceId, + resourceNameVar: resourceNameResource.resourceName, + arnVar: arnResource.arn + } + } + }] + } + return []; + }, + listEventSourceMappings: async arn => [] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [resourceIdResource, resourceNameResource, arnResource, lambda]); + + const {relationships} = rels.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + const actualResourceIdResourceRel = relationships.find(r => r.arn === resourceIdResource.arn); + const actualResourceNameResourceRel = relationships.find(r => r.arn === resourceNameResource.arn); + const actualArnResourceRel = relationships.find(r => r.arn === arnResource.arn); + + assert.deepEqual(actualResourceIdResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: resourceIdResource.arn, + resourceType: AWS_S3_BUCKET + }); + assert.deepEqual(actualResourceNameResourceRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: resourceNameResource.arn, + resourceType: AWS_IAM_ROLE + }); + assert.deepEqual(actualArnResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: arnResource.arn, + resourceType: AWS_S3_BUCKET + }); + }); + + it('should return additional db relationships for Lambda functions with environment variables', async () => { + const {default: schema} = await import('./fixtures/relationships/lambda/dbEnvVar.json', {with: {type: 'json' }}); + const {elasticsearch, opensearch, rdsCluster, rdsInstance, redshiftCluster, lambda} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createLambdaClient(_, region) { + return { + getAllFunctions: async arn => { + if(region === lambda.awsRegion) { + return [{ + FunctionArn: lambda.arn, + Environment: { + Variables: { + elasticsearchVar: elasticsearch.configuration.endpoints.vpc, + opensearchVar: opensearch.configuration.Endpoints.vpc, + rdsClusterVar: rdsCluster.configuration.endpoint.value, + rdsInstanceVar: rdsInstance.configuration.endpoint.address, + redshiftClusterVar: redshiftCluster.configuration.endpoint.address + } + } + }] + } + return []; + }, + listEventSourceMappings: async arn => [] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [ + elasticsearch, opensearch, rdsCluster, rdsInstance, redshiftCluster, lambda + ]); + + const {relationships} = rels.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + const actualElasticsearchResourceRel = relationships.find(r => r.arn === elasticsearch.arn); + const actualOpensearchResourceRel = relationships.find(r => r.arn === opensearch.arn); + const actualRdsClusterRel = relationships.find(r => r.arn === rdsCluster.arn); + const actualRdsInstanceRel = relationships.find(r => r.arn === rdsInstance.arn); + const actualRedshiftClusterRel = relationships.find(r => r.arn === redshiftCluster.arn); + + assert.deepEqual(actualElasticsearchResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: elasticsearch.arn, + resourceType: AWS_ELASTICSEARCH_DOMAIN + }); + assert.deepEqual(actualOpensearchResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: opensearch.arn, + resourceType: AWS_OPENSEARCH_DOMAIN + }); + assert.deepEqual(actualRdsClusterRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: rdsCluster.arn, + resourceType: AWS_RDS_DB_CLUSTER + }); + assert.deepEqual(actualRdsInstanceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: rdsInstance.arn, + resourceType: AWS_RDS_DB_INSTANCE + }); + assert.deepEqual(actualRedshiftClusterRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: redshiftCluster.arn, + resourceType: AWS_REDSHIFT_CLUSTER + }); + }); + + }); + + describe(AWS_ECS_CLUSTER, () => { + + it('should not add relationships between cluster and ec2 instances when none are present', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/cluster/noInstances.json', {with: {type: 'json' }}); + const {ecsCluster} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ecsCluster]); + const {relationships} = rels.find(r => r.resourceId === ecsCluster.resourceId); + + assert.deepEqual(relationships, []); + }); + + it('should add all relationships contained in cluster configuration field', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/cluster/configuration.json', {with: {type: 'json' }}); + const {ecsCluster} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ecsCluster]); + const {relationships} = rels.find(r => r.resourceId === ecsCluster.resourceId); + + const actualS3LogBucket = relationships.find(rel => rel.resourceType === AWS_S3_BUCKET); + + assert.deepEqual(actualS3LogBucket, { + relationshipName: IS_ASSOCIATED_WITH, + resourceType: AWS_S3_BUCKET, + resourceId: ecsCluster.configuration.LogConfiguration.S3BucketName + }); + }); + + it('should add relationships between cluster and ec2 instances if present', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/cluster/instances.json', {with: {type: 'json' }}); + const {ec2Instance1, ec2Instance2, ecsCluster} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createEcsClient() { + return { + getAllClusterInstances: async arn => [ec2Instance1.resourceId, ec2Instance2.resourceId] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [ecsCluster]); + const {relationships} = rels.find(r => r.resourceId === ecsCluster.resourceId); + + assert.deepEqual(relationships, [ + { + relationshipName: CONTAINS + INSTANCE, + resourceId: ec2Instance1.resourceId, + resourceType: AWS_EC2_INSTANCE + }, { + relationshipName: CONTAINS + INSTANCE, + resourceId: ec2Instance2.resourceId, + resourceType: AWS_EC2_INSTANCE + } + ]); + }); + + }); + + describe(AWS_ECS_SERVICE, () => { + + it('should add cluster, role and task definition relationships ECS service', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/service/noVpc.json', {with: {type: 'json' }}); + const {ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition]); + + const {relationships} = rels.find(r => r.arn === ecsService.arn); + + const actualClusterRel = relationships.find(r => r.arn === ecsCluster.arn); + const actualIamRoleRel = relationships.find(r => r.arn === ecsServiceRole.arn); + const actualTaskRel = relationships.find(r => r.arn === ecsTaskDefinition.arn); + + assert.deepEqual(actualClusterRel, { + relationshipName: IS_CONTAINED_IN, + arn: ecsCluster.arn + }); + assert.deepEqual(actualIamRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: ecsServiceRole.arn + }); + assert.deepEqual(actualTaskRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: ecsTaskDefinition.arn + }); + }); + + it('should add alb target groups relationships ECS service', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/service/alb.json', {with: {type: 'json' }}); + const {alb, ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition]); + + const {relationships} = rels.find(r => r.resourceId === ecsService.resourceId); + + const actualAlbTgRel = relationships.find(r => r.arn === alb.arn); + + assert.deepEqual(actualAlbTgRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: alb.arn + }); + }); + + it('should add networking relationships for ECS service in vpc', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/service/vpc.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, securityGroup, ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + subnet1, subnet2, securityGroup, ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition + ]); + + const {relationships, ...service} = rels.find(r => r.resourceType === AWS_ECS_SERVICE); + + const actualSubnet1Rel = relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSubnet2Rel = relationships.find(r => r.resourceId === subnet2.resourceId); + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + const actualSgRel = relationships.find(r => r.resourceId === securityGroup.resourceId); + + assert.strictEqual(service.vpcId, vpc.resourceId); + assert.strictEqual(service.availabilityZone, `${subnet1.availabilityZone},${subnet2.availabilityZone}`); + + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualSgRel, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + }); + + it('should add networking relationships for ECS service when subnets have not been discovered', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/service/vpc.json', {with: {type: 'json' }}); + const {vpc, securityGroup, ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + securityGroup, ecsServiceRole, ecsCluster, ecsService, ecsTaskDefinition + ]); + + const {relationships, ...service} = rels.find(r => r.resourceType === AWS_ECS_SERVICE); + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + + assert.notExists(service.vpcId); + assert.strictEqual(service.availabilityZone, 'Regional'); + + assert.notExists(actualVpcRel); + }); + + }); + + describe(AWS_ECS_TASK, () => { + + it('should not get networking relationships for tasks not using awsvpc mode', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/cluster.json', {with: {type: 'json' }}); + const {ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition + ]); + + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + const actualClusterRel = relationships.find(r => r.resourceType === AWS_ECS_CLUSTER); + + assert.deepEqual(actualClusterRel, { + relationshipName: IS_CONTAINED_IN, + arn: ecsCluster.arn, + resourceType: AWS_ECS_CLUSTER + }); + }); + + it('should handle missing task definition', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/missingTaskDefinition.json', {with: {type: 'json' }}); + const {ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask + ]); + + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + const actualClusterRel = relationships.find(r => r.resourceType === AWS_ECS_CLUSTER); + + assert.deepEqual(actualClusterRel, { + relationshipName: IS_CONTAINED_IN, + arn: ecsCluster.arn, + resourceType: AWS_ECS_CLUSTER + }); + }); + + it('should add IAM role relationship for ECS tasks if present', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/roles.json', {with: {type: 'json' }}); + const { + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTaskDefinition, ecsTask + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTaskDefinition, ecsTask + ]); + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + + const actualTaskRoleRel = relationships.find(r => r.arn === ecsTaskRole.arn); + + const actualTaskExecutionRoleRel = relationships.find(r => r.arn === ecsTaskExecutionRole.arn); + + assert.deepEqual(actualTaskRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: ecsTaskRole.arn, + resourceType: AWS_IAM_ROLE + }); + assert.deepEqual(actualTaskExecutionRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: ecsTaskExecutionRole.arn, + resourceType: AWS_IAM_ROLE + }); + }); + + it('should add overriden task role relationships for ECS tasks', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/roleOverrides.json', {with: {type: 'json' }}); + const { + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTaskDefinition, ecsTask + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTaskDefinition, ecsTask + ]); + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + + const actualTaskRoleRel = relationships.find(r => r.arn === ecsTaskRole.arn); + + const actualTaskExecutionRoleRel = relationships.find(r => r.arn === ecsTaskExecutionRole.arn); + + assert.deepEqual(actualTaskRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: ecsTaskRole.arn, + resourceType: AWS_IAM_ROLE + }); + assert.deepEqual(actualTaskExecutionRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: ecsTaskExecutionRole.arn, + resourceType: AWS_IAM_ROLE + }); + }); + + it('should not get networking relationships for tasks not using awsvpc mode', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/noVpc.json', {with: {type: 'json' }}); + const {ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition + ]); + + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + + assert.deepEqual(relationships.filter(x => ![AWS_IAM_ROLE, AWS_ECS_CLUSTER].includes(x.resourceType)), []); + }); + + it('should get networking relationships for tasks using awsvpc mode', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/vpc.json', {with: {type: 'json' }}); + const { + vpc, ecsCluster, ecsTaskRole, ecsTaskExecutionRole, subnet, eni, ecsTask, ecsTaskDefinition + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, subnet, eni, ecsTask, ecsTaskDefinition + ]); + + const task = rels.find(r => r.resourceId === ecsTask.resourceId); + const eniRel = rels.find(r => r.resourceId === eni.resourceId); + + const actualTaskVpcRel = task.relationships.find(r => r.resourceId === vpc.resourceId); + const actualTaskSubnetRel = task.relationships.find(r => r.resourceId === subnet.resourceId); + const actualTaskEniRel = eniRel.relationships.find(r => r.resourceId === task.resourceId); + + assert.strictEqual(task.vpcId, vpc.resourceId); + assert.strictEqual(task.subnetId, subnet.resourceId); + + assert.deepEqual(actualTaskSubnetRel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualTaskVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualTaskEniRel, { + relationshipName: IS_ATTACHED_TO, + resourceId: task.resourceId, + resourceType: AWS_ECS_TASK + }); + }); + + it('should get non-db relationships for tasks with environment variables', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/envVars.json', {with: {type: 'json' }}); + const { + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition, + resourceIdResource, resourceNameResource, arnResource + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition, resourceIdResource, + resourceNameResource, arnResource + ]); + + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + + const actualResourceIdResourceRel = relationships.find(r => r.arn === resourceIdResource.arn); + const actualResourceNameResourceRel = relationships.find(r => r.arn === resourceNameResource.arn); + const actualArnResourceRel = relationships.find(r => r.arn === arnResource.arn); + + assert.deepEqual(actualResourceIdResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: resourceIdResource.arn, + resourceType: AWS_S3_BUCKET + }); + assert.deepEqual(actualResourceNameResourceRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: resourceNameResource.arn, + resourceType: AWS_IAM_ROLE + }); + assert.deepEqual(actualArnResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: arnResource.arn, + resourceType: AWS_S3_BUCKET + }); + }); + + it('should handle overridden environment variables', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/envVarOverrides.json', {with: {type: 'json' }}); + const { + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition, + resourceIdResource, overridenResource + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition, resourceIdResource, + overridenResource + ]); + + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + + const actualOverridenResourceResourceRel = relationships.find(r => r.arn === overridenResource.arn); + + assert.deepEqual(actualOverridenResourceResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: overridenResource.arn, + resourceType: AWS_S3_BUCKET + }); + }); + + it('should get db relationships for tasks with environment variables', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/task/dbEnvVars.json', {with: {type: 'json' }}); + const { + ecsCluster, elasticsearch, opensearch, rdsCluster, rdsInstance, redshiftCluster, + ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsCluster, elasticsearch, opensearch, rdsCluster, rdsInstance, redshiftCluster, + ecsTaskRole, ecsTaskExecutionRole, ecsTask, ecsTaskDefinition + ]); + + const {relationships} = rels.find(r => r.resourceId === ecsTask.resourceId); + const actualElasticsearchResourceRel = relationships.find(r => r.arn === elasticsearch.arn); + const actualOpensearchResourceRel = relationships.find(r => r.arn === opensearch.arn); + const actualRdsClusterRel = relationships.find(r => r.arn === rdsCluster.arn); + const actualRdsInstanceRel = relationships.find(r => r.arn === rdsInstance.arn); + const actualRedshiftClusterRel = relationships.find(r => r.arn === redshiftCluster.arn); + + assert.deepEqual(actualElasticsearchResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: elasticsearch.arn, + resourceType: AWS_ELASTICSEARCH_DOMAIN + }); + assert.deepEqual(actualOpensearchResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: opensearch.arn, + resourceType: AWS_OPENSEARCH_DOMAIN + }); + assert.deepEqual(actualRdsClusterRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: rdsCluster.arn, + resourceType: AWS_RDS_DB_CLUSTER + }); + assert.deepEqual(actualRdsInstanceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: rdsInstance.arn, + resourceType: AWS_RDS_DB_INSTANCE + }); + assert.deepEqual(actualRedshiftClusterRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: redshiftCluster.arn, + resourceType: AWS_REDSHIFT_CLUSTER + }); + }); + + }); + + describe(AWS_ECS_TASK_DEFINITION, () => { + + it('should not add IAM role relationship for ECS tasks definitions when absent', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/taskDefinitions/noRoles.json', {with: {type: 'json' }}); + const {ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ecsTaskDefinition]); + const {relationships} = rels.find(r => r.resourceId === ecsTaskDefinition.resourceId); + + assert.deepEqual(relationships, []); + }); + + it('should add efs file system and accesspoint relationships', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/taskDefinitions/efs.json', {with: {type: 'json' }}); + const {ecsTaskDefinition, efsAp, efsFs} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ecsTaskDefinition]); + const {relationships} = rels.find(r => r.resourceId === ecsTaskDefinition.resourceId); + + const actualEfsApRel = relationships.find(r => r.resourceId === efsAp.resourceId); + const actualEfsFsRel = relationships.find(r => r.resourceId === efsFs.resourceId); + + assert.deepEqual(actualEfsApRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceType: AWS_EFS_ACCESS_POINT, + resourceId: efsAp.resourceId + }); + + assert.deepEqual(actualEfsFsRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceType: AWS_EFS_FILE_SYSTEM, + resourceId: efsFs.resourceId + }); + }); + + it('should add IAM role relationship for ECS tasks definitions if present', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/taskDefinitions/roles.json', {with: {type: 'json' }}); + const {ecsTaskRole, ecsTaskExecutionRole, ecsTaskDefinition} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ecsTaskRole, ecsTaskExecutionRole, ecsTaskDefinition]); + const {relationships} = rels.find(r => r.resourceId === ecsTaskDefinition.resourceId); + + const actualTaskRoleRel = relationships.find(r => r.arn === ecsTaskRole.arn); + const actualTaskExecutionRoleRel = relationships.find(r => r.arn === ecsTaskExecutionRole.arn); + + assert.deepEqual(actualTaskRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: ecsTaskRole.arn + }); + assert.deepEqual(actualTaskExecutionRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: ecsTaskExecutionRole.arn + }); + }); + + it('should get non-db relationships for tasks with environment variables', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/taskDefinitions/envVars.json', {with: {type: 'json' }}); + const { + ecsTaskDefinition, resourceIdResource, resourceNameResource, arnResource + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + ecsTaskDefinition, resourceIdResource, resourceNameResource, arnResource + ]); + + const {relationships} = rels.find(r => r.resourceId === ecsTaskDefinition.resourceId); + + const actualResourceIdResourceRel = relationships.find(r => r.arn === resourceIdResource.arn); + const actualResourceNameResourceRel = relationships.find(r => r.arn === resourceNameResource.arn); + const actualArnResourceRel = relationships.find(r => r.arn === arnResource.arn); + + assert.deepEqual(actualResourceIdResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: resourceIdResource.arn, + resourceType: AWS_S3_BUCKET + }); + assert.deepEqual(actualResourceNameResourceRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: resourceNameResource.arn, + resourceType: AWS_IAM_ROLE + }); + assert.deepEqual(actualArnResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: arnResource.arn, + resourceType: AWS_S3_BUCKET + }); + }); + + it('should get db relationships for tasks with environment variables', async () => { + const {default: schema} = await import('./fixtures/relationships/ecs/taskDefinitions/dbEnvVars.json', {with: {type: 'json' }}); + const { + elasticsearch, opensearch, rdsCluster, rdsInstance, redshiftCluster, ecsTaskDefinition + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + elasticsearch, opensearch, rdsCluster, rdsInstance, redshiftCluster, ecsTaskDefinition + ]); + + const {relationships} = rels.find(r => r.arn === ecsTaskDefinition.arn); + const actualElasticsearchResourceRel = relationships.find(r => r.arn === elasticsearch.arn); + const actualOpensearchResourceRel = relationships.find(r => r.arn === opensearch.arn); + const actualRdsClusterRel = relationships.find(r => r.arn === rdsCluster.arn); + const actualRdsInstanceRel = relationships.find(r => r.arn === rdsInstance.arn); + const actualRedshiftClusterRel = relationships.find(r => r.arn === redshiftCluster.arn); + + assert.deepEqual(actualElasticsearchResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: elasticsearch.arn, + resourceType: AWS_ELASTICSEARCH_DOMAIN + }); + assert.deepEqual(actualOpensearchResourceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: opensearch.arn, + resourceType: AWS_OPENSEARCH_DOMAIN + }); + assert.deepEqual(actualRdsClusterRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: rdsCluster.arn, + resourceType: AWS_RDS_DB_CLUSTER + }); + assert.deepEqual(actualRdsInstanceRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: rdsInstance.arn, + resourceType: AWS_RDS_DB_INSTANCE + }); + assert.deepEqual(actualRedshiftClusterRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: redshiftCluster.arn, + resourceType: AWS_REDSHIFT_CLUSTER + }); + }); + + }); + + describe(AWS_EFS_FILE_SYSTEM, () => { + + it('should add KMS key relationship for EFS', async () => { + const {default: schema} = await import('./fixtures/relationships/efs/kms.json', {with: {type: 'json' }}); + const {efs, kms} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [kms, efs]); + + const {relationships} = rels.find(r => r.resourceId === efs.resourceId); + const actualKmsRel = relationships.find(r => r.arn === kms.arn); + + assert.deepEqual(actualKmsRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: kms.arn + }); + }); + + }); + + describe(AWS_EFS_ACCESS_POINT, () => { + + it('should add relationship to EFS file system', async () => { + const {default: schema} = await import('./fixtures/relationships/efs/accessPoint/efs.json', {with: {type: 'json' }}); + const {efs, accessPoint} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [accessPoint]); + + const {relationships} = rels.find(r => r.resourceId === accessPoint.resourceId); + const actualEfsRel = relationships.find(r => r.resourceId === efs.resourceId); + + assert.deepEqual(actualEfsRel, { + relationshipName: IS_ATTACHED_TO, + resourceId: efs.resourceId, + resourceType: AWS_EFS_FILE_SYSTEM + }); + }); + + }); + + describe(AWS_EKS_CLUSTER, () => { + + it('should add relationships for networking', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/cluster/networking.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, cluster, clusterRole} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + subnet1, subnet2, cluster, clusterRole + ]); + + const {availabilityZone, relationships} = rels.find(r => r.resourceId === cluster.resourceId); + + assert.strictEqual(availabilityZone, 'eu-west-2a,eu-west-2b'); + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + const actualSubnet1Rel = relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSubnet2Rel = relationships.find(r => r.resourceId === subnet2.resourceId); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET + }); + }); + + it('should add relationships for networking when subnets have not been discovered', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/cluster/networking.json', {with: {type: 'json' }}); + const {vpc, cluster, clusterRole} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + cluster, clusterRole + ]); + + const {availabilityZone, relationships} = rels.find(r => r.resourceId === cluster.resourceId); + assert.strictEqual(availabilityZone, 'Regional'); + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + + assert.notExists(actualVpcRel); + }); + + it('should add relationships for security groups', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/cluster/securityGroup.json', {with: {type: 'json' }}); + const {securityGroup1, securityGroup2, cluster, clusterRole} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + securityGroup1, securityGroup2, cluster, clusterRole + ]); + + const {relationships} = rels.find(r => r.resourceId === cluster.resourceId); + + const actualSgRel1 = relationships.find(r => r.resourceId === securityGroup1.resourceId); + const actualSgRel2 = relationships.find(r => r.resourceId === securityGroup2.resourceId); + + assert.deepEqual(actualSgRel1, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup1.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + assert.deepEqual(actualSgRel2, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup2.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + }); + + it('should add relationships for IAM roles', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/cluster/role.json', {with: {type: 'json' }}); + const {cluster, clusterRole} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + cluster, clusterRole + ]); + + const {relationships} = rels.find(r => r.resourceId === cluster.resourceId); + + const actualClusterRoleRel = relationships.find(r => r.arn === clusterRole.arn); + + assert.deepEqual(actualClusterRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: clusterRole.arn + }); + }); + + }); + + describe(AWS_EKS_NODE_GROUP, () => { + + it('should add relationships for networking', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/nodeGroup/networking.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, nodeRole, nodeGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + subnet1, subnet2, nodeRole, nodeGroup + ]); + + const {availabilityZone, relationships} = rels.find(r => r.resourceId === nodeGroup.resourceId); + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + const actualSubnet1Rel = relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSubnet2Rel = relationships.find(r => r.resourceId === subnet2.resourceId); + + assert.strictEqual(availabilityZone, 'eu-west-2a,eu-west-2b'); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET + }); + }); + + it('should add relationships for networking when subnets have not been discovered', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/nodeGroup/networking.json', {with: {type: 'json' }}); + const {vpc, nodeRole, nodeGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [nodeRole, nodeGroup]); + + const {availabilityZone, relationships} = rels.find(r => r.resourceId === nodeGroup.resourceId); + assert.strictEqual(availabilityZone, 'Regional'); + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + + assert.notExists(actualVpcRel); + }); + + it('should add relationships for security groups with launch template', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/nodeGroup/securityGroupLT.json', {with: {type: 'json' }}); + const {securityGroup, launchTemplate, nodeRole, nodeGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + nodeGroup, nodeRole + ]); + + const {relationships} = rels.find(r => r.resourceId === nodeGroup.resourceId); + + const actualSgRel = relationships.find(r => r.resourceId === securityGroup.resourceId); + const actualLaunchTemplateRel = relationships.find(r => r.resourceId === launchTemplate.resourceId); + + assert.deepEqual(actualSgRel, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + assert.deepEqual(actualLaunchTemplateRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: launchTemplate.resourceId, + resourceType: AWS_EC2_LAUNCH_TEMPLATE + }); + }); + + it('should add relationships for security groups without launch template', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/nodeGroup/securityGroup.json', {with: {type: 'json' }}); + const {securityGroup, nodeRole, nodeGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + nodeGroup, nodeRole + ]); + + const {relationships} = rels.find(r => r.resourceId === nodeGroup.resourceId); + + const actualSgRel = relationships.find(r => r.resourceId === securityGroup.resourceId); + + assert.deepEqual(actualSgRel, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + }); + + it('should add relationships with autoscaling groups', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/nodeGroup/asg.json', {with: {type: 'json' }}); + const {asg, nodeRole, nodeGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + nodeGroup, nodeRole, asg + ]); + + const {relationships} = rels.find(r => r.resourceId === nodeGroup.resourceId); + + const actualAsgRel = relationships.find(r => r.resourceId === asg.resourceId); + + assert.deepEqual(actualAsgRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: asg.resourceId, + resourceType: AWS_AUTOSCALING_AUTOSCALING_GROUP + }); + }); + + it('should add relationships for IAM role', async () => { + const {default: schema} = await import('./fixtures/relationships/eks/nodeGroup/role.json', {with: {type: 'json' }}); + const {nodeRole, nodeGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + nodeGroup, nodeRole + ]); + + const {relationships} = rels.find(r => r.resourceId === nodeGroup.resourceId); + + const actualNodeRole = relationships.find(r => r.arn === nodeRole.arn); + + assert.deepEqual(actualNodeRole, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: nodeRole.arn + }); + }); + + }); + + describe(AWS_MSK_CLUSTER, () => { + + it('should add relationships for networking', async () => { + const {default: schema} = await import('./fixtures/relationships/msk/serverful.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, securityGroup, cluster} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + subnet1, subnet2, cluster + ]); + + const {availabilityZone, relationships} = rels.find(r => r.resourceId === cluster.resourceId); + + assert.strictEqual(availabilityZone, 'eu-west-2a,eu-west-2b') + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + const actualSubnet1Rel = relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSubnet2Rel = relationships.find(r => r.resourceId === subnet2.resourceId); + const actualSecurityGroupRel = relationships.find(r => r.resourceId === securityGroup.resourceId); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSecurityGroupRel, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + }); + + it('should handle relationships for networking when subnets have not been discovered', async () => { + const {default: schema} = await import('./fixtures/relationships/msk/serverful.json', {with: {type: 'json' }}); + const {vpc, cluster} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [cluster]); + + const {availabilityZone, relationships} = rels.find(r => r.resourceId === cluster.resourceId); + assert.strictEqual(availabilityZone, 'Regional') + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + + assert.notExists(actualVpcRel); + + }); + + }); + + describe(AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER, () => { + + it('should not add relationships between elb and ec2 instances if not present', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/elb/instances.json', {with: {type: 'json' }}); + const {elb} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [elb]); + const {relationships} = rels.find(r => r.resourceId === elb.resourceId); + + assert.deepEqual(relationships, []); + }); + + it('should add relationships between elb and ec2 instances if present', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/elb/instances.json', {with: {type: 'json' }}); + const {ec2Instance1, ec2Instance2, elb} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createElbClient() { + return { + getLoadBalancerInstances: async arn => [ec2Instance1.resourceId, ec2Instance2.resourceId] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [elb]); + const {relationships} = rels.find(r => r.resourceId === elb.resourceId); + + assert.deepEqual(relationships, [ + { + relationshipName: IS_ASSOCIATED_WITH + INSTANCE, + resourceId: ec2Instance1.resourceId, + resourceType: AWS_EC2_INSTANCE + }, { + relationshipName: IS_ASSOCIATED_WITH + INSTANCE, + resourceId: ec2Instance2.resourceId, + resourceType: AWS_EC2_INSTANCE + } + ]); + }); + + }); + + describe(AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, () => { + + it('should add relationships with autoscaling groups', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/alb/targetGroup/asg.json', {with: {type: 'json' }}); + const {ec2Instance1, ec2Instance2, asg, targetGroup} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createElbV2Client() { + return { + describeTargetHealth: async arn => [ + {Target: {Id: ec2Instance1.resourceId}}, + {Target: {Id: ec2Instance2.resourceId}} + ] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [ + targetGroup, asg + ]); + + const {relationships} = rels.find(r => r.resourceId === targetGroup.resourceId); + + const actualAsgRel = relationships.find(r => r.resourceId === asg.resourceId); + + assert.deepEqual(relationships.filter(x => x.resourceType === AWS_EC2_INSTANCE), []); + assert.deepEqual(actualAsgRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: asg.resourceId, + resourceType: AWS_AUTOSCALING_AUTOSCALING_GROUP + }); + }); + + it('should add relationships for ec2 instances not in autoscaling groups', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/alb/targetGroup/asgAndInstances.json', {with: {type: 'json' }}); + const {ec2Instance1, ec2Instance2, asg, targetGroup} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createElbV2Client() { + return { + describeTargetHealth: async arn => [ + {Target: {Id: ec2Instance1.resourceId}}, + {Target: {Id: ec2Instance2.resourceId}} + ] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [ + targetGroup, asg + ]); + + const {relationships} = rels.find(r => r.resourceId === targetGroup.resourceId); + + const actualAsgRel = relationships.find(r => r.resourceId === asg.resourceId); + const actualEc2Rel = relationships.find(r => r.resourceId === ec2Instance2.resourceId); + + assert.strictEqual(relationships.filter(x => x.resourceType === AWS_EC2_INSTANCE).length, 1); + assert.deepEqual(actualAsgRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: asg.resourceId, + resourceType: AWS_AUTOSCALING_AUTOSCALING_GROUP + }); + assert.deepEqual(actualEc2Rel, { + relationshipName: IS_ASSOCIATED_WITH + INSTANCE, + resourceId: actualEc2Rel.resourceId, + resourceType: AWS_EC2_INSTANCE + }); + }); + + it('should add relationships with lambda functions', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/alb/targetGroup/lambda.json', {with: {type: 'json' }}); + const {lambda, targetGroup} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createElbV2Client() { + return { + describeTargetHealth: async arn => [ + {Target: {Id: lambda.arn}} + ] + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [ + targetGroup, lambda + ]); + + const {relationships} = rels.find(r => r.resourceId === targetGroup.resourceId); + + const actualLambdaRel = relationships.find(r => r.arn === lambda.arn); + + assert.deepEqual(actualLambdaRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: lambda.arn + }); + }); + + it('should add relationship with VPC', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/alb/targetGroup/vpc.json', {with: {type: 'json' }}); + const {vpc, targetGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + targetGroup + ]); + + const {relationships} = rels.find(r => r.resourceId === targetGroup.resourceId); + + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + }); + + }); + + describe(AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, () => { + + it('should add relationship between listeners and single target group', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/alb/listeners/singleTargetGroup.json', {with: {type: 'json' }}); + const {alb, targetGroup, listener} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [listener]); + + const {relationships} = rels.find(r => r.resourceType === AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER); + + const actualTgRel = relationships.find(r => r.resourceType === AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP); + const actualAlbRel = relationships.find(r => r.resourceType === AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER); + + assert.deepEqual(actualTgRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: targetGroup.resourceId, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP + }); + assert.deepEqual(actualAlbRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: alb.resourceId, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_LOADBALANCER + }); + }); + + it('should add relationship between listeners and multiple target groups', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/alb/listeners/multipleTargetGroups.json', {with: {type: 'json' }}); + const {targetGroup1, targetGroup2, listener} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [listener]); + + const {relationships} = rels.find(r => r.resourceType === AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER); + + const actualTgRel1 = relationships.find(r => r.resourceId === targetGroup1.resourceId); + const actualTgRel2 = relationships.find(r => r.resourceId === targetGroup2.resourceId); + + assert.deepEqual(actualTgRel1, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: targetGroup1.resourceId, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP + }); + assert.deepEqual(actualTgRel2, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: targetGroup2.resourceId, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP + }); + }); + + it('should add relationship between listeners and cognito user pools', async () => { + const {default: schema} = await import('./fixtures/relationships/loadBalancer/alb/listeners/cognito.json', {with: {type: 'json' }}); + const {userPool, listener} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [listener]); + + const {relationships} = rels.find(r => r.resourceType === AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER); + + const actualTgRel = relationships.find(r => r.resourceType === AWS_COGNITO_USER_POOL); + + assert.deepEqual(actualTgRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: userPool.resourceId, + resourceType: AWS_COGNITO_USER_POOL + }); + }); + + }); + + describe(AWS_IAM_ROLE, () => { + + it('should add relationships for managed policies', async () => { + const {default: schema} = await import('./fixtures/relationships/iam/role/managedPolices.json', {with: {type: 'json' }}); + const {role, managedRole} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [role, managedRole]); + + const {relationships} = rels.find(r => r.resourceId === role.resourceId); + const actualManagedRoleRel = relationships.find(r => r.arn === managedRole.arn); + + assert.deepEqual(actualManagedRoleRel, { + relationshipName: IS_ATTACHED_TO, + arn: managedRole.arn, + resourceType: AWS_IAM_AWS_MANAGED_POLICY + }); + }); + + }); + + describe(AWS_IAM_INLINE_POLICY, () => { + + it('should parse multiple statements', async () => { + const {default: schema} = await import('./fixtures/relationships/iam/inlinePolicy/multipleStatement.json', {with: {type: 'json' }}); + const {policy, s3Bucket1, s3Bucket2} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [policy, s3Bucket1, s3Bucket2]); + const {relationships} = rels.find(r => r.resourceId === policy.resourceId); + + const actualBucket1 = relationships.find(r => r.arn === s3Bucket1.arn); + const actualBucket2 = relationships.find(r => r.arn === s3Bucket2.arn); + + assert.deepEqual(actualBucket1, { + relationshipName: IS_ATTACHED_TO, + arn: s3Bucket1.arn, + resourceType: AWS_S3_BUCKET + }); + assert.deepEqual(actualBucket2, { + relationshipName: IS_ATTACHED_TO, + arn: s3Bucket2.arn, + resourceType: AWS_S3_BUCKET + }); + }); + + }); + + describe(AWS_IAM_INSTANCE_PROFILE, () => { + + it('should add relationships for associated IAM roles', async () => { + const {default: schema} = await import('./fixtures/relationships/iam/instanceProfile/mutipleRoles.json', {with: {type: 'json' }}); + const {profile} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [profile]); + + const {relationships} = rels.find(r => r.arn === profile.arn); + + assert.deepEqual(relationships, [{ + relationshipName: IS_ASSOCIATED_WITH + ROLE, + resourceName: 'roleName1', + resourceType: AWS_IAM_ROLE + }, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + resourceName: 'roleName2', + resourceType: AWS_IAM_ROLE + }]); + }); + + }); + + describe(AWS_IAM_USER, () => { + + it('should add relationships for managed policies', async () => { + const {default: schema} = await import('./fixtures/relationships/iam/user/managedPolicy.json', {with: {type: 'json' }}); + const {user, managedRole} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [user, managedRole]); + + const {relationships} = rels.find(r => r.resourceId === user.resourceId); + const actualManagedRoleRel = relationships.find(r => r.arn === managedRole.arn); + + assert.deepEqual(actualManagedRoleRel, { + relationshipName: IS_ATTACHED_TO, + arn: managedRole.arn, + resourceType: AWS_IAM_AWS_MANAGED_POLICY + }); + }); + + }); + + describe(AWS_MEDIA_PACKAGE_PACKAGING_CONFIGURATION, () => { + + it('should add relationship to packaging groups', async () => { + const {default: schema} = await import('./fixtures/relationships/mediapackage/packagingConfiguration/group.json', {with: {type: 'json' }}); + const {packagingConfiguration, packagingGroup} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [packagingConfiguration]); + + const {relationships} = rels.find(r => r.arn === packagingConfiguration.arn); + const actualPackagingGroupRel = relationships.find(r => r.resourceId === packagingGroup.resourceId); + + assert.deepEqual(actualPackagingGroupRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: packagingGroup.resourceId, + resourceType: AWS_MEDIA_PACKAGE_PACKAGING_GROUP + }); + }); + + it('should add encryption role relationships', async () => { + const {default: schema} = await import('./fixtures/relationships/mediapackage/packagingConfiguration/encryption.json', {with: {type: 'json' }}); + const { + cmafRole, dashRole, hlsRole, mssRole, packagingConfigurationCmaf, packagingConfigurationDash, packagingConfigurationHls, packagingConfigurationMss + } = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [ + packagingConfigurationCmaf, packagingConfigurationDash, packagingConfigurationHls, packagingConfigurationMss + ]); + + const {relationships: cmafRelationships} = rels.find(r => r.arn === packagingConfigurationCmaf.arn); + const {relationships: dashRelationships} = rels.find(r => r.arn === packagingConfigurationDash.arn); + const {relationships: hlsRelationships} = rels.find(r => r.arn === packagingConfigurationHls.arn); + const {relationships: mssRelationships} = rels.find(r => r.arn === packagingConfigurationMss.arn); + + const actualCmafRoleRel = cmafRelationships.find(r => r.arn === cmafRole.arn); + const actualDashRoleRel = dashRelationships.find(r => r.arn === dashRole.arn); + const actualHlsRoleRel = hlsRelationships.find(r => r.arn === hlsRole.arn); + const actualMssRoleRel = mssRelationships.find(r => r.arn === mssRole.arn); + + assert.deepEqual(actualCmafRoleRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: cmafRole.arn + }); + + assert.deepEqual(actualDashRoleRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: dashRole.arn + }); + + assert.deepEqual(actualHlsRoleRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: hlsRole.arn + }); + + assert.deepEqual(actualMssRoleRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: mssRole.arn + }); + }); + + }); + + describe(AWS_MEDIA_PACKAGE_PACKAGING_GROUP, () => { + + it('should add authorization relationships', async () => { + const {default: schema} = await import('./fixtures/relationships/mediapackage/packagingGroup/authorization.json', {with: {type: 'json' }}); + const {packagingGroup, role, secret} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [packagingGroup]); + + const {relationships} = rels.find(r => r.arn === packagingGroup.arn); + const actualRoleRel = relationships.find(r => r.arn === role.arn); + const actualSecretRel = relationships.find(r => r.arn === secret.arn); + + assert.deepEqual(actualRoleRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: role.arn + }); + + assert.deepEqual(actualSecretRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: secret.arn + }); + }); + }); + + describe(AWS_MEDIA_CONNECT_FLOW_ENTITLEMENT, () => { + + it('should add relationship with flow', async () => { + const {default: schema} = await import('./fixtures/relationships/mediaconnect/entitlement/flow.json', {with: {type: 'json' }}); + const {entitlement, flow} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [entitlement]); + + const {relationships} = rels.find(r => r.resourceId === entitlement.resourceId); + const actualFlowRel = relationships.find(r => r.arn === flow.arn); + + assert.deepEqual(actualFlowRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: flow.arn + }); + }); + + }); + + describe(AWS_MEDIA_CONNECT_FLOW_SOURCE, () => { + + it('should add interface and flow relationships for VPC sources', async () => { + const {default: schema} = await import('./fixtures/relationships/mediaconnect/flowsource/vpc.json', {with: {type: 'json' }}); + const {flow, source, vpcInterface} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [source]); + + const {relationships} = rels.find(r => r.resourceId === source.resourceId); + const actualFlowRel = relationships.find(r => r.arn === flow.arn); + const actualVpcInterfaceRel = relationships.find(r => r.resourceName === vpcInterface.resourceName); + + assert.deepEqual(actualFlowRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: flow.arn + }); + + assert.deepEqual(actualVpcInterfaceRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceName: vpcInterface.resourceName, + resourceType: AWS_MEDIA_CONNECT_FLOW_VPC_INTERFACE + }); + }); + + it('should add relationship with flow entitlement', async () => { + const {default: schema} = await import('./fixtures/relationships/mediaconnect/flowsource/entitlement.json', {with: {type: 'json' }}); + const {entitlement, source} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [source]); + + const {relationships} = rels.find(r => r.resourceId === source.resourceId); + const actualEntitlementRel = relationships.find(r => r.arn === entitlement.arn); + + assert.deepEqual(actualEntitlementRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: entitlement.arn + }); + }); + + it('should add relationships for encrypted sources', async () => { + const {default: schema} = await import('./fixtures/relationships/mediaconnect/flowsource/encrypted.json', {with: {type: 'json' }}); + const {role, secret, source} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [source]); + + const {relationships} = rels.find(r => r.resourceId === source.resourceId); + const actualRoleRel = relationships.find(r => r.arn === role.arn); + const actualSecretRel = relationships.find(r => r.arn === secret.arn); + + assert.deepEqual(actualRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: role.arn + }); + + assert.deepEqual(actualSecretRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: secret.arn + }); + }); + + }); + + describe(AWS_MEDIA_CONNECT_FLOW_VPC_INTERFACE, () => { + + it('should add networking relationships', async () => { + const {default: schema} = await import('./fixtures/relationships/mediaconnect/flowVpcInterface/networking.json', {with: {type: 'json' }}); + const {eni, securityGroup, subnet1, vpc, vpcInterface} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet1, vpcInterface]); + + const {relationships} = rels.find(r => r.resourceId === vpcInterface.resourceId); + const actualVpcRel = relationships.find(r => r.resourceId === vpc.resourceId); + const actualSubnetRel = relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSecurityGroupRel = relationships.find(r => r.resourceId === securityGroup.resourceId); + const actualEniRel = relationships.find(r => r.resourceId === eni.resourceId); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + + assert.deepEqual(actualSubnetRel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + + assert.deepEqual(actualSecurityGroupRel, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + + assert.deepEqual(actualEniRel, { + relationshipName: IS_ATTACHED_TO + NETWORK_INTERFACE, + resourceId: eni.resourceId, + resourceType: AWS_EC2_NETWORK_INTERFACE + }); + }); + + }); + + describe(AWS_RDS_DB_INSTANCE, () => { + + it('should add VPC relationships for RDS DB instances', async () => { + const {default: schema} = await import('./fixtures/relationships/rds/instance/vpc.json', {with: {type: 'json' }}); + const {dbInstance, $constants} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [dbInstance]); + + const actual = rels[0]; + const actualSubnet1Rel = actual.relationships.find(r => r.resourceId === $constants.subnet1); + const actualVpcRel = actual.relationships.find(r => r.resourceId === $constants.vpcId); + + assert.strictEqual(dbInstance.vpcId, $constants.vpcId); + + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: $constants.subnet1, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: $constants.vpcId, + resourceType: AWS_EC2_VPC + }); + }); + + }); + + describe(AWS_EC2_INSTANCE, () => { + + it('should add relationship for associated instance profile', async () => { + const {default: schema} = await import('./fixtures/relationships/ec2/instance/configuration.json', {with: {type: 'json' }}); + const {instance} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [instance]); + + const {relationships} = rels.find(r => r.arn === instance.arn); + const actualInstanceProfileRel = relationships.find(r => r.arn === instance.configuration.iamInstanceProfile.arn); + + assert.deepEqual(actualInstanceProfileRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: instance.configuration.iamInstanceProfile.arn, + }); + + }); + }); + + describe(AWS_EC2_SPOT_FLEET, () => { + + it('should not add relationships when no load balancers config present', async () => { + const {default: schema} = await import('./fixtures/relationships/ec2/spotfleet/noLb.json', {with: {type: 'json' }}); + const {spotFleet} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [spotFleet]); + + const {relationships} = rels.find(r => r.resourceType === AWS_EC2_SPOT_FLEET); + + assert.deepEqual(relationships, []); + }); + + it('should add relationship between ELBs and spot fleets', async () => { + const {default: schema} = await import('./fixtures/relationships/ec2/spotfleet/elb.json', {with: {type: 'json' }}); + const {elb, spotFleet} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [spotFleet]); + + const {relationships} = rels.find(r => r.resourceType === AWS_EC2_SPOT_FLEET); + + const actualTgRel = relationships.find(r => r.resourceType === AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER); + + assert.deepEqual(actualTgRel, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: elb.resourceId, + resourceType: AWS_ELASTIC_LOAD_BALANCING_LOADBALANCER + }); + }); + + it('should add relationship between ALBs and spot fleets', async () => { + const {default: schema} = await import('./fixtures/relationships/ec2/spotfleet/alb.json', {with: {type: 'json' }}); + const {targetGroup, spotFleet} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [targetGroup, spotFleet]); + + const {relationships} = rels.find(r => r.resourceType === AWS_EC2_SPOT_FLEET); + + const actualTgRel = relationships.find(r => r.arn === targetGroup.arn); + + assert.deepEqual(actualTgRel, { + relationshipName: IS_ASSOCIATED_WITH, + arn: targetGroup.arn + }); + }); + + }); + + describe(AWS_S3_BUCKET, () => { + + it('should get relationships in supplemtary configuration', async () => { + const {default: schema} = await import('./fixtures/relationships/s3/bucket/supplementary.json', {with: {type: 'json' }}); + const {s3Bucket} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [s3Bucket]); + + const {relationships} = rels.find(r => r.resourceType === AWS_S3_BUCKET); + + const actualLoggingBucketRel = relationships.find(r => r.resourceId === s3Bucket.supplementaryConfiguration.BucketLoggingConfiguration.destinationBucketName) + const actualLambdaNotificationRel = relationships.find(r => r.arn === s3Bucket.supplementaryConfiguration.BucketNotificationConfiguration.configurations.LambdaFunctionConfigurationId.functionARN) + const actualSnsNotificationRel = relationships.find(r => r.arn === s3Bucket.supplementaryConfiguration.BucketNotificationConfiguration.configurations.SnsConfigurationId.topicARN) + const actualLSqsNotificationRel = relationships.find(r => r.arn === s3Bucket.supplementaryConfiguration.BucketNotificationConfiguration.configurations.SqsFunctionConfigurationId.queueARN) + + assert.deepEqual(actualLoggingBucketRel, { + resourceId: s3Bucket.supplementaryConfiguration.BucketLoggingConfiguration.destinationBucketName, + resourceType: AWS_S3_BUCKET, + relationshipName: IS_ASSOCIATED_WITH + }); + + assert.deepEqual(actualLambdaNotificationRel, { + arn: s3Bucket.supplementaryConfiguration.BucketNotificationConfiguration.configurations.LambdaFunctionConfigurationId.functionARN, + relationshipName: IS_ASSOCIATED_WITH + }); + + assert.deepEqual(actualSnsNotificationRel, { + arn: s3Bucket.supplementaryConfiguration.BucketNotificationConfiguration.configurations.SnsConfigurationId.topicARN, + relationshipName: IS_ASSOCIATED_WITH + }); + + assert.deepEqual(actualLSqsNotificationRel, { + arn: s3Bucket.supplementaryConfiguration.BucketNotificationConfiguration.configurations.SqsFunctionConfigurationId.queueARN, + relationshipName: IS_ASSOCIATED_WITH + }); + }); + + }); + + describe(AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, () => { + + it('should handle applications with no application tag', async () => { + const {default: schema} = await import('./fixtures/relationships/appregistry/application/noApplicationTag.json', {with: {type: 'json' }}); + const {application} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [application]); + + const {relationships} = rels.find(r => r.arn === application.arn); + + assert.lengthOf(relationships, 0); + }); + + it('should handle when application tag is present but tag resource type is missing', async () => { + const {default: schema} = await import('./fixtures/relationships/appregistry/application/default.json', {with: {type: 'json' }}); + const {application} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [application]); + + const {relationships} = rels.find(r => r.arn === application.arn); + + assert.lengthOf(relationships, 0); + }); + + it('should associate resources with application tag to application', async () => { + const {default: schema} = await import('./fixtures/relationships/appregistry/application/default.json', {with: {type: 'json' }}); + const {application, tag} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [application, tag]); + + const {relationships} = rels.find(r => r.arn === application.arn); + + const actualLambdaRel = relationships.find(r => r.resourceType === AWS_LAMBDA_FUNCTION); + const sctualEc2Rel = relationships.find(r => r.resourceType === AWS_EC2_INSTANCE); + const actualRoleRel = relationships.find(r => r.resourceType === AWS_IAM_ROLE); + + assert.lengthOf(relationships, 3); + + assert.deepEqual(actualLambdaRel, { + relationshipName: CONTAINS, + resourceType: AWS_LAMBDA_FUNCTION, + resourceId: 'lambdaResourceId' + }); + + assert.deepEqual(sctualEc2Rel, { + relationshipName: `${CONTAINS}Instance`, + resourceType: AWS_EC2_INSTANCE, + resourceId: 'ec2InstanceResourceId' + }); + + assert.deepEqual(actualRoleRel, { + relationshipName: `${CONTAINS}Role`, + resourceType: AWS_IAM_ROLE, + resourceName: 'roleName' + }); + }); + + }); + + describe(AWS_SNS_TOPIC, () => { + + it('should ignore relationships to undiscovered resources', async () => { + const {default: schema} = await import('./fixtures/relationships/sns/lambda/undiscovered.json', {with: {type: 'json' }}); + const {snsTopic} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createSnsClient(_, region) { + return { + async getAllSubscriptions() { + return region === snsTopic.awsRegion ? [{ + TopicArn: snsTopic.arn, Endpoint: 'undiscoveredArn' + }] : [] + } + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [snsTopic]); + + const {relationships} = rels.find(r => r.resourceType === AWS_SNS_TOPIC); + + assert.deepEqual(relationships, []); + }); + + it('should add additional relationships to Lambda functions', async () => { + const {default: schema} = await import('./fixtures/relationships/sns/lambda/sameRegion.json', {with: {type: 'json' }}); + const {snsTopic, lambda} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createSnsClient(_, region) { + return { + async getAllSubscriptions() { + return region === snsTopic.awsRegion ? [{ + TopicArn: snsTopic.arn, Endpoint: lambda.arn + }] : [] + } + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [snsTopic, lambda]); + + const {relationships} = rels.find(r => r.resourceType === AWS_SNS_TOPIC); + const actualLambdaRel = relationships.find(r => r.arn === lambda.arn); + + assert.deepEqual(actualLambdaRel, { + arn: lambda.arn, + resourceType: AWS_LAMBDA_FUNCTION, + relationshipName: IS_ASSOCIATED_WITH + }); + }); + + it('should add additional relationships to SQS queues', async () => { + const {default: schema} = await import('./fixtures/relationships/sns/sqs/differentRegion.json', {with: {type: 'json' }}); + const {snsTopic, sqs} = generate(schema); + + const mockAwsClient = { + ...defaultMockAwsClient, + createSnsClient(_, region) { + return { + async getAllSubscriptions() { + return region === snsTopic.awsRegion ? [{ + TopicArn: snsTopic.arn, Endpoint: sqs.arn + }] : [] + } + } + } + }; + + const rels = await addAdditionalRelationships(mockAwsClient, [snsTopic, sqs]); + + const {relationships} = rels.find(r => r.resourceType === AWS_SNS_TOPIC); + const actualSqsRel = relationships.find(r => r.arn === sqs.arn); + + assert.deepEqual(actualSqsRel, { + arn: sqs.arn, + resourceType: AWS_SQS_QUEUE, + relationshipName: IS_ASSOCIATED_WITH + }); + }); + + }); + + describe(AWS_CODEBUILD_PROJECT, () => { + + it('should add relationship to service role', async () => { + const {default: schema} = await import('./fixtures/relationships/codebuild/project/role.json', {with: {type: 'json' }}); + const {serviceRole, project} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [serviceRole, project]); + + const {relationships} = rels.find(r => r.resourceId === project.resourceId); + const actualRoleRel = relationships.find(r => r.arn === serviceRole.arn); + + assert.deepEqual(actualRoleRel, { + relationshipName: IS_ASSOCIATED_WITH + ROLE, + arn: serviceRole.arn + }) + }); + + it('should add VPC relationships for CodeBuild projects', async () => { + const {default: schema} = await import('./fixtures/relationships/codebuild/project/vpc.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, securityGroup, project} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [subnet1, subnet2, project]); + + const actual = rels.find(r => r.resourceId === project.resourceId); + const actualVpcRel = actual.relationships.find(r => r.resourceId === vpc.resourceId); + const actualSubnet1Rel = actual.relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSubnet2Rel = actual.relationships.find(r => r.resourceId === subnet2.resourceId); + const actualSecurityGroupRel = actual.relationships.find(r => r.resourceId === securityGroup.resourceId); + + assert.strictEqual(actual.availabilityZone, `${subnet1.availabilityZone},${subnet2.availabilityZone}`); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSecurityGroupRel, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }) + }); + + }); + + describe(AWS_OPENSEARCH_DOMAIN, () => { + + it('should add VPC relationships for OpenSearch domains', async () => { + const {default: schema} = await import('./fixtures/relationships/opensearch/domain/vpc.json', {with: {type: 'json' }}); + const {vpc, subnet1, subnet2, securityGroup, domain} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [domain, subnet1, subnet2]); + + const actual = rels.find(r => r.resourceId === domain.resourceId); + const actualVpcRel = actual.relationships.find(r => r.resourceId === vpc.resourceId); + const actualSubnet1Rel = actual.relationships.find(r => r.resourceId === subnet1.resourceId); + const actualSubnet2Rel = actual.relationships.find(r => r.resourceId === subnet2.resourceId); + const actualSg = actual.relationships.find(r => r.resourceId === securityGroup.resourceId); + + assert.strictEqual(actual.availabilityZone, 'eu-west-2a,eu-west-2b'); + + assert.deepEqual(actualVpcRel, { + relationshipName: IS_CONTAINED_IN + VPC, + resourceId: vpc.resourceId, + resourceType: AWS_EC2_VPC + }); + assert.deepEqual(actualSubnet1Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet1.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSubnet2Rel, { + relationshipName: IS_CONTAINED_IN + SUBNET, + resourceId: subnet2.resourceId, + resourceType: AWS_EC2_SUBNET + }); + assert.deepEqual(actualSg, { + relationshipName: IS_ASSOCIATED_WITH + SECURITY_GROUP, + resourceId: securityGroup.resourceId, + resourceType: AWS_EC2_SECURITY_GROUP + }); + }); + + }); + + describe(AWS_APPSYNC_DATASOURCE, async ()=> { + it('should add dynamodb relationships', async ()=> { + + const {default: schema} = await import('./fixtures/relationships/appsync/graphQlApi.json', {with: {type: 'json' }}); + const {dynamoDBTable, dynamoLinkedDataSource} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [dynamoLinkedDataSource, dynamoDBTable]) + const actual = rels.find(r => r.dataSourceArn === dynamoLinkedDataSource.dataSourceArn); + + const dynamoRelationship = actual.relationships.find(r => r.resourceName === dynamoDBTable.resourceName) + + assert.deepEqual(dynamoRelationship, { + relationshipName: IS_ASSOCIATED_WITH, + resourceName: dynamoDBTable.resourceName, + resourceType: AWS_DYNAMODB_TABLE + }); + + }) + it('should add lambda relationships', async ()=> { + const {default: schema} = await import('./fixtures/relationships/appsync/graphQlApi.json', {with: {type: 'json' }}); + const { lambdaLinkedDataSource, lambda} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [lambdaLinkedDataSource, lambda]) + const actual = rels.find(r => r.dataSourceArn === lambdaLinkedDataSource.dataSourceArn); + + const lambdaRelationship = actual.relationships.find(r => r.arn === lambda.arn) + + assert.deepEqual(lambdaRelationship, { + relationshipName: IS_ASSOCIATED_WITH, + arn: lambda.arn + }); + }) + + it('should add eventBridge relationships', async ()=> { + const {default: schema} = await import('./fixtures/relationships/appsync/graphQlApi.json', {with: {type: 'json' }}); + const { eventBridgeLinkedDataSource, eventBus} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [eventBridgeLinkedDataSource, eventBus]) + const actual = rels.find(r => r.dataSourceArn === eventBridgeLinkedDataSource.dataSourceArn); + + const eventBusRelationship = actual.relationships.find(r => r.arn === eventBus.arn) + + assert.deepEqual(eventBusRelationship, { + relationshipName: IS_ASSOCIATED_WITH, + arn: eventBus.arn + }); + }) + + it('should add relational database relationships', async ()=> { + const {default: schema} = await import('./fixtures/relationships/appsync/graphQlApi.json', {with: {type: 'json' }}); + const { rdsLinkedDataSource, rds} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [rdsLinkedDataSource, rds]) + const actual = rels.find(r => r.dataSourceArn === rdsLinkedDataSource.dataSourceArn); + + const rdsRelationship = actual.relationships.find(r => r.arn === rds.arn) + + assert.deepEqual(rdsRelationship, { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: rds.resourceId, + resourceType: AWS_RDS_DB_CLUSTER + }); + }) + it('should add Opensearch relationships', async ()=> { + const schema = require('./fixtures/relationships/appsync/graphQlApi.json'); + const { openSearchLinkedDataSource, opensearchEndpoint} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [openSearchLinkedDataSource, opensearchEndpoint]) + const actual = rels.find(r => r.dataSourceArn === openSearchLinkedDataSource.dataSourceArn); + + const endpointRelationship = actual.relationships.find(r => r.arn === opensearchEndpoint.arn) + + assert.deepEqual(endpointRelationship, { + relationshipName: IS_ASSOCIATED_WITH, + arn: opensearchEndpoint.arn, + }); + }) + it('should add elasticSearch relationships', async ()=> { + const schema = require('./fixtures/relationships/appsync/graphQlApi.json'); + const { elasticSearchLinkedDataSource, elasticsearchEndpoint} = generate(schema); + + const rels = await addAdditionalRelationships(defaultMockAwsClient, [elasticSearchLinkedDataSource, elasticsearchEndpoint]) + const actual = rels.find(r => r.dataSourceArn === elasticSearchLinkedDataSource.dataSourceArn); + + const endpointRelationship = actual.relationships.find(r => r.arn === elasticsearchEndpoint.arn) + + assert.deepEqual(endpointRelationship, { + relationshipName: IS_ASSOCIATED_WITH, + arn: elasticsearchEndpoint.arn, + }); + }) + }) + + }); + +}); \ No newline at end of file diff --git a/source/backend/discovery/test/apiClient/index.test.mjs b/source/backend/discovery/test/apiClient/index.test.mjs new file mode 100644 index 00000000..d4b7b8fb --- /dev/null +++ b/source/backend/discovery/test/apiClient/index.test.mjs @@ -0,0 +1,617 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, afterAll, beforeAll, describe, it} from 'vitest'; +import sinon from 'sinon'; +import createAppSync from '../../src/lib/apiClient/appSync.mjs'; +import {createApiClient} from '../../src/lib/apiClient/index.mjs'; +import {setGlobalDispatcher, getGlobalDispatcher} from 'undici'; +import {createSuccessThenError} from '../mocks/agents/utils.mjs'; +import ConnectionClosedAgent from '../mocks/agents/ConnectionClosed.mjs'; +import GetAccountsSelfManaged from '../mocks/agents/GetAccountsSelfManaged.mjs'; +import GetAccountsOrgsEmpty from '../mocks/agents/GetAccountsOrgsEmpty.mjs'; +import GetAccountsOrgsLastCrawled from '../mocks/agents/GetAccountsOrgsLastCrawled.mjs'; +import GetAccountsOrgsDeleted from '../mocks/agents/GetAccountsOrgsDeleted.mjs'; +import GetDbResourcesMapPagination from '../mocks/agents/GetDbResourcesMapPagination.mjs'; +import GetDbRelationshipsMapPagination from '../mocks/agents/GetDbRelationshipsMapPagination.mjs'; +import GenericError from '../mocks/agents/GenericError.mjs'; +import IndexResourcesPartialSuccess from '../mocks/agents/IndexResourcesPartialSuccess.mjs'; +import DeleteIndexedResourcesPartialSuccess from '../mocks/agents/DeleteIndexedResourcesPartialSuccess.mjs'; +import UpdateIndexedResourcesPartialSuccess from '../mocks/agents/UpdateIndexedResourcesPartialSuccess.mjs'; +import { + CONTAINS, + AWS_LAMBDA_FUNCTION, + FUNCTION_RESPONSE_SIZE_TOO_LARGE, ACCESS_DENIED +} from '../../src/lib/constants.mjs'; +import {generateBaseResource} from '../generator.mjs'; +import {UnprocessedOpenSearchResourcesError} from '../../src/lib/errors.mjs'; + +const ACCOUNT_X = 'xxxxxxxxxxxx'; +const ACCOUNT_Y = 'yyyyyyyyyyyy'; +const ACCOUNT_Z = 'zzzzzzzzzzzz'; +const EU_WEST_1= 'eu-west-1'; +const US_EAST_1= 'us-east-1'; + +describe('index.mjs', () => { + + let globalDispatcher = null; + + beforeAll(() => { + globalDispatcher = getGlobalDispatcher(); + }); + + const defaultMockAwsClient = { + createEc2Client() { + return { + async getAllRegions() { + return [] + } + }; + }, + createConfigServiceClient() { + return {} + }, + createOrganizationsClient() { + return { + async getAllAccounts() { + return [] + }, + async getRootAccount() { + return { + Arn: `arn:aws:organizations::${ACCOUNT_X}:account/o-exampleorgid/:${ACCOUNT_X}` + } + } + } + }, + createStsClient() { + return { + getCredentials: async role => {} + } + } + }; + + const defaultConfig = { + isUsingOrganizations: false, + rootAccountId: ACCOUNT_X + }; + + const appSync = createAppSync({graphgQlUrl: 'https://www.workload-discovery/graphql'}); + const apiClient = createApiClient(defaultMockAwsClient, appSync, defaultConfig); + + describe('error', () => { + + it('should recover from premature connection closed error', async () => { + + setGlobalDispatcher(ConnectionClosedAgent) + const actual = await apiClient.storeRelationships({concurrency:10, batchSize:10}, [{ + source: 'sourceArn', + target: 'targetArn', + label: CONTAINS + }]); + assert.deepEqual(actual, { + errors: [], + results: [ + [] + ] + }); + }); + + }); + + describe('getDbResourcesMap', () => { + + it('should page through server results', async () => { + + setGlobalDispatcher(GetDbResourcesMapPagination) + const actual = await apiClient.getDbResourcesMap(); + assert.deepEqual(actual, new Map([['arn1', { + id: 'arn1', + label: 'label', + md5Hash: '', + properties: { + id: 'arn1', + resourceId: 'resourceId1', + resourceName: 'resourceName1', + resourceType: 'AWS::Lambda::Function', + accountId: 'xxxxxxxxxxxx', + arn: 'arn1', + awsRegion: 'eu-west-1', + relationships: [], + tags: [], + configuration: { + a: 1 + } + } + }]])); + }); + + it('should handle resource to large errors', async () => { + const resources = [1,2].map(i => { + const properties = generateBaseResource('xxxxxxxxxxxx', 'eu-west-1', AWS_LAMBDA_FUNCTION, i); + return {id: properties.id, label: 'label', md5Hash: '', properties}; + }) + + const appSync = createAppSync({graphgQlUrl: 'https://www.workload-discovery/graphql'}); + const mockGetResources = sinon.stub(); + + mockGetResources + .withArgs({pagination: {start: 0, end: 1000}}) + .rejects(new Error(FUNCTION_RESPONSE_SIZE_TOO_LARGE)) + .withArgs({pagination: {start: 0, end: 500}}) + .rejects(new Error(FUNCTION_RESPONSE_SIZE_TOO_LARGE)) + .withArgs({pagination: {start: 0, end: 250}}) + .resolves([resources[0]]) + .withArgs({pagination: {start: 250, end: 1250}}) + .rejects(new Error(FUNCTION_RESPONSE_SIZE_TOO_LARGE)) + .withArgs({pagination: {start: 250, end: 750}}) + .resolves([resources[1]]) + .withArgs({pagination: {start: 750, end: 1750}}) + .resolves([]); + + const apiClient = createApiClient(defaultMockAwsClient, {...appSync, getResources: mockGetResources}, defaultConfig); + + const actual = await apiClient.getDbResourcesMap(); + + assert.deepEqual(actual, new Map(resources.map(resource => [resource.id, resource]))); + }); + + }); + + describe('getDbRelationshipsMap', () => { + + it('should page through server results', async () => { + + setGlobalDispatcher(GetDbRelationshipsMapPagination) + const actual = await apiClient.getDbRelationshipsMap(); + assert.deepEqual(actual, new Map([['sourceArn_Contains _targetArn', { + id: 'testId', + source: 'sourceArn', + target: 'targetArn', + label: CONTAINS + }]])); + }); + + }); + + describe('storeResources', () => { + + it('should handle total failure writing resources to OpenSearch', async () => { + setGlobalDispatcher(GenericError); + const actual = await apiClient.storeResources({concurrency:10, batchSize:10}, [{}]); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + it('should handle partial success writing resources to OpenSearch', async () => { + setGlobalDispatcher(IndexResourcesPartialSuccess); + const actual = await apiClient.storeResources({concurrency:10, batchSize:10}, [{ + id: 'arn1' + }, { + id: 'arn2' + }, { + id: 'arn3' + }]); + + assert.lengthOf(actual.errors, 1); + assert.instanceOf(actual.errors[0].raw, UnprocessedOpenSearchResourcesError); + assert.deepEqual(actual.errors[0].item, [{id: 'arn1'}]); + }); + + it('should handle errors writing resources to Neptune', async () => { + setGlobalDispatcher(createSuccessThenError({ + data: { + indexResources: { + unprocessedResources: [] + } + } + }, "Validation error")); + const actual = await apiClient.storeResources({concurrency:10, batchSize:10}, [{}]); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + }); + + describe('deleteResources', () => { + + it('should handle total failure deleting resources from OpenSearch', async () => { + setGlobalDispatcher(GenericError); + const actual = await apiClient.deleteResources({concurrency:10, batchSize:10}, [{}]); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + it('should handle partial success deleting resources from OpenSearch', async () => { + setGlobalDispatcher(DeleteIndexedResourcesPartialSuccess); + const actual = await apiClient.deleteResources({concurrency:10, batchSize:10}, [ + 'arn1', 'arn2', 'arn3' + ]); + + assert.lengthOf(actual.errors, 1); + assert.instanceOf(actual.errors[0].raw, UnprocessedOpenSearchResourcesError); + assert.deepEqual(actual.errors[0].item, ['arn1']); + }); + + it('should handle errors deleting resources from Neptune', async () => { + setGlobalDispatcher(createSuccessThenError({ + data: { + deleteIndexedResources: { + unprocessedResources: [] + } + } + }, "Validation error")); + const actual = await apiClient.deleteResources({concurrency:10, batchSize:10}, [{}]); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + }); + + describe('updateResources', () => { + + it('should handle total failure updating resources in OpenSearch', async () => { + setGlobalDispatcher(GenericError); + const actual = await apiClient.updateResources({concurrency:10, batchSize:10}, [{}]); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + it('should handle partial success deleting resources from OpenSearch', async () => { + setGlobalDispatcher(UpdateIndexedResourcesPartialSuccess); + const actual = await apiClient.updateResources({concurrency:10, batchSize:10}, [{ + id: 'arn1' + }, { + id: 'arn2' + }, { + id: 'arn3' + }]); + + assert.lengthOf(actual.errors, 1); + assert.instanceOf(actual.errors[0].raw, UnprocessedOpenSearchResourcesError); + assert.deepEqual(actual.errors[0].item, [{id: 'arn1'}]); + }); + + it('should handle errors updating resources in Neptune', async () => { + setGlobalDispatcher(createSuccessThenError({ + data: { + updateIndexedResources: { + unprocessedResources: [] + } + } + }, "Validation error")); + const actual = await apiClient.updateResources({concurrency: 10, batchSize: 10}, [{}]); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + }); + + describe('deleteRelationships', () => { + + it('should handle errors', async () => { + setGlobalDispatcher(GenericError); + const actual = await apiClient.deleteRelationships({concurrency:10, batchSize:10}, [{}]); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + }); + + describe('updateAccountsCrawledTime', () => { + + it('should handle errors', async () => { + setGlobalDispatcher(GenericError); + const actual = await apiClient.updateCrawledAccounts(['xxxxxxxxxxxx']); + assert.strictEqual(actual.errors[0].message, "[{\"message\":\"Validation error\"}]"); + }); + + }); + + describe('getAccounts', () => { + + it('should mark accounts that do not have the discovery role', async () => { + setGlobalDispatcher(GetAccountsSelfManaged); + + const mockAwsClient = { + createStsClient() { + const accessError = new Error(); + accessError.Code = ACCESS_DENIED; + + return { + getCredentials: sinon.stub() + .onFirstCall().rejects(accessError) + .onSecondCall().resolves({accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken'}) + } + } + }; + + + const client = createApiClient({...defaultMockAwsClient, ...mockAwsClient}, appSync, defaultConfig); + + const accounts = await client.getAccounts(); + + const accX = accounts.find(x => x.accountId === ACCOUNT_X); + const accY = accounts.find(x => x.accountId === ACCOUNT_Y); + + assert.isFalse(accX.isIamRoleDeployed); + assert.isTrue(accY.isIamRoleDeployed); + }); + + it('should retrieve accounts when not in AWS Organization', async () => { + setGlobalDispatcher(GetAccountsSelfManaged); + + const mockAwsClient = { + createStsClient() { + return { + getCredentials: sinon.stub() + .onFirstCall().resolves({accessKeyId: 'accessKeyIdX', secretAccessKey: 'secretAccessKeyX', sessionToken: 'sessionTokenX'}) + .onSecondCall().resolves({accessKeyId: 'accessKeyIdY', secretAccessKey: 'secretAccessKeyY', sessionToken: 'sessionTokenY'}) + } + }, + createEc2Client() { + return { + async getAllRegions() { + return [{name: EU_WEST_1}, {name: US_EAST_1}] + } + }; + } + }; + + const client = createApiClient({...defaultMockAwsClient, ...mockAwsClient}, appSync, { + ...defaultConfig, isUsingOrganizations: false}); + + const accounts = await client.getAccounts(); + + const accX = accounts.find(x => x.accountId === ACCOUNT_X); + const accY = accounts.find(x => x.accountId === ACCOUNT_Y); + + assert.deepEqual(accX, { + accountId: ACCOUNT_X, + credentials: { + accessKeyId: 'accessKeyIdX', + secretAccessKey: 'secretAccessKeyX', + sessionToken: 'sessionTokenX', + }, + name: 'Account X', + isIamRoleDeployed: true, + regions: [ + EU_WEST_1, + US_EAST_1 + ] + }); + assert.deepEqual(accY, { + accountId: ACCOUNT_Y, + credentials: { + accessKeyId: 'accessKeyIdY', + secretAccessKey: 'secretAccessKeyY', + sessionToken: 'sessionTokenY', + }, + name: 'Account Y', + isIamRoleDeployed: true, + regions: [ + EU_WEST_1 + ] + }); + + }); + + it('should retrieve accounts from AWS Organizations', async () => { + setGlobalDispatcher(GetAccountsOrgsEmpty); + + const mockAwsClient = { + createStsClient() { + return { + getCredentials: sinon.stub() + .onFirstCall().resolves({accessKeyId: 'accessKeyIdX', secretAccessKey: 'secretAccessKeyX', sessionToken: 'sessionTokenX'}) + .onSecondCall().resolves({accessKeyId: 'accessKeyIdY', secretAccessKey: 'secretAccessKeyY', sessionToken: 'sessionTokenY'}) + } + }, + createConfigServiceClient() { + return { + async getConfigAggregator() { + return { + OrganizationAggregationSource: { + AllAwsRegions: true + } + }; + } + } + }, + createEc2Client() { + return { + async getAllRegions() { + return [{name: EU_WEST_1}, {name: US_EAST_1}] + } + }; + }, + createOrganizationsClient() { + return { + async getAllActiveAccountsFromParent() { + return [ + {Id: ACCOUNT_X, Name: 'Account X', isManagementAccount: true, Arn: `arn:aws:organizations::${ACCOUNT_X}:account/o-exampleorgid/:${ACCOUNT_X}`}, + {Id: ACCOUNT_Y, Name: 'Account Y', Arn: `arn:aws:organizations:::${ACCOUNT_Y}:account/o-exampleorgid/:${ACCOUNT_Y}`} + ] + } + } + } + }; + + const client = createApiClient({...defaultMockAwsClient, ...mockAwsClient}, appSync, { + ...defaultConfig, isUsingOrganizations: true}); + + const accounts = await client.getAccounts(); + + const accX = accounts.find(x => x.accountId === ACCOUNT_X); + const accY = accounts.find(x => x.accountId === ACCOUNT_Y); + + assert.deepEqual(accX, { + accountId: ACCOUNT_X, + credentials: { + accessKeyId: 'accessKeyIdX', + secretAccessKey: 'secretAccessKeyX', + sessionToken: 'sessionTokenX', + }, + name: 'Account X', + organizationId: 'o-exampleorgid', + isIamRoleDeployed: true, + isManagementAccount: true, + regions: [ + EU_WEST_1, + US_EAST_1 + ], + toDelete: false + }); + + assert.deepEqual(accY, { + accountId: ACCOUNT_Y, + credentials: { + accessKeyId: 'accessKeyIdY', + secretAccessKey: 'secretAccessKeyY', + sessionToken: 'sessionTokenY', + }, + name: 'Account Y', + organizationId: 'o-exampleorgid', + isIamRoleDeployed: true, + regions: [ + EU_WEST_1, + US_EAST_1 + ], + toDelete: false + }); + + }); + + it('should mark accounts for deletion in AWS Organizations', async () => { + setGlobalDispatcher(GetAccountsOrgsDeleted); + + const mockAwsClient = { + createStsClient() { + return { + getCredentials: sinon.stub() + .onFirstCall().resolves({accessKeyId: 'accessKeyIdX', secretAccessKey: 'secretAccessKeyX', sessionToken: 'sessionTokenX'}) + .onSecondCall().resolves({accessKeyId: 'accessKeyIdY', secretAccessKey: 'secretAccessKeyY', sessionToken: 'sessionTokenY'}) + .onThirdCall().resolves({accessKeyId: 'accessKeyIdZ', secretAccessKey: 'secretAccessKeyZ', sessionToken: 'sessionTokenZ'}) + } + }, + createConfigServiceClient() { + return { + async getConfigAggregator() { + return { + OrganizationAggregationSource: { + AllAwsRegions: true + } + }; + } + } + }, + createEc2Client() { + return { + async getAllRegions() { + return [{name: EU_WEST_1}, {name: US_EAST_1}] + } + }; + }, + createOrganizationsClient() { + return { + async getAllActiveAccountsFromParent() { + return [ + {Id: ACCOUNT_X, Name: 'Account X', isManagementAccount: true, Arn: `arn:aws:organizations::${ACCOUNT_X}:account/o-exampleorgid/:${ACCOUNT_X}`}, + {Id: ACCOUNT_Y, Name: 'Account Y', Arn: `arn:aws:organizations:::${ACCOUNT_Y}:account/o-exampleorgid/:${ACCOUNT_Y}`} + ] + } + } + } + }; + + const client = createApiClient({...defaultMockAwsClient, ...mockAwsClient}, appSync, { + ...defaultConfig, isUsingOrganizations: true}); + + const accounts = await client.getAccounts(); + + const accZ = accounts.find(x => x.accountId === ACCOUNT_Z); + + assert.deepEqual(accZ, { + accountId: ACCOUNT_Z, + credentials: { + accessKeyId: 'accessKeyIdZ', + secretAccessKey: 'secretAccessKeyZ', + sessionToken: 'sessionTokenZ', + }, + name: 'Account Z', + organizationId: 'o-exampleorgid', + isIamRoleDeployed: true, + + regions: [ + {name: EU_WEST_1}, {name: US_EAST_1} + ], + toDelete: true + }); + + }); + + it('should retain last crawled time from accounts from AWS Organizations', async () => { + setGlobalDispatcher(GetAccountsOrgsLastCrawled); + + const mockAwsClient = { + createStsClient() { + return { + getCredentials: sinon.stub() + .onFirstCall().resolves({accessKeyId: 'accessKeyIdX', secretAccessKey: 'secretAccessKeyX', sessionToken: 'sessionTokenX'}) + } + }, + createConfigServiceClient() { + return { + async getConfigAggregator() { + return { + OrganizationAggregationSource: { + AllAwsRegions: true + } + }; + } + } + }, + createEc2Client() { + return { + async getAllRegions() { + return [{name: EU_WEST_1}, {name: US_EAST_1}] + } + }; + }, + createOrganizationsClient() { + return { + async getAllActiveAccountsFromParent() { + return [ + {Id: ACCOUNT_X, Name: 'Account X', isManagementAccount: true, Arn: `arn:aws:organizations::${ACCOUNT_X}:account/o-exampleorgid/:${ACCOUNT_X}`}, + ] + } + } + } + }; + + const client = createApiClient({...defaultMockAwsClient, ...mockAwsClient}, appSync, { + ...defaultConfig, isUsingOrganizations: true}); + + const accounts = await client.getAccounts(); + + const accX = accounts.find(x => x.accountId === ACCOUNT_X); + + assert.deepEqual(accX, { + accountId: ACCOUNT_X, + credentials: { + accessKeyId: 'accessKeyIdX', + secretAccessKey: 'secretAccessKeyX', + sessionToken: 'sessionTokenX', + }, + name: 'Account X', + organizationId: 'o-exampleorgid', + isIamRoleDeployed: true, + isManagementAccount: true, + lastCrawled: "2022-10-25T00:00:00.000Z", + regions: [ + EU_WEST_1, + US_EAST_1 + ], + toDelete: false + }); + }); + + }); + + afterAll(() => { + setGlobalDispatcher(globalDispatcher); + }) + +}); \ No newline at end of file diff --git a/source/backend/discovery/test/awsClient.test.mjs b/source/backend/discovery/test/awsClient.test.mjs new file mode 100644 index 00000000..cf414cb6 --- /dev/null +++ b/source/backend/discovery/test/awsClient.test.mjs @@ -0,0 +1,1989 @@ +import {assert, afterAll, beforeAll, describe, it} from 'vitest'; +import pThrottle from 'p-throttle'; +import {mockClient} from 'aws-sdk-client-mock'; +import sinon from 'sinon'; +import { + APIGatewayClient, + GetAuthorizersCommand, + GetMethodCommand, + GetResourcesCommand, +} from '@aws-sdk/client-api-gateway'; +import { + ServiceCatalogAppRegistryClient, + GetApplicationCommand, + ListApplicationsCommand +} from '@aws-sdk/client-service-catalog-appregistry'; +import { + BatchGetAggregateResourceConfigCommand, + ConfigServiceClient, + DescribeConfigurationAggregatorsCommand, + ListAggregateDiscoveredResourcesCommand, + SelectAggregateResourceConfigCommand, +} from '@aws-sdk/client-config-service'; +import { + DescribeStreamCommand, + DynamoDBStreamsClient +} from '@aws-sdk/client-dynamodb-streams'; +import { + DescribeRegionsCommand, + DescribeSpotFleetRequestsCommand, + DescribeSpotInstanceRequestsCommand, + DescribeTransitGatewayAttachmentsCommand, + EC2Client +} from '@aws-sdk/client-ec2'; +import { + DescribeContainerInstancesCommand, + DescribeTasksCommand, + ECSClient, + ListContainerInstancesCommand, + ListTasksCommand +} from '@aws-sdk/client-ecs'; +import { + DescribeLoadBalancersCommand, + ElasticLoadBalancingClient +} from '@aws-sdk/client-elastic-load-balancing'; +import { + DescribeTargetGroupsCommand, + DescribeTargetHealthCommand, + ElasticLoadBalancingV2Client +} from '@aws-sdk/client-elastic-load-balancing-v2'; +import { + DescribeNodegroupCommand, + EKSClient, + ListNodegroupsCommand +} from '@aws-sdk/client-eks'; +import { + IAMClient, + ListPoliciesCommand +} from '@aws-sdk/client-iam'; +import { + LambdaClient, + ListEventSourceMappingsCommand, + ListFunctionsCommand +} from '@aws-sdk/client-lambda'; +import { + MediaConnectClient, ListFlowsCommand +} from '@aws-sdk/client-mediaconnect'; +import { + ListDomainNamesCommand, + DescribeDomainsCommand, + OpenSearchClient +} from '@aws-sdk/client-opensearch'; +import { + OrganizationsClient, + ListRootsCommand, + DescribeOrganizationCommand, + ListAccountsCommand, + ListAccountsForParentCommand, + ListOrganizationalUnitsForParentCommand +} from '@aws-sdk/client-organizations'; +import { + ListSubscriptionsCommand, + SNSClient +} from '@aws-sdk/client-sns'; +import { + AssumeRoleCommand, + STSClient +} from '@aws-sdk/client-sts'; +import {OPENSEARCH} from '../src/lib/constants.mjs'; +import {ListDataSourcesCommand, AppSyncClient, ListResolversCommand} from '@aws-sdk/client-appsync'; +import {throttledPaginator, createAwsClient} from '../src/lib/awsClient.mjs'; + +const awsClient = createAwsClient(); +const EU_WEST_1 = 'eu-west-1'; + +describe('awsClient', () => { + + const mockCredentials = { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + sessionToken: 'optionalSessionToken' + }; + + describe('throttledPaginator', () => { + const throttler = pThrottle({ + limit: 1, + interval: 10 + }); + + it('should handle one page', async() => { + const asyncGenerator = (async function* () { + yield 1; + })(); + + const results = []; + for await(const x of throttledPaginator(throttler, asyncGenerator)) { + results.push(x); + } + assert.deepEqual(results, [1]); + }); + + it('should handle multiple pages', async() => { + const asyncGenerator = (async function* () { + yield* [1, 2, 3]; + })(); + + const results = []; + for await(const x of throttledPaginator(throttler, asyncGenerator)) { + results.push(x); + } + assert.deepEqual(results, [1, 2, 3]); + }); + + }); + + describe('apiGatewayClient', () => { + const { + getAuthorizers, + getMethod, + getResources, + } = awsClient.createApiGatewayClient(mockCredentials, EU_WEST_1); + + describe('getAuthorizers', () => { + + it('should get authorizers', async () => { + const mockApiGatewayClient = mockClient(APIGatewayClient); + + const authorizers = { + restApiId: [ + {id: 'authorizerId1'}, + {id: 'authorizerId2'}, + {id: 'authorizerId3'}, + {id: 'authorizerId4'} + ] + }; + + mockApiGatewayClient + .on(GetAuthorizersCommand) + .callsFake(({restApiId}) => { + return {items: authorizers[restApiId]}; + }); + + const actual = await getAuthorizers('restApiId'); + + assert.deepEqual(actual, [ + {id: 'authorizerId1'}, + {id: 'authorizerId2'}, + {id: 'authorizerId3'}, + {id: 'authorizerId4'} + ]); + }); + + it('should get methods', async () => { + const mockApiGatewayClient = mockClient(APIGatewayClient); + + const methods = { + restApiId: { + resourceId: { + GET: { + httpMethod: 'GET' + } + } + } + }; + + mockApiGatewayClient + .on(GetMethodCommand) + .callsFake(({restApiId, resourceId, httpMethod}) => { + return methods[restApiId][resourceId][httpMethod]; + }); + + const actual = await getMethod('GET', 'resourceId', 'restApiId'); + + assert.deepEqual(actual, {httpMethod: 'GET'}); + }); + + it('should get resources', async () => { + const mockApiGatewayClient = mockClient(APIGatewayClient); + + const resources = { + restApiId: { + items: [ + {id: 'resourceId1'}, + {id: 'resourceId2'}, + ], + position: 'restApiId-position' + }, + 'restApiId-position': { + items: [ + {id: 'resourceId3'}, + {id: 'resourceId4'} + ] + } + }; + + mockApiGatewayClient + .on(GetResourcesCommand) + .callsFake(({restApiId, position}) => { + if(position != null) return resources[position]; + return resources[restApiId]; + }); + + const actual = await getResources('restApiId'); + assert.deepEqual(actual, [ + { + 'id': 'resourceId1' + }, + { + 'id': 'resourceId2' + }, + { + 'id': 'resourceId3' + }, + { + 'id': 'resourceId4' + } + ]); + }); + + }); + + }); + + describe('serviceCatalogAppRegistryClient', () => { + const { + getAllApplications + } = awsClient.createServiceCatalogAppRegistryClient(mockCredentials, EU_WEST_1); + + describe('getAllApplications', () => { + + it('should return hydrated applications list', async () => { + const mockAppRegistryClient = mockClient(ServiceCatalogAppRegistryClient); + + const listApplicationsResp = { + firstPage: { + applications: [ + {name: 'applicationName1'}, + {name: 'applicationName2'}, + ], + nextToken: 'applicationToken' + }, + applicationToken: { + applications: [ + {name: 'applicationName3'}, + {name: 'applicationName4'}, + ] + } + }; + + mockAppRegistryClient + .on(ListApplicationsCommand) + .callsFake(({nextToken} ) => { + if(nextToken != null) return listApplicationsResp[nextToken]; + return listApplicationsResp.firstPage; + }); + + const applications = { + 'applicationName1': { + name: 'applicationName1', applicationTag: { + awsApplication: 'applicationTag1' + } + }, + 'applicationName2': { + name: 'applicationName2', applicationTag: { + awsApplication: 'applicationTag2' + } + }, + 'applicationName3': { + name: 'applicationName3', applicationTag: { + awsApplication: 'applicationTag3' + } + }, + 'applicationName4': { + name: 'applicationName4', applicationTag: { + awsApplication: 'applicationTag4' + } + }, + } + + mockAppRegistryClient + .on(GetApplicationCommand) + .callsFake(({application}) => { + return applications[application]; + }); + + const actual = await getAllApplications(); + + assert.deepEqual(actual, [ + { + name: 'applicationName1', applicationTag: { + awsApplication: 'applicationTag1' + } + }, + { + name: 'applicationName2', applicationTag: { + awsApplication: 'applicationTag2' + } + }, + { + name: 'applicationName3', applicationTag: { + awsApplication: 'applicationTag3' + } + }, + { + name: 'applicationName4', applicationTag: { + awsApplication: 'applicationTag4' + } + } + ]); + }); + + }); + + }); + + describe('configServiceClient', () => { + const { + getAllAggregatorResources, + getAggregatorResources, + getConfigAggregator, + } = awsClient.createConfigServiceClient(mockCredentials, EU_WEST_1); + + describe('getAllAggregatorResources', () => { + + it('should get resources from Config aggregator', async () => { + const mockConfigClient = mockClient(ConfigServiceClient); + + const resources = { + configAggregator: { + Results: [ + JSON.stringify({arn: 'resourceArn1', resourceType: 'AWS::EC2::Instance'}), + JSON.stringify({arn: 'resourceArn2', resourceType: 'AWS::IAM::Role'}) + ], + NextToken: 'configAggregator-token' + }, + 'configAggregator-token': { + Results: [ + JSON.stringify({arn: 'resourceArn3', resourceType: 'AWS::Lambda::Function'}), + JSON.stringify({arn: 'resourceArn4', resourceType: 'AWS::S3::Bucket'}) + ] + } + }; + + mockConfigClient + .on(SelectAggregateResourceConfigCommand) + .callsFake(({ConfigurationAggregatorName, Expression, NextToken}) => { + const expectedExpression = + 'SELECT *, configuration, configurationItemStatus, relationships, supplementaryConfiguration, tags'; + + assert.strictEqual( + Expression.replace(/\s+/g, ' ').trim(), expectedExpression + ); + + if(NextToken != null) return resources[NextToken]; + return resources[ConfigurationAggregatorName]; + }); + + const actual = await getAllAggregatorResources( + 'configAggregator', { + excludes: { + resourceTypes: [] + } + }); + + assert.deepEqual(actual, [ + { + arn: 'resourceArn1', + resourceType: 'AWS::EC2::Instance' + }, + { + arn: 'resourceArn2', + resourceType: 'AWS::IAM::Role' + }, + { + arn: 'resourceArn3', + resourceType: 'AWS::Lambda::Function' + }, + { + arn: 'resourceArn4', + resourceType: 'AWS::S3::Bucket' + } + ]); + }); + + // This test will be re-enabled when https://github.com/m-radzikowski/aws-sdk-client-mock/issues/205 is + // resolved. + it.skip('should retry getting resources if there is an error', async () => { + const mockConfigClient = mockClient(ConfigServiceClient); + + mockConfigClient + .on(SelectAggregateResourceConfigCommand) + .rejectsOnce('reject') + .resolvesOnce({ + Results: [ + JSON.stringify({arn: 'resourceArn1', resourceType: 'AWS::EC2::Instance'}), + JSON.stringify({arn: 'resourceArn2', resourceType: 'AWS::IAM::Role'}) + ] + }) + + const actual = await getAllAggregatorResources( + 'configAggregator', { + excludes: { + resourceTypes: [] + } + }); + + assert.deepEqual(actual, [ + { + arn: 'resourceArn1', + resourceType: 'AWS::EC2::Instance' + }, + { + arn: 'resourceArn2', + resourceType: 'AWS::IAM::Role' + } + ]); + }); + + it('should filter by resource type when getting resources from Config aggregator', async () => { + const mockConfigClient = mockClient(ConfigServiceClient); + + const resources = { + configAggregator: { + Results: [ + JSON.stringify({arn: 'resourceArn1', resourceType: 'AWS::EC2::Instance'}), + JSON.stringify({arn: 'resourceArn2', resourceType: 'AWS::IAM::Role'}) + ], + NextToken: 'configAggregator-token' + }, + 'configAggregator-token': { + Results: [ + JSON.stringify({arn: 'resourceArn3', resourceType: 'AWS::Lambda::Function'}), + JSON.stringify({arn: 'resourceArn4', resourceType: 'AWS::S3::Bucket'}) + ] + } + }; + + mockConfigClient + .on(SelectAggregateResourceConfigCommand) + .callsFake(({ConfigurationAggregatorName, Expression, NextToken}) => { + const expectedExpression = + `SELECT *, configuration, configurationItemStatus, relationships, supplementaryConfiguration, tags WHERE resourceType NOT IN ('AWS::RDS:DbInstance','AWS::EC2::VPC')`; + + assert.strictEqual( + Expression.replace(/\s+/g, ' ').trim(), expectedExpression + ); + + if(NextToken != null) return resources[NextToken]; + return resources[ConfigurationAggregatorName]; + }); + + const actual = await getAllAggregatorResources( + 'configAggregator', { + excludes: { + resourceTypes: ['AWS::RDS:DbInstance', 'AWS::EC2::VPC'] + } + }); + + assert.deepEqual(actual, [ + { + arn: 'resourceArn1', + resourceType: 'AWS::EC2::Instance' + }, + { + arn: 'resourceArn2', + resourceType: 'AWS::IAM::Role' + }, + { + arn: 'resourceArn3', + resourceType: 'AWS::Lambda::Function' + }, + { + arn: 'resourceArn4', + resourceType: 'AWS::S3::Bucket' + } + ]); + }); + + }); + + describe('getAggregatorResources', () => { + + it('should get resources for specific resource types', async () => { + const mockConfigClient = mockClient(ConfigServiceClient); + + const resourcesList = { + 'AWS::EC2:instance': { + ResourceIdentifiers: [ + {ResourceId: 'ResourceId1'}, + {ResourceId: 'ResourceId2'} + ], + NextToken: 'AWS::EC2:instance-token' + }, + 'AWS::EC2:instance-token': { + ResourceIdentifiers: [ + {ResourceId: 'ResourceId3'}, + {ResourceId: 'ResourceId4'} + ] + } + } + + mockConfigClient + .on(ListAggregateDiscoveredResourcesCommand) + .callsFake(({ResourceType, NextToken}) => { + if(NextToken != null) return resourcesList[NextToken]; + return resourcesList[ResourceType]; + }); + + const resources = { + ResourceId1: {Arn: 'ResourceArn1'}, + ResourceId2: {Arn: 'ResourceArn2'}, + ResourceId3: {Arn: 'ResourceArn3'}, + ResourceId4: {Arn: 'ResourceArn4'}, + }; + + mockConfigClient + .on(BatchGetAggregateResourceConfigCommand) + .callsFake(({ResourceIdentifiers}) => { + return { + BaseConfigurationItems: ResourceIdentifiers.map(({ResourceId}) => { + return resources[ResourceId]; + }) + }; + }); + + const actual = await getAggregatorResources('aggregatorName', 'AWS::EC2:instance'); + + assert.deepEqual(actual, [ + { + Arn: 'ResourceArn1' + }, + { + Arn: 'ResourceArn2' + }, + { + Arn: 'ResourceArn3' + }, + { + Arn: 'ResourceArn4' + } + ]); + }); + + }); + + describe('getConfigAggregator', () => { + + it('should get config aggregator', async () => { + const mockConfigClient = mockClient(ConfigServiceClient); + + mockConfigClient + .on(DescribeConfigurationAggregatorsCommand) + .resolves({ + ConfigurationAggregators: [{ + ConfigurationAggregatorName: 'configAggregatorName' + }] + }); + + const actual = await getConfigAggregator('configAggregatorName'); + + assert.deepEqual(actual, {ConfigurationAggregatorName: 'configAggregatorName'}); + }); + + }); + }); + + describe('dynamoDBStreamsClient', () => { + const { + describeStream + } = awsClient.createDynamoDBStreamsClient(mockCredentials, EU_WEST_1); + + describe('describeStream', () => { + + it('should get stream details', async () => { + const mockDynamoDBStreamsClient = mockClient(DynamoDBStreamsClient); + + mockDynamoDBStreamsClient + .on(DescribeStreamCommand) + .resolves({ + StreamDescription: { + StreamArn: 'streamArn1' + } + }); + + const actual = await describeStream('streamArn1'); + + assert.deepEqual(actual, { + StreamArn: 'streamArn1' + }); + }); + + }); + }); + + describe('ec2Client', () => { + const { + getAllRegions, + getAllSpotFleetRequests, + getAllSpotInstanceRequests, + getAllTransitGatewayAttachments + } = awsClient.createEc2Client(mockCredentials, EU_WEST_1); + + describe('getAllRegions', () => { + + it('should get all regions', async () => { + const mockEc2Client = mockClient(EC2Client); + + mockEc2Client + .on(DescribeRegionsCommand) + .resolves({ + Regions: [ + {RegionName: 'eu-west-1'}, + {RegionName: 'eu-west-2'}, + {RegionName: 'us-east-1'} + ] + }); + + const actual = await getAllRegions(); + + assert.deepEqual(actual, [ + { + name: 'eu-west-1' + }, + { + name: 'eu-west-2' + }, + { + name: 'us-east-1' + } + ]); + }); + + }); + + describe('getAllSpotFleetRequests', () => { + + it('should get all spot fleet requests', async () => { + const mockEc2Client = mockClient(EC2Client); + + const spotFleets = { + firstPage: { + SpotFleetRequestConfigs: [ + {SpotFleetRequestId: 'sfr-uuid1'}, + {SpotFleetRequestId: 'sfr-uuid2'} + ], + NextToken: 'spotFleetRequest-token' + }, + 'spotFleetRequest-token': { + SpotFleetRequestConfigs: [ + {SpotFleetRequestId: 'sfr-uuid3'}, + {SpotFleetRequestId: 'sfr-uuid4'} + ] + } + } + + mockEc2Client + .on(DescribeSpotFleetRequestsCommand) + .callsFake(({NextToken}) => { + if(NextToken != null) return spotFleets[NextToken]; + return spotFleets.firstPage; + }); + + const actual = await getAllSpotFleetRequests(); + + assert.deepEqual(actual, [ + {SpotFleetRequestId: 'sfr-uuid1'}, + {SpotFleetRequestId: 'sfr-uuid2'}, + {SpotFleetRequestId: 'sfr-uuid3'}, + {SpotFleetRequestId: 'sfr-uuid4'} + ]); + }); + + }); + + describe('getAllSpotInstanceRequests', () => { + + it('should get all spot instance requests', async () => { + const mockEc2Client = mockClient(EC2Client); + + const spotInstances = { + firstPage: { + SpotInstanceRequests: [ + {SpotInstanceRequestId: 'sfi-1111111'}, + {SpotInstanceRequestId: 'sfi-2222222'} + ], + NextToken: 'SpotInstanceRequests-token' + }, + 'SpotInstanceRequests-token': { + SpotInstanceRequests: [ + {SpotInstanceRequestId: 'sfi-3333333'}, + {SpotInstanceRequestId: 'sfi-4444444'} + ] + } + } + + mockEc2Client + .on(DescribeSpotInstanceRequestsCommand) + .callsFake(({NextToken}) => { + if(NextToken != null) return spotInstances[NextToken]; + return spotInstances.firstPage; + }); + + const actual = await getAllSpotInstanceRequests(); + + assert.deepEqual(actual, [ + { + SpotInstanceRequestId: 'sfi-1111111' + }, + { + SpotInstanceRequestId: 'sfi-2222222' + }, + { + SpotInstanceRequestId: 'sfi-3333333' + }, + { + SpotInstanceRequestId: 'sfi-4444444' + } + ]); + }); + + }); + + describe('getAllTransitGatewayAttachments', () => { + + it('should get all transit gateway attachments', async () => { + const mockEc2Client = mockClient(EC2Client); + + const attachments = { + firstPage: { + TransitGatewayAttachments: [ + {TransitGatewayId: 'tgw-111111111111', ResourceType: 'vpc'}, + {TransitGatewayId: 'tgw-222222222222', ResourceType: 'direct-connect-gateway'} + ], + NextToken: 'attachments-token' + }, + 'attachments-token': { + TransitGatewayAttachments: [ + {TransitGatewayId: 'tgw-333333333333', ResourceType: 'vpc'}, + {TransitGatewayId: 'tgw-444444444444', ResourceType: 'direct-connect-gateway'} + ] + } + }; + + mockEc2Client + .on(DescribeTransitGatewayAttachmentsCommand) + .callsFake(({NextToken}) => { + if(NextToken != null) return attachments[NextToken]; + return attachments.firstPage; + }); + + const actual = await getAllTransitGatewayAttachments(); + + assert.deepEqual(actual, [ + { + TransitGatewayId: 'tgw-111111111111', + ResourceType: 'vpc' + }, + { + TransitGatewayId: 'tgw-222222222222', + ResourceType: 'direct-connect-gateway' + }, + { + TransitGatewayId: 'tgw-333333333333', + ResourceType: 'vpc' + }, + { + TransitGatewayId: 'tgw-444444444444', + ResourceType: 'direct-connect-gateway' + } + ]); + }); + + it('should get filtered list of transit gateway attachments', async () => { + const mockEc2Client = mockClient(EC2Client); + + const attachments = { + vpc: { + TransitGatewayAttachments: [ + {TransitGatewayId: 'tgw-111111111111', ResourceType: 'vpc'}, + {TransitGatewayId: 'tgw-222222222222', ResourceType: 'vpc'} + ] + } + }; + + mockEc2Client + .on(DescribeTransitGatewayAttachmentsCommand) + .callsFake(({Filters}) => { + return attachments[Filters[0].Values[0]]; + }); + + const actual = await getAllTransitGatewayAttachments([{Name: 'resource-type', Values: ['vpc']}]); + + assert.deepEqual(actual, [ + { + TransitGatewayId: 'tgw-111111111111', + ResourceType: 'vpc' + }, + { + TransitGatewayId: 'tgw-222222222222', + ResourceType: 'vpc' + } + ]); + }); + + }); + + }); + + describe('ecsClient', () => { + const { + getAllClusterInstances, + getAllClusterTasks, + getAllServiceTasks + } = awsClient.createEcsClient(mockCredentials, EU_WEST_1); + + describe('getAllClusterInstances', () => { + + it('should get all EC2 instances associated with cluster', async () => { + const mockEcsClient = mockClient(ECSClient); + + const instanceArns = { + cluster: { + containerInstanceArns: [ + 'containerInstanceArn1', + 'containerInstanceArn2' + ], + nextToken: 'clusterToken' + }, + clusterToken: { + containerInstanceArns: [ + 'containerInstanceArn3', + 'containerInstanceArn4' + ] + } + }; + + mockEcsClient + .on(ListContainerInstancesCommand) + .callsFake(({cluster, nextToken} ) => { + if(nextToken != null) return instanceArns[nextToken]; + return instanceArns[cluster]; + }); + + const instances = { + 'containerInstanceArn1': {ec2InstanceId: 'i-1111111111'}, + 'containerInstanceArn2': {ec2InstanceId: 'i-2222222222'}, + 'containerInstanceArn3': {ec2InstanceId: 'i-3333333333'}, + 'containerInstanceArn4': {ec2InstanceId: 'i-4444444444'}, + } + + mockEcsClient + .on(DescribeContainerInstancesCommand) + .callsFake(({containerInstances}) => { + return { + containerInstances: containerInstances.map(arn => instances[arn]) + }; + }); + + const actual = await getAllClusterInstances('cluster'); + + assert.deepEqual(actual, [ + 'i-1111111111', + 'i-2222222222', + 'i-3333333333', + 'i-4444444444' + ]); + }); + + }); + + describe('getAllClusterTasks', () => { + + it('should get all tasks running in a cluster', async () => { + const mockEcsClient = mockClient(ECSClient); + + const taskArns = { + cluster: { + taskArns: [ + 'taskArn1', + 'taskArn2' + ], + nextToken: 'taskToken' + }, + taskToken: { + taskArns: [ + 'taskArn3', + 'taskArn4' + ] + } + }; + + mockEcsClient + .on(ListTasksCommand) + .callsFake((input) => { + const {cluster, nextToken} = input; + if(nextToken != null) return taskArns[nextToken]; + return taskArns[cluster]; + }); + + const tasksObj = { + taskArn1: {taskArn: 'taskArn1'}, + taskArn2: {taskArn: 'taskArn2'}, + taskArn3: {taskArn: 'taskArn3'}, + taskArn4: {taskArn: 'taskArn4'} + } + + mockEcsClient + .on(DescribeTasksCommand) + .callsFake(({tasks}) => { + return { + tasks: tasks.map(arn => tasksObj[arn]) + }; + }); + + const actual = await getAllClusterTasks('cluster'); + + assert.deepEqual(actual, [ + { + taskArn: 'taskArn1' + }, + { + taskArn: 'taskArn2' + }, + { + taskArn: 'taskArn3' + }, + { + taskArn: 'taskArn4' + } + ]) + }); + }); + + describe('getAllServiceTasks', () => { + + it('should get all tasks associated with a service', async () => { + const mockEcsClient = mockClient(ECSClient); + + const taskArns = { + 'cluster-service': { + taskArns: [ + 'taskArn1', + 'taskArn2' + ], + nextToken: 'taskToken' + }, + taskToken: { + taskArns: [ + 'taskArn3', + 'taskArn4' + ] + } + }; + + mockEcsClient + .on(ListTasksCommand) + .callsFake((input) => { + const {cluster, serviceName, nextToken} = input; + if(nextToken != null) return taskArns[nextToken]; + return taskArns[`${cluster}-${serviceName}`]; + }); + + const tasksObj = { + taskArn1: {taskArn: 'serviceTaskArn1'}, + taskArn2: {taskArn: 'serviceTaskArn2'}, + taskArn3: {taskArn: 'serviceTaskArn3'}, + taskArn4: {taskArn: 'serviceTaskArn4'} + } + + mockEcsClient + .on(DescribeTasksCommand) + .callsFake(({tasks}) => { + return { + tasks: tasks.map(arn => tasksObj[arn]) + }; + }); + + const actual = await getAllServiceTasks('cluster', 'service'); + + assert.deepEqual(actual, [ + { + taskArn: 'serviceTaskArn1' + }, + { + taskArn: 'serviceTaskArn2' + }, + { + taskArn: 'serviceTaskArn3' + }, + { + taskArn: 'serviceTaskArn4' + } + ]) + }); + }); + + }); + + describe('elbClient', () => { + const { + getLoadBalancerInstances + } = awsClient.createElbClient(mockCredentials, EU_WEST_1); + + describe('getLoadBalancerInstances', () => { + + it('should handle missing Instances field', async () => { + const mockElbClient = mockClient(ElasticLoadBalancingClient); + + const elb = { + loadBalancer: { + LoadBalancerName: 'loadBalancer', + } + } + + mockElbClient + .on(DescribeLoadBalancersCommand) + .callsFake(({LoadBalancerNames}) => { + return { + LoadBalancerDescriptions: [ + elb[LoadBalancerNames[0]] + ] + } + }); + + const actual = await getLoadBalancerInstances('loadBalancer'); + + assert.deepEqual(actual, []); + }); + + it('should get EC2 instances associated with ELB', async () => { + const mockElbClient = mockClient(ElasticLoadBalancingClient); + + const elb = { + loadBalancer: { + LoadBalancerName: 'loadBalancer', + Instances: [ + {InstanceId: 'i-1111111111'}, + {InstanceId: 'i-2222222222'}, + ] + } + } + + mockElbClient + .on(DescribeLoadBalancersCommand) + .callsFake(({LoadBalancerNames}) => { + return { + LoadBalancerDescriptions: [ + elb[LoadBalancerNames[0]] + ] + } + }); + + const actual = await getLoadBalancerInstances('loadBalancer'); + + assert.deepEqual(actual, [ + 'i-1111111111', + 'i-2222222222' + ]); + }); + + }); + }); + + describe('elbClient', () => { + const { + describeTargetHealth, + getAllTargetGroups + } = awsClient.createElbV2Client(mockCredentials, EU_WEST_1); + + describe('describeTargetHealth', () => { + + it('should get target health', async () => { + const mockElbV2Client = mockClient(ElasticLoadBalancingV2Client); + + const targetHealth = { + targetGroupArn: { + TargetHealthDescriptions: [ + {Target: {ID: 'i-111111111'}}, + {Target: {ID: 'i-222222222'}}, + {Target: {ID: 'i-333333333'}}, + ] + } + }; + + mockElbV2Client + .on(DescribeTargetHealthCommand) + .callsFake(input => { + const {TargetGroupArn} = input; + return targetHealth[TargetGroupArn]; + }); + + const actual = await describeTargetHealth('targetGroupArn'); + + assert.deepEqual(actual, [ + {Target: {ID: 'i-111111111'}}, + {Target: {ID: 'i-222222222'}}, + {Target: {ID: 'i-333333333'}}, + ]); + }); + + }); + + describe('getAllTargetGroups', () => { + + it('should get all target groups', async () => { + const mockElbV2Client = mockClient(ElasticLoadBalancingV2Client); + + const targetGroups = { + firstPage: { + TargetGroups: [ + {TargetGroupArn: 'targetGroupArn1'}, + {TargetGroupArn: 'targetGroupArn2'} + ], + NextMarker: 'TargetGroups-marker' + }, + 'TargetGroups-marker': { + TargetGroups: [ + {TargetGroupArn: 'targetGroupArn3'}, + {TargetGroupArn: 'targetGroupArn4'} + ] + } + }; + + mockElbV2Client + .on(DescribeTargetGroupsCommand) + .callsFake(({TargetGroups, Marker}) => { + if(Marker != null) return targetGroups[Marker]; + return targetGroups.firstPage; + }); + + const actual = await getAllTargetGroups(); + + assert.deepEqual(actual, [ + {TargetGroupArn: 'targetGroupArn1'}, + {TargetGroupArn: 'targetGroupArn2'}, + {TargetGroupArn: 'targetGroupArn3'}, + {TargetGroupArn: 'targetGroupArn4'} + ]); + }); + + }); + + }); + + describe('eksClient', () => { + const { + listNodeGroups + } = awsClient.createEksClient(mockCredentials, EU_WEST_1); + + describe('listNodeGroups', () => { + + it('should list all node groups in cluster', async () => { + const mockEksClient = mockClient(EKSClient); + + const nodeGroupsList = { + 'eksCluster': { + nodegroups: [ + 'nodegroup1', + 'nodegroup2' + ], + nextToken: 'nodegroups-token' + }, + 'nodegroups-token': { + nodegroups: [ + 'nodegroup3', + 'nodegroup4' + ] + } + } + + mockEksClient + .on(ListNodegroupsCommand) + .callsFake(({clusterName, nextToken}) => { + if(nextToken != null) return nodeGroupsList[nextToken]; + return nodeGroupsList[clusterName]; + }); + + const nodeGroups = { + nodegroup1: {nodegroupArn: 'nodegroupArn1'}, + nodegroup2: {nodegroupArn: 'nodegroupArn2'}, + nodegroup3: {nodegroupArn: 'nodegroupArn3'}, + nodegroup4: {nodegroupArn: 'nodegroupArn4'}, + } + + mockEksClient + .on(DescribeNodegroupCommand) + .callsFake(({nodegroupName}) => { + return { + nodegroup: nodeGroups[nodegroupName] + }; + }) + + const actual = await listNodeGroups('eksCluster'); + + assert.deepEqual(actual, [ + { + nodegroupArn: 'nodegroupArn1' + }, + { + nodegroupArn: 'nodegroupArn2' + }, + { + nodegroupArn: 'nodegroupArn3' + }, + { + nodegroupArn: 'nodegroupArn4' + } + ]); + }); + + }); + }); + + describe('appSyncClient', () => { + describe('listDataSources', ()=> { + const { + listDataSources + } = awsClient.createAppSyncClient(mockCredentials, EU_WEST_1); + + it("should list data sources", async ()=> { + const mockAppSyncClient = mockClient(AppSyncClient); + + const dataSources = { + first : { + dataSources: [{ + dataSourceArn: "dataSourceArn1", + }], + nextToken: "second" + }, + second: { + dataSources: [{ + dataSourceArn: "dataSourceArn2", + }], + nextToken: "third" + }, + third: { + dataSources: [{ + dataSourceArn: "dataSourceArn3", + }], + nextToken: null + } + + + } + + mockAppSyncClient.on(ListDataSourcesCommand).callsFake(({nextToken}) => { + + if(nextToken != null) return dataSources[nextToken]; + return dataSources.first; + }) + + const actual = await listDataSources("fake-api") + + assert.deepEqual(actual, [{ dataSourceArn: "dataSourceArn1"}, {dataSourceArn:"dataSourceArn2"}, {dataSourceArn:"dataSourceArn3"}]) + }) + }) + + describe('listResolvers', () => { + const { + listResolvers + } = awsClient.createAppSyncClient(mockCredentials, EU_WEST_1); + + it("should list resolvers", async ()=> { + const mockAppSyncClient = mockClient(AppSyncClient); + const resolvers = { + first : { + resolvers: [{ + resolverArn: "resolverArn1", + }], + nextToken: "second" + }, + second: { + resolvers: [{ + resolverArn: "resolverArn2", + }], + nextToken: "third" + }, + third: { + resolvers: [{ + resolverArn: "resolverArn3", + }], + nextToken: null + } + } + + mockAppSyncClient.on(ListResolversCommand).callsFake(({nextToken}) => { + if(nextToken != null) return resolvers[nextToken]; + return resolvers.first; + }) + const actual = await listResolvers("fake-api", "Query") + assert.deepEqual(actual, [{ resolverArn: "resolverArn1"}, { resolverArn: "resolverArn2"}, { resolverArn: "resolverArn3"}]) + }) + }) + + }) + + + describe('iamClient', () => { + const { + getAllAttachedAwsManagedPolices + } = awsClient.createIamClient(mockCredentials, EU_WEST_1); + + describe('getAllAttachedAwsManagedPolices', () => { + + it('should get all attached polices', async () => { + const mockIamClient = mockClient(IAMClient); + + const attachedAwsManagedPolicies = { + 'AWS-true': { + Policies: [ + {Arn: 'policyArn1'}, + {Arn: 'policyArn2'} + ], + Marker: 'policiesMarker' + }, + policiesMarker: { + Policies: [ + {Arn: 'policyArn3'}, + {Arn: 'policyArn4'} + ] + } + }; + + mockIamClient + .on(ListPoliciesCommand) + .callsFake((input) => { + const {OnlyAttached, Scope, Marker} = input; + if(Marker != null) return attachedAwsManagedPolicies[Marker]; + return attachedAwsManagedPolicies[`${Scope}-${OnlyAttached}`]; + }); + + const actual = await getAllAttachedAwsManagedPolices(); + + assert.deepEqual(actual, [ + { + Arn: 'policyArn1' + }, + { + Arn: 'policyArn2' + }, + { + Arn: 'policyArn3' + }, + { + Arn: 'policyArn4' + } + ]); + }); + + }); + }); + + describe('lambdaClient', () => { + const { + getAllFunctions, + listEventSourceMappings + } = awsClient.createLambdaClient(mockCredentials, EU_WEST_1); + + describe('getAllFunctions', () => { + + it('should get all functions', async () => { + const mockLambdaClient = mockClient(LambdaClient); + + const functions = { + firstPage: { + Functions: [ + {FunctionName: 'Function1'}, + {FunctionName: 'Function2'}, + ], + NextMarker: 'listFunctionsMarker' + }, + listFunctionsMarker: { + Functions: [ + {FunctionName: 'Function3'}, + {FunctionName: 'Function4'} + ] + } + } + + mockLambdaClient + .on(ListFunctionsCommand) + .callsFake(({Marker}) => { + if(Marker != null) return functions[Marker]; + return functions.firstPage; + }); + + const actual = await getAllFunctions(); + + assert.deepEqual(actual, [ + { + FunctionName: 'Function1' + }, + { + FunctionName: 'Function2' + }, + { + FunctionName: 'Function3' + }, + { + FunctionName: 'Function4' + } + ]); + }); + + }); + + describe('listEventSourceMappings', () => { + + it('should get event source mappings for specific function', async () => { + const mockLambdaClient = mockClient(LambdaClient); + + const mappings = { + functionArn: { + EventSourceMappings: [ + {EventSourceArn: 'EventSourceArn1'}, + ], + NextMarker: 'listEventSourceMappingsMarker' + }, + listEventSourceMappingsMarker: { + EventSourceMappings: [ + {EventSourceArn: 'EventSourceArn2'}, + ] + } + } + + mockLambdaClient + .on(ListEventSourceMappingsCommand) + .callsFake(({FunctionName, Marker}) => { + if(Marker != null) return mappings[Marker]; + return mappings[FunctionName]; + }); + + const actual = await listEventSourceMappings('functionArn'); + + assert.deepEqual(actual, [ + { + EventSourceArn: 'EventSourceArn1' + }, + { + EventSourceArn: 'EventSourceArn2' + } + ]); + }); + + it('should get all event source mappings', async () => { + const mockLambdaClient = mockClient(LambdaClient); + + const mappings = { + firstPage: { + EventSourceMappings: [ + {EventSourceArn: 'EventSourceArn1'}, + {EventSourceArn: 'EventSourceArn2'}, + ], + NextMarker: 'listEventSourceMappingsMarker' + }, + listEventSourceMappingsMarker: { + EventSourceMappings: [ + {EventSourceArn: 'EventSourceArn3'}, + {EventSourceArn: 'EventSourceArn4'} + ] + } + } + + mockLambdaClient + .on(ListEventSourceMappingsCommand) + .callsFake(({Marker}) => { + if(Marker != null) return mappings[Marker]; + return mappings.firstPage; + }); + + const actual = await listEventSourceMappings(); + + assert.deepEqual(actual, [ + { + EventSourceArn: 'EventSourceArn1' + }, + { + EventSourceArn: 'EventSourceArn2' + }, + { + EventSourceArn: 'EventSourceArn3' + }, + { + EventSourceArn: 'EventSourceArn4' + } + ]); + }); + + }); + }); + + describe('mediaConnectClient', () => { + const {getAllFlows} = awsClient.createMediaConnectClient(mockCredentials, EU_WEST_1); + + describe('getAllFlows', () => { + + it('should get all flows in paginated operation', async () => { + const mockMediaConnectClient = mockClient(MediaConnectClient); + + const flows = { + firstPage: { + Flows: [ + {FlowArn: 'FlowArn1'}, + {FlowArn: 'FlowArn2'}, + ], + NextToken: 'flowNextToken' + }, + flowNextToken: { + Flows: [ + {FlowArn: 'FlowArn3'}, + {FlowArn: 'FlowArn4'} + ] + } + }; + + mockMediaConnectClient + .on(ListFlowsCommand) + .callsFake(({NextToken}) => { + if(NextToken != null) return flows[NextToken]; + return flows.firstPage; + }); + + const actual = await getAllFlows(); + + assert.deepEqual(actual, [ + {FlowArn: 'FlowArn1'}, + {FlowArn: 'FlowArn2'}, + {FlowArn: 'FlowArn3'}, + {FlowArn: 'FlowArn4'} + ]); + }); + + }); + + }); + + describe('openSearchClient', () => { + const {getAllOpenSearchDomains} = awsClient.createOpenSearchClient(mockCredentials, EU_WEST_1); + + describe('getAllOpenSearchDomains', () => { + + it('should get OpenSearch domains', async () => { + const mockOpenSearchClient = mockClient(OpenSearchClient); + + const domains = { + 'opensearchdomai-abcdefgh1': { + ARN: 'domainArn1' + }, + 'opensearchdomai-abcdefgh2': { + ARN: 'domainArn2' + }, + 'opensearchdomai-abcdefgh3': { + ARN: 'domainArn3' + }, + 'opensearchdomai-abcdefgh4': { + ARN: 'domainArn4' + }, + 'opensearchdomai-abcdefgh5': { + ARN: 'domainArn5' + }, + 'opensearchdomai-abcdefgh6': { + ARN: 'domainArn6' + }, + 'opensearchdomai-abcdefgh7': { + ARN: 'domainArn7' + }, + 'opensearchdomai-abcdefgh8': { + ARN: 'domainArn8' + }, + 'opensearchdomai-abcdefgh9': { + ARN: 'domainArn9' + } + } + + mockOpenSearchClient + .on(ListDomainNamesCommand, {EngineType: OPENSEARCH}) + .resolves({ + DomainNames: Object.keys(domains).map(DomainName => ({DomainName})) + }); + + mockOpenSearchClient + .on(DescribeDomainsCommand) + .callsFake(({DomainNames}) => { + return { + DomainStatusList: DomainNames.map(domainName => domains[domainName]) + }; + }); + + const actual = await getAllOpenSearchDomains(); + + assert.deepEqual(actual, [ + { + 'ARN': 'domainArn1' + }, + { + 'ARN': 'domainArn2' + }, + { + 'ARN': 'domainArn3' + }, + { + 'ARN': 'domainArn4' + }, + { + 'ARN': 'domainArn5' + }, + { + 'ARN': 'domainArn6' + }, + { + 'ARN': 'domainArn7' + }, + { + 'ARN': 'domainArn8' + }, + { + 'ARN': 'domainArn9' + } + ]); + }); + + }); + }); + + describe('organizationsClient', () => { + const {getAllActiveAccountsFromParent} = awsClient.createOrganizationsClient(mockCredentials, EU_WEST_1); + + describe('getAllActiveAccountsFromParent', () => { + + it('should get all accounts from root OU', async () => { + const sandbox = sinon.createSandbox({ + useFakeTimers: true + }); + const mockOrganizationsClient = mockClient(OrganizationsClient, {sandbox}); + + mockOrganizationsClient + .on(ListRootsCommand) + .resolves({ + Roots: [ + { + Id: 'r-xxxx', + } + ] + }); + + mockOrganizationsClient + .on(DescribeOrganizationCommand) + .resolves({ + Organization: { + MasterAccountId: 'xxxxxxxxxxxx' + } + }); + + mockOrganizationsClient + .on(ListAccountsCommand) + .resolvesOnce({ + Accounts: [ + {Id: 'xxxxxxxxxxxx', Status: 'ACTIVE'}, + {Id: 'yyyyyyyyyyyy', Status: 'ACTIVE'} + ], + NextToken: 'token' + }) + .resolves({ + Accounts: [ + {Id: 'zzzzzzzzzzzz', Status: 'ACTIVE'}, + {Id: 'inactive', Status: 'SUSPENDED'} + ], + }); + + const actualP = getAllActiveAccountsFromParent('r-xxxx'); + + await sandbox.clock.tickAsync(2000); + + assert.deepEqual(await actualP, [ + { + Id: 'xxxxxxxxxxxx', + Status: 'ACTIVE', + isManagementAccount: true + }, + { + Id: 'yyyyyyyyyyyy', + Status: 'ACTIVE' + }, + { + Id: 'zzzzzzzzzzzz', + Status: 'ACTIVE' + } + ]); + + sandbox.clock.restore(); + }); + + it('should get all accounts from non-root OU', async () => { + const sandbox = sinon.createSandbox({ + useFakeTimers: true + }); + const mockOrganizationsClient = mockClient(OrganizationsClient, {sandbox}); + + mockOrganizationsClient + .on(ListRootsCommand) + .resolves({ + Roots: [ + { + Id: 'r-xxxx', + } + ] + }); + + mockOrganizationsClient + .on(DescribeOrganizationCommand) + .resolves({ + Organization: { + MasterAccountId: 'xxxxxxxxxxxx' + } + }); + + const orgUnits = { + 'ou-xxxx-1111111': { + OrganizationalUnits: [ + {Id: 'ou-xxxx-2222222'}, + {Id: 'ou-xxxx-3333333'}, + ], + NextToken: 'ou-xxxx-1111111-token' + }, + 'ou-xxxx-1111111-token': { + OrganizationalUnits: [ + {Id: 'ou-xxxx-4444444'} + ] + }, + 'ou-xxxx-2222222': { + OrganizationalUnits: [ + {Id: 'ou-xxxx-5555555'} + ] + }, + 'ou-xxxx-3333333': { + OrganizationalUnits: [] + }, + 'ou-xxxx-4444444': { + OrganizationalUnits: [] + }, + 'ou-xxxx-5555555': { + OrganizationalUnits: [ + {Id: 'ou-xxxx-6666666'} + ] + }, + 'ou-xxxx-6666666': { + OrganizationalUnits: [] + }, + } + + mockOrganizationsClient + .on(ListOrganizationalUnitsForParentCommand) + .callsFake(({ParentId, NextToken}) => { + if(NextToken != null) return orgUnits[NextToken]; + return orgUnits[ParentId]; + }); + + const orgUnitAccounts = { + 'ou-xxxx-1111111': { + Accounts: [ + {Id: 'aaaaaaaaaaaa', Status: 'ACTIVE'}, + {Id: 'bbbbbbbbbbbb', Status: 'ACTIVE'}, + ], + NextToken: 'ou-xxxx-1111111-token' + }, + 'ou-xxxx-1111111-token': { + Accounts: [ + {Id: 'cccccccccccc', Status: 'ACTIVE'}, + {Id: 'dddddddddddd', Status: 'SUSPENDED'}, + ], + }, + 'ou-xxxx-2222222': { + Accounts: [ + {Id: 'eeeeeeeeeeee', Status: 'ACTIVE'}, + {Id: 'ffffffffffff', Status: 'ACTIVE'}, + ] + }, + 'ou-xxxx-3333333': { + Accounts: [ + {Id: 'gggggggggggg', Status: 'ACTIVE'}, + {Id: 'hhhhhhhhhhhh', Status: 'ACTIVE'}, + ] + }, + 'ou-xxxx-4444444': { + Accounts: [ + {Id: 'iiiiiiiiiiii', Status: 'ACTIVE'} + ] + }, + 'ou-xxxx-5555555': { + Accounts: [ + {Id: 'jjjjjjjjjjjj', Status: 'ACTIVE'}, + {Id: 'kkkkkkkkkkkk', Status: 'SUSPENDED'}, + {Id: 'llllllllllll', Status: 'PENDING_CLOSURE'} + ] + }, + 'ou-xxxx-6666666': { + Accounts: [] + }, + }; + + mockOrganizationsClient + .on(ListAccountsForParentCommand) + .callsFake(({ParentId, NextToken}) => { + if(NextToken != null) return orgUnitAccounts[NextToken]; + if(ParentId != null) return orgUnitAccounts[ParentId]; + }); + + const actualP = getAllActiveAccountsFromParent('ou-xxxx-1111111'); + + await sandbox.clock.tickAsync(30000); + + assert.deepEqual(await actualP, [ + { + 'Id': 'aaaaaaaaaaaa', + 'Status': 'ACTIVE' + }, + { + 'Id': 'bbbbbbbbbbbb', + 'Status': 'ACTIVE' + }, + { + 'Id': 'cccccccccccc', + 'Status': 'ACTIVE' + }, + { + 'Id': 'eeeeeeeeeeee', + 'Status': 'ACTIVE' + }, + { + 'Id': 'ffffffffffff', + 'Status': 'ACTIVE' + }, + { + 'Id': 'gggggggggggg', + 'Status': 'ACTIVE' + }, + { + 'Id': 'hhhhhhhhhhhh', + 'Status': 'ACTIVE' + }, + { + 'Id': 'iiiiiiiiiiii', + 'Status': 'ACTIVE' + }, + { + 'Id': 'jjjjjjjjjjjj', + 'Status': 'ACTIVE' + } + ]); + + sandbox.clock.restore(); + }); + + }); + + }); + + describe('snsClient', () => { + const {getAllSubscriptions} = awsClient.createSnsClient(mockCredentials, EU_WEST_1); + + describe('getAllSubscriptions', () => { + + it('should list all sns subscriptions', async () => { + const mockSnsClient = mockClient(SNSClient); + + const snsSubscription = { + firstPage: { + Subscriptions: [ + {SubscriptionArn: 'SubscriptionArn1'}, + {SubscriptionArn: 'SubscriptionArn2'}, + ], + NextToken: 'listSnsToken' + }, + listSnsToken: { + Subscriptions: [ + {SubscriptionArn: 'SubscriptionArn3'}, + {SubscriptionArn: 'SubscriptionArn4'} + ] + } + } + + mockSnsClient + .on(ListSubscriptionsCommand) + .callsFake(({NextToken}) => { + if(NextToken != null) return snsSubscription[NextToken]; + return snsSubscription.firstPage; + }); + + const actual = await getAllSubscriptions(); + assert.deepEqual(actual, [ + { + SubscriptionArn: 'SubscriptionArn1' + }, + { + SubscriptionArn: 'SubscriptionArn2' + }, + { + SubscriptionArn: 'SubscriptionArn3' + }, + { + SubscriptionArn: 'SubscriptionArn4' + } + ]); + }); + + }); + }); + + describe('stsClient', () => { + const { + getCredentials, + getCurrentCredentials + } = awsClient.createStsClient(mockCredentials, EU_WEST_1); + + describe('getAllAttachedAwsManagedPolices', () => { + + it('should get credentials for role', async () => { + const mockStsClient = mockClient(STSClient); + + const credentials = { + role: { + Credentials: { + AccessKeyId: 'accessKeyId', + SecretAccessKey: 'secretAccessKey', + SessionToken: 'optionalSessionToken' + } + } + }; + + mockStsClient + .on(AssumeRoleCommand) + .callsFake(({RoleArn}) => { + return credentials[RoleArn]; + }); + + const actual = await getCredentials('role'); + + assert.deepEqual(actual, { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + sessionToken: 'optionalSessionToken' + }); + }); + + describe('getCurrentCredentials', () => { + + beforeAll(() => { + process.env.AWS_ACCESS_KEY_ID = 'accessKeyEnv'; + process.env.AWS_SECRET_ACCESS_KEY = 'secretAccessKeyEnv'; + }); + + afterAll(() => { + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + }); + + it('should get credentials for role', async () => { + const actual = await getCurrentCredentials(); + + assert.deepEqual(actual, { + accessKeyId: 'accessKeyEnv', + secretAccessKey: 'secretAccessKeyEnv' + }); + }); + + }); + + }); + }); +}); \ No newline at end of file diff --git a/source/backend/discovery/test/createResourceAndRelationshipDeltas.test.mjs b/source/backend/discovery/test/createResourceAndRelationshipDeltas.test.mjs new file mode 100644 index 00000000..0d0e05eb --- /dev/null +++ b/source/backend/discovery/test/createResourceAndRelationshipDeltas.test.mjs @@ -0,0 +1,340 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import * as R from 'ramda'; +import { + AWS_API_GATEWAY_METHOD, + AWS_EKS_NODE_GROUP, + AWS_API_GATEWAY_RESOURCE, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_EC2_SPOT, + AWS_IAM_AWS_MANAGED_POLICY, + AWS_ECS_TASK, + AWS_EC2_INSTANCE, + AWS_EC2_VPC, + AWS_IAM_ROLE, + CONTAINS, + IS_CONTAINED_IN, + IS_ASSOCIATED_WITH, + VPC, + AWS_LAMBDA_FUNCTION, + AWS_RDS_DB_CLUSTER, + AWS_RDS_DB_INSTANCE, + AWS_SNS_TOPIC, + AWS_SQS_QUEUE +} from '../src/lib/constants.mjs'; +import {generate} from './generator.mjs'; +import createResourceAndRelationshipDeltas from '../src/lib/createResourceAndRelationshipDeltas.mjs'; + +describe('createResourceAndRelationshipDeltas', () => { + + describe('resources', () => { + + it('should calculate sdk discovered resource updates and ignore unchanged resources', async () => { + const {default: {dbResources, resources}} = await import('./fixtures/createResourceAndRelationshipDeltas/resources/sdkResources.json', {with: {type: 'json' }}); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + resourceIdsToDelete, resourcesToStore, resourcesToUpdate + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + assert.lengthOf(resourceIdsToDelete, 0); + assert.lengthOf(resourcesToStore, 0); + + const actualUpdateEksNg = resourcesToUpdate.find(x => x.md5Hash === resources[AWS_EKS_NODE_GROUP].md5Hash); + assert.deepEqual(actualUpdateEksNg, { + id: resources[AWS_EKS_NODE_GROUP].id, + md5Hash: resources[AWS_EKS_NODE_GROUP].md5Hash, + properties: R.omit(['g'], resources[AWS_EKS_NODE_GROUP].properties) + }); + + const actualUpdateTg = resourcesToUpdate.find(x => x.md5Hash === resources[AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP].md5Hash); + assert.deepEqual(actualUpdateTg, { + id: resources[AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP].id, + md5Hash: resources[AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP].md5Hash, + properties: R.omit(['e'], resources[AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP].properties) + }); + + + const actualSpotInstance = resourcesToUpdate.find(x => x.md5Hash === resources[AWS_EC2_SPOT].md5Hash); + assert.deepEqual(actualSpotInstance, { + id: resources[AWS_EC2_SPOT].id, + md5Hash: resources[AWS_EC2_SPOT].md5Hash, + properties: R.omit(['i'], resources[AWS_EC2_SPOT].properties) + }); + + const actualManagedPolicy = resourcesToUpdate.find(x => x.md5Hash === resources[AWS_IAM_AWS_MANAGED_POLICY].md5Hash); + assert.deepEqual(actualManagedPolicy, { + id: resources[AWS_IAM_AWS_MANAGED_POLICY].id, + md5Hash: resources[AWS_IAM_AWS_MANAGED_POLICY].md5Hash, + properties: resources[AWS_IAM_AWS_MANAGED_POLICY].properties + }); + + }); + + it('should calculate resources to store for config discovered resources', async () => { + const {default: {dbResources, resources}} = await import('./fixtures/createResourceAndRelationshipDeltas/resources/storedResources.json', {with: {type: 'json' }}); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + resourcesToStore + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + assert.lengthOf(resourcesToStore, 3); + + const actualStoreInstance = resourcesToStore.find(x => x.id === resources[AWS_EC2_INSTANCE].id); + assert.deepEqual(actualStoreInstance, { + id: resources[AWS_EC2_INSTANCE].id, + md5Hash: resources[AWS_EC2_INSTANCE].md5Hash, + label: AWS_EC2_INSTANCE.replace(/::/g, "_"), + properties: resources[AWS_EC2_INSTANCE].properties + }); + + const actualStoreVpc = resourcesToStore.find(x => x.id === resources[AWS_EC2_VPC].id); + assert.deepEqual(actualStoreVpc, { + id: resources[AWS_EC2_VPC].id, + md5Hash: resources[AWS_EC2_VPC].md5Hash, + label: AWS_EC2_VPC.replace(/::/g, "_"), + properties: resources[AWS_EC2_VPC].properties + }); + + const actualStoreRole = resourcesToStore.find(x => x.id === resources[AWS_IAM_ROLE].id); + assert.deepEqual(actualStoreRole, { + id: resources[AWS_IAM_ROLE].id, + md5Hash: resources[AWS_IAM_ROLE].md5Hash, + label: AWS_IAM_ROLE.replace(/::/g, "_"), + properties: resources[AWS_IAM_ROLE].properties + }); + }); + + it('should calculate deleted resources', async () => { + const {default: {dbResources, resources}} = await import('./fixtures/createResourceAndRelationshipDeltas/resources/deletedResources.json', {with: {type: 'json' }}); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + resourceIdsToDelete + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + assert.lengthOf(resourceIdsToDelete, 3); + + assert.include(resourceIdsToDelete, dbResources[AWS_API_GATEWAY_RESOURCE].id); + assert.include(resourceIdsToDelete, dbResources[AWS_API_GATEWAY_METHOD].id); + assert.include(resourceIdsToDelete, dbResources[AWS_ECS_TASK].id); + }); + + it('should calculate resources from Config to update', async () => { + const {default: {dbResources, resources}} = await import('./fixtures/createResourceAndRelationshipDeltas/resources/configUpdated.json', {with: {type: 'json' }}); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + resourcesToUpdate + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + assert.lengthOf(resourcesToUpdate, 3); + + const actualStoreInstance = resourcesToUpdate.find(x => x.id === resources[AWS_EC2_INSTANCE].id); + assert.deepEqual(actualStoreInstance, { + id: resources[AWS_EC2_INSTANCE].id, + md5Hash: resources[AWS_EC2_INSTANCE].md5Hash, + properties: R.omit(['a'], resources[AWS_EC2_INSTANCE].properties) + }); + + const actualStoreVpc = resourcesToUpdate.find(x => x.id === resources[AWS_EC2_VPC].id); + assert.deepEqual(actualStoreVpc, { + id: resources[AWS_EC2_VPC].id, + md5Hash: resources[AWS_EC2_VPC].md5Hash, + properties: resources[AWS_EC2_VPC].properties + }); + }); + + it('should not calculate updates for tags', async () => { + const {default: {dbResources, resources}} = await import('./fixtures/createResourceAndRelationshipDeltas/resources/tags.json', {with: {type: 'json' }}); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + resourcesToUpdate + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + assert.lengthOf(resourcesToUpdate, 0); + }); + }); + + describe('relationships', () => { + + it('should calculate stored relationships', async () => { + const {default: schema} = await import('./fixtures/createResourceAndRelationshipDeltas/relationships/stored.json', {with: {type: 'json' }}); + const {dbResources, resources} = generate(schema); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + linksToAdd + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + const actualVpcRelationship = linksToAdd.find(x => x.source === resources[AWS_EC2_VPC].id); + assert.deepEqual(actualVpcRelationship, { + source: resources[AWS_EC2_VPC].id, + target: resources[AWS_EC2_INSTANCE].id, + label: CONTAINS.toUpperCase().trim() + }); + + const actualEc2Relationship = linksToAdd.find(x => x.source === resources[AWS_EC2_INSTANCE].id); + assert.deepEqual(actualEc2Relationship, { + source: resources[AWS_EC2_INSTANCE].id, + target: resources[AWS_EC2_VPC].id, + label: (`${IS_CONTAINED_IN}${VPC}`).toUpperCase().replace(/ /g, '_') + }); + + }); + + + it('should calculate cross region relationships', async () => { + const {default: schema} = await import('./fixtures/createResourceAndRelationshipDeltas/relationships/crossRegion.json', {with: {type: 'json' }}); + const {dbResources, resources} = generate(schema); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + linksToAdd + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + const actualSqsRelationship = linksToAdd.find(x => x.source === resources[AWS_SNS_TOPIC].id); + assert.deepEqual(actualSqsRelationship, { + source: resources[AWS_SNS_TOPIC].id, + target: resources[AWS_SQS_QUEUE].id, + label: IS_ASSOCIATED_WITH.toUpperCase().trim().replace(/ /g, '_') + }); + + }); + + it('should skip relationships where target has not been discovered', async () => { + const {default: schema} = await import('./fixtures/createResourceAndRelationshipDeltas/relationships/unknownRelationships.json', {with: {type: 'json' }}); + const {dbResources, resources, eni} = generate(schema); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + linksToAdd + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + assert.lengthOf(linksToAdd, 2); + const unknownRelationship = linksToAdd.find(x => x.target === eni.resourceId); + assert.notExists(unknownRelationship); + }); + + it('should handle links to resources that use resourceName', async () => { + const {default: schema} = await import('./fixtures/createResourceAndRelationshipDeltas/relationships/resourceName.json', {with: {type: 'json' }}); + const {dbResources, resources} = generate(schema); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + linksToAdd + } = createResourceAndRelationshipDeltas(dbResourcesMap, new Map(), Object.values(resources)); + + const actualRoleRelationship = linksToAdd.find(x => x.source === resources[AWS_LAMBDA_FUNCTION].id); + assert.deepEqual(actualRoleRelationship, { + source: resources[AWS_LAMBDA_FUNCTION].id, + target: resources[AWS_IAM_ROLE].id, + label: `${IS_ASSOCIATED_WITH}Role`.toUpperCase().replace(/ /g, '_') + }); + + const actualDbInstanceRelationship = linksToAdd.find(x => x.source === resources[AWS_RDS_DB_CLUSTER].id); + assert.deepEqual(actualDbInstanceRelationship, { + source: resources[AWS_RDS_DB_CLUSTER].id, + target: resources[AWS_RDS_DB_INSTANCE].id, + label: CONTAINS.trim().toUpperCase() + }); + }); + + it('should skip relationships that are already present in db', async () => { + const {default: schema} = await import('./fixtures/createResourceAndRelationshipDeltas/relationships/unchanged.json', {with: {type: 'json' }}); + const {dbResources, dbRelationships, resources} = generate(schema); + + const dbRelationshipsMap = Object.values(dbRelationships).reduce((acc,item) => { + acc.set(`${item.source}_${item.label}_${item.target}`, item); + return acc; + }, new Map()); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + linksToAdd, linksToDelete + } = createResourceAndRelationshipDeltas(dbResourcesMap, dbRelationshipsMap, Object.values(resources)); + + assert.lengthOf(linksToAdd, 0) + assert.lengthOf(linksToDelete, 0); + }); + + it('should handle relationships that have been deleted', async () => { + const {default: schema} = await import('./fixtures/createResourceAndRelationshipDeltas/relationships/deleted.json', {with: {type: 'json' }}); + const {dbResources, dbRelationships, resources} = generate(schema); + + const dbRelationshipsMap = Object.values(dbRelationships).reduce((acc,item) => { + acc.set(`${item.source}_${item.label}_${item.target}`, item); + return acc; + }, new Map()); + + const dbResourcesMap = Object.values(dbResources) + .reduce((acc, item) => { + acc.set(item.id, item); + return acc; + }, new Map()); + + const { + linksToDelete + } = createResourceAndRelationshipDeltas(dbResourcesMap, dbRelationshipsMap, Object.values(resources)); + + assert.include(linksToDelete, dbRelationships[AWS_EC2_INSTANCE].id); + assert.include(linksToDelete, dbRelationships[AWS_EC2_VPC].id);; + }); + + }); + +}); \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/additionalResources/appregistry/application.json b/source/backend/discovery/test/fixtures/additionalResources/appregistry/application.json new file mode 100644 index 00000000..dc26c9d5 --- /dev/null +++ b/source/backend/discovery/test/fixtures/additionalResources/appregistry/application.json @@ -0,0 +1,10 @@ +{ + "euWest2": [{ + "arn": "applicationArn1", + "name": "applicationName1" + }], + "usWest2": [{ + "arn": "applicationArn2", + "name": "applicationName2" + }] +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/additionalResources/appsync/graphQlApi.json b/source/backend/discovery/test/fixtures/additionalResources/appsync/graphQlApi.json new file mode 100644 index 00000000..120c675b --- /dev/null +++ b/source/backend/discovery/test/fixtures/additionalResources/appsync/graphQlApi.json @@ -0,0 +1,35 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2" + }, + "graphQLApi": { + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "arn": "GraphQLApiArn", + "resourceType": "AWS::AppSync::GraphQLApi", + "resourceId": "random-id", + "resourceName": "random-name", + "configuration": { + "Tags": [] + }, + "relationships": [] + }, + "dataSource": { + "dataSourceArn": "DataSourceArn", + "name": "${dataSource.dataSourceArn}" + }, + "mutationResolver": { + "resolverArn": "ResolverArn", + "typeName": "Mutation", + "fieldName": "MutationFieldName", + "dataSourceName": "DataSourceName" + + }, + "queryResolver": { + "resolverArn": "ResolverArn", + "typeName": "Query", + "fieldName": "QueryFieldName", + "dataSourceName": "DataSourceName" + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/additionalResources/mediaconnect/flows.json b/source/backend/discovery/test/fixtures/additionalResources/mediaconnect/flows.json new file mode 100644 index 00000000..7281910b --- /dev/null +++ b/source/backend/discovery/test/fixtures/additionalResources/mediaconnect/flows.json @@ -0,0 +1,12 @@ +{ + "euWest2": [{ + "FlowArn": "flowArn1", + "AvailabilityZone": "eu-west-2a", + "Name": "flowName1" + }], + "usWest2": [{ + "FlowArn": "flowArn2", + "AvailabilityZone": "us-west-2a", + "Name": "flowName2" + }] +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/appregistry/application/default.json b/source/backend/discovery/test/fixtures/relationships/appregistry/application/default.json new file mode 100644 index 00000000..b74431a4 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/appregistry/application/default.json @@ -0,0 +1,48 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "associatedRelationship": "Is associated with " + }, + "application": { + "id": "${application.arn}", + "arn": "myApplicationArn", + "accountId": "${$constants.accountId}", + "resourceType": "AWS::ServiceCatalogAppRegistry::Application", + "awsRegion": "${$constants.region}", + "resourceId": "applicationName", + "configuration": { + "applicationTag": { + "awsApplication": "applicationTag" + } + }, + "relationships": [] + }, + "tag": { + "id": "${tag.arn}", + "arn": "arn:aws:tags::${$constants.accountId}:tag/${tag.resourceName}", + "accountId": "${$constants.accountId}", + "resourceType": "AWS::Tags::Tag", + "awsRegion": "global", + "resourceId": "${tag.arn}", + "resourceName": "awsApplication=applicationTag", + "relationships": [ + { + "resourceId": "ec2InstanceResourceId", + "resourceType": "AWS::EC2::Instance", + "relationshipName": "${$constants.associatedRelationship}" + }, + { + "resourceId": "lambdaResourceId", + "resourceType": "AWS::Lambda::Function", + "relationshipName": "${$constants.associatedRelationship}" + }, + { + "resourceName": "roleName", + "resourceType": "AWS::IAM::Role", + "relationshipName": "${$constants.associatedRelationship}" + } + ], + "configuration": {} + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/appregistry/application/noApplicationTag.json b/source/backend/discovery/test/fixtures/relationships/appregistry/application/noApplicationTag.json new file mode 100644 index 00000000..d445b4af --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/appregistry/application/noApplicationTag.json @@ -0,0 +1,18 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "associatedRelationship": "Is associated with ", + "applicationTag": "applicationTag" + }, + "application": { + "id": "${application.arn}", + "arn": "myApplicationArn", + "accountId": "${$constants.accountId}", + "resourceType": "AWS::ServiceCatalogAppRegistry::Application", + "awsRegion": "${$constants.region}", + "resourceId": "applicationName", + "configuration": {}, + "relationships": [] + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/appsync/graphQlApi.json b/source/backend/discovery/test/fixtures/relationships/appsync/graphQlApi.json new file mode 100644 index 00000000..8e1c9a8e --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/appsync/graphQlApi.json @@ -0,0 +1,143 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2" + }, + "dynamoDBTable": { + "id": "${dynamoDBTable.arn}", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "arn": "arn:aws:dynamodb:${$constants.region}:${$constants.accountId}:table/test", + "resourceId": "${dynamoDBTable.arn}", + "resourceName": "test", + "resourceType": "AWS::DynamoDB::Table", + "relationships": [], + "configuration": {} + }, + "lambda": { + "id": "${lambda.arn}", + "arn": "arn:aws:lambda:${$constants.region}:${$constants.accountId}:function:test-function", + "accountId": "${$constants.accountId}", + "resourceType": "AWS::Lambda::Function", + "relationships": [], + "configuration": { + "deadLetterConfig": { + "targetArn": "dlqArn" + }, + "kmsKeyArn": "kmsKeyArn" + } + }, + "rds":{ + "resourceType": "AWS::RDS::DBCluster", + "resourceId": "cluster-id", + "resourceName": "cluster-name", + "configuration":{}, + "relationships":[] + }, + "eventBus": { + "id": "${eventBus.arn}", + "arn": "arn:aws:events:${$constants.region}:${$constants.accountId}:event-bus/eventBusArn", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "resourceType": "AWS::Events::EventBus", + "relationships": [], + "configuration": {} + }, + "opensearchEndpoint": { + "id": "${opensearchEndpoint.arn}", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "resourceType": "AWS::OpenSearch::Domain", + "resourceId": "opensearchEndpoint", + "arn": "opensearchArn", + "relationships": [], + "configuration": { + "Endpoint": "elasticsearch.domain.aws.com" + } + }, + "elasticsearchEndpoint": { + "id": "${elasticsearchEndpoint.arn}", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "resourceType": "AWS::Elasticsearch::Domain", + "resourceId": "elasticsearchEndpoint", + "arn": "elasticsearchArn", + "relationships": [], + "configuration": { + "Endpoint": "elasticsearch.domain.aws.com" + } + }, + "dynamoLinkedDataSource": { + "dataSourceArn": "DataSourceArn", + "name": "${dynamoLinkedDataSource.dataSourceArn}", + "configuration": { + "dynamodbConfig": { + "tableName": "${dynamoDBTable.resourceName}" + } + }, + "relationships": [], + "resourceType":"AWS::AppSync::DataSource" + }, + "lambdaLinkedDataSource": { + "dataSourceArn": "DataSourceArn", + "name": "${lambdaLinkedDataSource.dataSourceArn}", + "configuration": { + "lambdaConfig": { + "lambdaFunctionArn": "${lambda.arn}" + } + }, + "relationships": [], + "resourceType":"AWS::AppSync::DataSource" + }, + "eventBridgeLinkedDataSource": { + "dataSourceArn": "DataSourceArn", + "name": "${eventBridgeLinkedDataSource.dataSourceArn}", + "configuration": { + "eventBridgeConfig": { + "eventBusArn": "${eventBus.arn}" + } + }, + "relationships": [], + "resourceType":"AWS::AppSync::DataSource" + }, + "rdsLinkedDataSource": { + "dataSourceArn": "DataSourceArn", + "name": "${rdsLinkedDataSource.dataSourceArn}", + "configuration": { + "relationalDatabaseConfig": { + "rdsHttpEndpointConfig": { + "awsRegion":"${$constants.region}", + "dbClusterIdentifier":"${rds.resourceId}", + "databaseName":"${rds.resourceName}" + } + } + }, + "relationships": [], + "resourceType":"AWS::AppSync::DataSource" + }, + "openSearchLinkedDataSource": { + "id":"${openSearchLinkedDataSource.dataSourceArn}", + "dataSourceArn": "DataSourceArn", + "name": "${openSearchLinkedDataSource.dataSourceArn}", + "configuration": { + "openSearchServiceConfig": { + "endpoint": "${opensearchEndpoint.configuration.Endpoint}" + } + }, + "relationships": [], + "resourceType":"AWS::AppSync::DataSource" + }, + "elasticSearchLinkedDataSource": { + "id":"${elasticSearchLinkedDataSource.dataSourceArn}", + "dataSourceArn": "DataSourceArn", + "name": "${elasticSearchLinkedDataSource.dataSourceArn}", + "configuration": { + "elasticsearchConfig": { + "endpoint": "${elasticsearchEndpoint.configuration.Endpoint}" + } + }, + "relationships": [], + "resourceType":"AWS::AppSync::DataSource" + } + } + diff --git a/source/backend/discovery/test/fixtures/relationships/asg/warmPool/configuration.json b/source/backend/discovery/test/fixtures/relationships/asg/warmPool/configuration.json new file mode 100644 index 00000000..b1f0cd19 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/asg/warmPool/configuration.json @@ -0,0 +1,25 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2" + }, + "asg": { + "resourceName": "autoscalingGroupResourceName" + }, + "warmPool": { + "accountId": "${$constants.accountId}", + "arn": "arn:aws:autoscaling:${$constants.awsRegion}:${$constants.accountId}:warmpool/MyAsgWarmPool", + "availabilityZone": "${$constants.awsRegion}a", + "awsRegion": "${$constants.awsRegion}", + "configuration": { + "AutoScalingGroupName": "${asg.resourceName}" + }, + "resourceId": "MyAsgWarmPool", + "resourceName": "MyAsgWarmPool", + "resourceType": "AWS::AutoScaling::WarmPool", + "supplementaryConfiguration": {}, + "version": "1.3", + "relationships": [], + "tags": [] + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/ec2/instance/configuration.json b/source/backend/discovery/test/fixtures/relationships/ec2/instance/configuration.json new file mode 100644 index 00000000..ce48cc6a --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/ec2/instance/configuration.json @@ -0,0 +1,24 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "awsRegion": "eu-west-2" + }, + "instance": { + "accountId": "${$constants.accountId}", + "arn": "arn:aws:ec2:${$constants.awsRegion}:${$constants.accountId}:instance/i-11111111111111111", + "availabilityZone": "${$constants.awsRegion}a", + "awsRegion": "${$constants.awsRegion}", + "configuration": { + "iamInstanceProfile": { + "arn": "arn:aws:iam::${$constants.accountId}:instance-profile/MyInstanceProfile" + } + }, + "resourceId": "MyInstanceProfile", + "resourceName": "MyInstanceProfile", + "resourceType": "AWS::EC2::Instance", + "supplementaryConfiguration": {}, + "version": "1.3", + "relationships": [], + "tags": [] + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/ecs/cluster/configuration.json b/source/backend/discovery/test/fixtures/relationships/ecs/cluster/configuration.json new file mode 100644 index 00000000..1263f416 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/ecs/cluster/configuration.json @@ -0,0 +1,20 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2" + }, + "ecsCluster": { + "id": "${ecsCluster.arn}", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "arn": "arn:aws:ecs:${$constants.region}:${$constants.accountId}:cluster/testCluster", + "resourceType": "AWS::ECS::Cluster", + "resourceId": "testCluster", + "relationships": [], + "configuration": { + "LogConfiguration": { + "S3BucketName": "LogsBucket" + } + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/ecs/taskDefinitions/efs.json b/source/backend/discovery/test/fixtures/relationships/ecs/taskDefinitions/efs.json new file mode 100644 index 00000000..fe586835 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/ecs/taskDefinitions/efs.json @@ -0,0 +1,38 @@ +{ + "$constants": { + "accountId": "${$constants.accountId}", + "region": "eu-west-2" + }, + "efsFs": { + "resourceId": "efsFsResourceId" + }, + "efsAp": { + "resourceId": "efsApResourceId" + }, + "ecsTaskDefinition": { + "id": "${ecsTaskDefinition.arn}", + "resourceId": "ecsTaskDefinitionResourceId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.awsRegion}", + "arn": "ecsTaskDefinitionArn", + "resourceType": "AWS::ECS::TaskDefinition", + "relationships": [], + "configuration": { + "ContainerDefinitions": [], + "Volumes": [ + { + "EfsVolumeConfiguration": { + "FileSystemId": "${efsFs.resourceId}" + } + }, + { + "EfsVolumeConfiguration": { + "AuthorizationConfig": { + "AccessPointId": "${efsAp.resourceId}" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/iam/instanceProfile/mutipleRoles.json b/source/backend/discovery/test/fixtures/relationships/iam/instanceProfile/mutipleRoles.json new file mode 100644 index 00000000..a3bf027b --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/iam/instanceProfile/mutipleRoles.json @@ -0,0 +1,25 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "awsRegion": "eu-west-2" + }, + "profile": { + "accountId": "${$constants.accountId}", + "arn": "arn:aws:iam::${$constants.accountId}:instance-profile/MyInstanceProfile", + "availabilityZone": "Not Applicable", + "awsRegion": "us-east-1", + "configuration": { + "Roles": [ + "roleName1", + "roleName2" + ] + }, + "resourceId": "MyInstanceProfile", + "resourceName": "MyInstanceProfile", + "resourceType": "AWS::IAM::InstanceProfile", + "supplementaryConfiguration": {}, + "version": "1.3", + "relationships": [], + "tags": [] + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/lambda/configuration.json b/source/backend/discovery/test/fixtures/relationships/lambda/configuration.json new file mode 100644 index 00000000..4f0a9cb4 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/lambda/configuration.json @@ -0,0 +1,19 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "awsRegion": "eu-west-2" + }, + "lambda": { + "id": "${lambda.arn}", + "arn": "arn:aws:lambda:${$constants.awsRegion}:${$constants.accountId}:function:test-function", + "accountId": "${$constants.accountId}", + "resourceType": "AWS::Lambda::Function", + "relationships": [], + "configuration": { + "deadLetterConfig": { + "targetArn": "dlqArn" + }, + "kmsKeyArn": "kmsKeyArn" + } + } +} diff --git a/source/backend/discovery/test/fixtures/relationships/mediaconnect/entitlement/flow.json b/source/backend/discovery/test/fixtures/relationships/mediaconnect/entitlement/flow.json new file mode 100644 index 00000000..87a449a3 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediaconnect/entitlement/flow.json @@ -0,0 +1,25 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "entitlement": { + "resourceType": "AWS::MediaConnect::FlowEntitlement" + } + }, + "flow": { + "arn": "flowArn" + }, + "entitlement": { + "id": "${entitlement.arn}", + "arn": "arn:aws:mediaconnect:${$constants.region}:${$constants.accountId}:entitlement:${entitlement.resourceId}", + "resourceId": "entitlementId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "${$constants.region}a", + "resourceType": "${$constants.entitlement.resourceType}", + "relationships": [], + "configuration": { + "FlowArn": "${flow.arn}" + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowVpcInterface/networking.json b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowVpcInterface/networking.json new file mode 100644 index 00000000..fdb6c2e2 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowVpcInterface/networking.json @@ -0,0 +1,59 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "vpcInterface": { + "resourceType": "AWS::MediaConnect::FlowVpcInterface" + }, + "subnet": { + "id1": "subnet-0123456789abcdef", + "resourceType": "AWS::EC2::Subnet", + "relationshipName": "Is contained in " + } + }, + "vpc": { + "resourceId": "vpc-0123456789abcdef0" + }, + "subnet1": { + "id": "${subnet1.arn}", + "arn": "arn:aws:ec2:${$constants.region}:${$constants.accountId}:subnet/${$constants.subnet.id1}", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "${$constants.region}a", + "resourceType": "${$constants.subnet.resourceType}", + "resourceId": "${$constants.subnet.id1}", + "configuration": { + "vpcId": "${vpc.resourceId}" + }, + "relationships": [] + }, + "flow": { + "arn": "flowArn" + }, + "securityGroup": { + "resourceId": "sg-0123456789abcdef0" + }, + "eni": { + "resourceId": "eni-0123456789abcdef0" + }, + "vpcInterface": { + "id": "${vpcInterface.arn}", + "arn": "arn:aws:mediaconnect:${$constants.region}:${$constants.accountId}:flowvpcinterface:${vpcInterface.resourceId}", + "resourceId": "vpcInterfaceId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "${subnet1.availabilityZone}", + "resourceType": "${$constants.vpcInterface.resourceType}", + "relationships": [], + "configuration": { + "SubnetId": "${subnet1.resourceId}", + "SecurityGroupIds": [ + "${securityGroup.resourceId}" + ], + "NetworkInterfaceIds": [ + "${eni.resourceId}" + ], + "FlowArn": "${flow.arn}" + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/encrypted.json b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/encrypted.json new file mode 100644 index 00000000..1ed59487 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/encrypted.json @@ -0,0 +1,35 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "source": { + "resourceType": "AWS::MediaConnect::FlowSource" + } + }, + "flow": { + "arn": "flowArn" + }, + "role": { + "arn": "roleArn" + }, + "secret": { + "arn": "secretArn" + }, + "source": { + "id": "${source.arn}", + "arn": "arn:aws:mediaconnect:${$constants.region}:${$constants.accountId}:source:${source.resourceId}", + "resourceId": "sourceId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "${$constants.region}a", + "resourceType": "${$constants.source.resourceType}", + "relationships": [], + "configuration": { + "FlowArn": "${flow.arn}", + "Decryption": { + "RoleArn": "${role.arn}", + "SecretArn": "${secret.arn}" + } + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/entitlement.json b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/entitlement.json new file mode 100644 index 00000000..5c873f2a --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/entitlement.json @@ -0,0 +1,29 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "source": { + "resourceType": "AWS::MediaConnect::FlowSource" + } + }, + "flow": { + "arn": "flowArn" + }, + "entitlement": { + "arn": "entitlementArn" + }, + "source": { + "id": "${source.arn}", + "arn": "arn:aws:mediaconnect:${$constants.region}:${$constants.accountId}:source:${source.resourceId}", + "resourceId": "sourceId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "${$constants.region}a", + "resourceType": "${$constants.source.resourceType}", + "relationships": [], + "configuration": { + "FlowArn": "${flow.arn}", + "EntitlementArn": "${entitlement.arn}" + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/vpc.json b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/vpc.json new file mode 100644 index 00000000..92c8065f --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediaconnect/flowsource/vpc.json @@ -0,0 +1,32 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "source": { + "resourceType": "AWS::MediaConnect::FlowSource" + } + }, + "vpc": { + "resourceId": "vpc-0123456789abcdef0" + }, + "flow": { + "arn": "flowArn" + }, + "vpcInterface": { + "resourceName": "vpcInterfaceName" + }, + "source": { + "id": "${source.arn}", + "arn": "arn:aws:mediaconnect:${$constants.region}:${$constants.accountId}:source:${source.resourceId}", + "resourceId": "sourceId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "${$constants.region}a", + "resourceType": "${$constants.source.resourceType}", + "relationships": [], + "configuration": { + "VpcInterfaceName": "${vpcInterface.resourceName}", + "FlowArn": "${flow.arn}" + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/encryption.json b/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/encryption.json new file mode 100644 index 00000000..f1343882 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/encryption.json @@ -0,0 +1,107 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "packagingConfiguration": { + "resourceType": "AWS::MediaPackage::PackagingConfiguration" + } + }, + "dashRole": { + "arn": "dashRoleArn" + }, + "cmafRole": { + "arn": "cmafRoleArn" + }, + "hlsRole": { + "arn": "hlsRoleArn" + }, + "hlsRole": { + "arn": "hlsRoleArn" + }, + "mssRole": { + "arn": "mssRoleArn" + }, + "packagingGroup": { + "resourceId": "packagingGroupId" + }, + "packagingConfigurationCmaf": { + "id": "${packagingConfigurationCmaf.arn}", + "arn": "arn:aws:mediapackage:${$constants.region}:${$constants.accountId}:packaging-configurations:${packagingConfigurationCmaf.resourceId}", + "resourceId": "packagingConfigurationCmafId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "Regional", + "resourceType": "${$constants.packagingConfiguration.resourceType}", + "relationships": [], + "configuration": { + "PackagingGroupId": "${packagingGroup.resourceId}", + "CmafPackage": { + "Encryption": { + "SpekeKeyProvider": { + "RoleArn": "${cmafRole.arn}" + } + } + } + } + }, + "packagingConfigurationDash": { + "id": "${packagingConfigurationDash.arn}", + "arn": "arn:aws:mediapackage:${$constants.region}:${$constants.accountId}:packaging-configurations:${packagingConfigurationDash.resourceId}", + "resourceId": "packagingConfigurationDashId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "Regional", + "resourceType": "${$constants.packagingConfiguration.resourceType}", + "relationships": [], + "configuration": { + "PackagingGroupId": "${packagingGroup.resourceId}", + "DashPackage": { + "Encryption": { + "SpekeKeyProvider": { + "RoleArn": "${dashRole.arn}" + } + } + } + } + }, + "packagingConfigurationHls": { + "id": "${packagingConfigurationHls.arn}", + "arn": "arn:aws:mediapackage:${$constants.region}:${$constants.accountId}:packaging-configurations:${packagingConfigurationHls.resourceId}", + "resourceId": "packagingConfigurationHlsId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "Regional", + "resourceType": "${$constants.packagingConfiguration.resourceType}", + "relationships": [], + "configuration": { + "PackagingGroupId": "${packagingGroup.resourceId}", + "HlsPackage": { + "Encryption": { + "SpekeKeyProvider": { + "RoleArn": "${hlsRole.arn}" + } + } + } + } + }, + "packagingConfigurationMss": { + "id": "${packagingConfigurationMss.arn}", + "arn": "arn:aws:mediapackage:${$constants.region}:${$constants.accountId}:packaging-configurations:${packagingConfigurationMss.resourceId}", + "resourceId": "packagingConfigurationMssId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "Regional", + "resourceType": "${$constants.packagingConfiguration.resourceType}", + "relationships": [], + "configuration": { + "PackagingGroupId": "${packagingGroup.resourceId}", + "MssPackage": { + "Encryption": { + "SpekeKeyProvider": { + "RoleArn": "${mssRole.arn}" + } + } + } + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/group.json b/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/group.json new file mode 100644 index 00000000..a02fe9d2 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingConfiguration/group.json @@ -0,0 +1,25 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "packagingConfiguration": { + "resourceType": "AWS::MediaPackage::PackagingConfiguration" + } + }, + "packagingGroup": { + "resourceId": "packagingGroupId" + }, + "packagingConfiguration": { + "id": "${packagingConfiguration.arn}", + "arn": "arn:aws:mediapackage:${$constants.region}:${$constants.accountId}:packaging-configurations:${packagingConfiguration.resourceId}", + "resourceId": "packagingConfigurationId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "Regional", + "resourceType": "${$constants.packagingConfiguration.resourceType}", + "relationships": [], + "configuration": { + "PackagingGroupId": "${packagingGroup.resourceId}" + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingGroup/authorization.json b/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingGroup/authorization.json new file mode 100644 index 00000000..0697dcb1 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/mediapackage/packagingGroup/authorization.json @@ -0,0 +1,31 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "region": "eu-west-2", + "packagingGroup": { + "resourceType": "AWS::MediaPackage::PackagingGroup" + } + }, + "role": { + "arn": "roleArn" + }, + "secret": { + "arn": "secretArn" + }, + "packagingGroup": { + "id": "${packagingGroup.arn}", + "arn": "arn:aws:mediapackage:${$constants.region}:${$constants.accountId}:packaging-groups:${packagingGroup.resourceId}", + "resourceId": "packagingGroupId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.region}", + "availabilityZone": "Regional", + "resourceType": "${$constants.packagingGroup.resourceType}", + "relationships": [], + "configuration": { + "Authorization": { + "CdnIdentifierSecret": "${secret.arn}", + "SecretsRoleArn": "${role.arn}" + } + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/fixtures/relationships/s3/bucket/supplementary.json b/source/backend/discovery/test/fixtures/relationships/s3/bucket/supplementary.json new file mode 100644 index 00000000..355283e3 --- /dev/null +++ b/source/backend/discovery/test/fixtures/relationships/s3/bucket/supplementary.json @@ -0,0 +1,34 @@ +{ + "$constants": { + "accountId": "xxxxxxxxxxxx", + "awsRegion": "eu-west-2" + }, + "s3Bucket": { + "id": "${s3Bucket.arn}", + "arn": "s3BucketArnArn", + "resourceId": "snsLambdaResourceId", + "accountId": "${$constants.accountId}", + "awsRegion": "${$constants.awsRegion}", + "resourceType": "AWS::S3::Bucket", + "relationships": [], + "configuration": {}, + "supplementaryConfiguration": { + "BucketLoggingConfiguration": { + "destinationBucketName": "loggingBucket" + }, + "BucketNotificationConfiguration": { + "configurations": { + "LambdaFunctionConfigurationId": { + "functionARN": "notificationLambdaArn" + }, + "SnsConfigurationId": { + "topicARN": "notificationSnsTopicArn" + }, + "SqsFunctionConfigurationId": { + "queueARN": "notificationSnsQueueArn" + } + } + } + } + } +} \ No newline at end of file diff --git a/source/backend/discovery/test/generator.mjs b/source/backend/discovery/test/generator.mjs new file mode 100644 index 00000000..05d63f29 --- /dev/null +++ b/source/backend/discovery/test/generator.mjs @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; + +const stringInterpolationRegex = /(?<=\$\{)(.*?)(?=\})/g; + +function isObject(val) { + return val !== null && typeof val === 'object' && !Array.isArray(val); +} + +function getRel(schema, rel) { + const [k, ...path] = rel.split('.'); + return R.path(path, schema[k]); +} + +export function generate(schema) { + function interpolate(input) { + if(isObject(input)) { + return Object.entries(input).reduce((acc, [key, val]) => { + acc[key] = interpolate(val); + return acc; + }, {}); + } else if(Array.isArray(input)) { + return input.map(interpolate); + } else { + if(typeof input === 'string') { + const matches = input.match(stringInterpolationRegex); + if(matches != null) { + return matches.reduce((acc, match) => { + return acc.replace('${' + match + '}', getRel(schema, match)); + }, input); + } + } + return input; + } + } + + const interpolated = R.map(interpolate, R.map(interpolate, schema)); + + function generateRec(input) { + if(isObject(input)) { + if(input.$rel != null) { + return getRel(interpolated, input.$rel); + } else { + return Object.entries(input).reduce((acc, [key, val]) => { + acc[key] = generateRec(val); + return acc; + }, {}); + } + } else if(Array.isArray(input)) { + return input.map(generateRec); + } else { + return input; + } + } + + return R.map(generateRec, interpolated); +} + +export function generateBaseResource(accountId, awsRegion, resourceType, num) { + return { + id: 'arn' + num, + resourceId: 'resourceId' + num, + resourceName: 'resourceName' + num, + resourceType, + accountId, + arn: 'arn' + num, + awsRegion, + relationships: [], + tags: [], + configuration: {a: +num} + }; +} + +export function generateRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} diff --git a/source/backend/discovery/test/getAllConfigResources.test.mjs b/source/backend/discovery/test/getAllConfigResources.test.mjs new file mode 100644 index 00000000..4d318ede --- /dev/null +++ b/source/backend/discovery/test/getAllConfigResources.test.mjs @@ -0,0 +1,109 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import sinon from 'sinon'; +import { + AWS_EC2_INSTANCE, + AWS_EKS_CLUSTER, + AWS_IAM_ROLE, + GLOBAL, + AWS_ECS_SERVICE, + AWS_KINESIS_STREAM, + AWS_ECS_TASK_DEFINITION, +} from '../src/lib/constants.mjs'; +import getAllConfigResources from '../src/lib/aggregator/getAllConfigResources.mjs'; + +describe('getAllConfigResources', () => { + + const ACCOUNT_IDX = 'xxxxxxxxxxxx'; + const EU_WEST_1 = 'eu-west-1'; + + const DATE1 = '2014-04-09T01:05:00.000Z'; + const DATE2 = '2011-06-21T18:40:00.000Z'; + + const aggregatorName = 'configAggregator'; + + it('should not remove global resources from discovered accounts', async () => { + const mockConfigClient = { + async getAllAggregatorResources() { + return [ + { + accountId: ACCOUNT_IDX, awsRegion: GLOBAL, resourceType: AWS_IAM_ROLE, + arn: 'roleArn', resourceId: 'roleResourceId', configuration: {}, + configurationItemCaptureTime: DATE1 + }, + { + accountId: ACCOUNT_IDX, awsRegion: EU_WEST_1, resourceType: AWS_EC2_INSTANCE, + arn: 'ec2InstanceArn', resourceId: 'ec2InstanceResourceId', configuration: {}, + configurationItemCaptureTime: DATE2 + } + ] + }, + getAggregatorResources: () => [] + } + + const actual = await getAllConfigResources(mockConfigClient, aggregatorName); + assert.lengthOf(actual, 2); + }); + + it('should normalise resources', async () => { + const mockConfigClient = { + async getAllAggregatorResources() { + return [] + }, + getAggregatorResources: sinon.stub().onFirstCall().resolves([ + { + accountId: ACCOUNT_IDX, awsRegion: EU_WEST_1, resourceType: AWS_ECS_SERVICE, + arn: 'ecsServiceArn', resourceId: 'ecsServiceResourceId', configuration: '{"a": 1}', + configurationItemCaptureTime: new Date(DATE1) + } + ]).resolves([]) + } + + const actual = await getAllConfigResources(mockConfigClient, aggregatorName); + const actualEcsService = actual.find(x => x.arn === 'ecsServiceArn'); + assert.deepEqual(actualEcsService, { + id: "ecsServiceArn", accountId: ACCOUNT_IDX, awsRegion: EU_WEST_1, resourceType: AWS_ECS_SERVICE, + arn: 'ecsServiceArn', resourceId: 'ecsServiceResourceId', configuration: {a: 1}, + configurationItemCaptureTime: DATE1, relationships: [], tags: [] + }); + }); + + + it('should create a unique resourceId for Kinesis streams, EKS Clusters and ECS Task definitions', async () => { + const mockConfigClient = { + async getAllAggregatorResources() { + return [] + }, + getAggregatorResources: sinon.stub().onFirstCall().resolves([ + { + accountId: ACCOUNT_IDX, awsRegion: EU_WEST_1, resourceType: AWS_KINESIS_STREAM, + arn: 'kinesisArn', resourceId: 'kinesisResourceId', configuration: '{"a": 1}', + configurationItemCaptureTime: new Date(DATE1) + }, + { + accountId: ACCOUNT_IDX, awsRegion: EU_WEST_1, resourceType: AWS_EKS_CLUSTER, + arn: 'eksClusterArn', resourceId: 'eksClusterResourceId', configuration: '{"b": 1}', + configurationItemCaptureTime: new Date(DATE2) + }, + { + accountId: ACCOUNT_IDX, awsRegion: EU_WEST_1, resourceType: AWS_ECS_TASK_DEFINITION, + arn: 'ecsTaskDefArn', resourceId: 'ecsTaskDefResourceId', configuration: '{"c": 1}', + configurationItemCaptureTime: new Date(DATE2) + } + ]).resolves([]) + } + + const actual = await getAllConfigResources(mockConfigClient, aggregatorName); + + const actualKinesis = actual.find(x => x.resourceType === AWS_KINESIS_STREAM); + const actualEcsCluster = actual.find(x => x.resourceType === AWS_EKS_CLUSTER); + const actualEcsTaskDef = actual.find(x => x.resourceType === AWS_ECS_TASK_DEFINITION); + + assert.strictEqual(actualKinesis.resourceId, 'kinesisArn'); + assert.strictEqual(actualEcsCluster.resourceId, 'eksClusterArn'); + assert.strictEqual(actualEcsTaskDef.resourceId, 'ecsTaskDefArn'); + }); + +}); diff --git a/source/backend/discovery/test/getAllSdkResources.test.mjs b/source/backend/discovery/test/getAllSdkResources.test.mjs new file mode 100644 index 00000000..5c9ab6cc --- /dev/null +++ b/source/backend/discovery/test/getAllSdkResources.test.mjs @@ -0,0 +1,1932 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import { + AWS, + AWS_IAM_AWS_MANAGED_POLICY, + RESOURCE_DISCOVERED, + NOT_APPLICABLE, + GLOBAL, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + MULTIPLE_AVAILABILITY_ZONES, + AWS_EC2_SPOT_FLEET, + IS_ASSOCIATED_WITH, + AWS_EC2_INSTANCE, + AWS_EC2_SPOT, + AWS_API_GATEWAY_RESOURCE, + IS_CONTAINED_IN, + AWS_API_GATEWAY_REST_API, + AWS_API_GATEWAY_AUTHORIZER, + AWS_DYNAMODB_STREAM, + AWS_DYNAMODB_TABLE, + AWS_ECS_TASK, AWS_ECS_SERVICE, + AWS_EKS_NODE_GROUP, + AWS_EKS_CLUSTER, + AWS_IAM_INLINE_POLICY, + AWS_IAM_ROLE, + AWS_IAM_USER, + AWS_API_GATEWAY_METHOD, + GET, + POST, + NOT_FOUND_EXCEPTION, + AWS_SQS_QUEUE, + AWS_TAGS_TAG, + AWS_OPENSEARCH_DOMAIN, + AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + AWS_APPSYNC_GRAPHQLAPI, + AWS_APPSYNC_DATASOURCE, + AWS_APPSYNC_RESOLVER, AWS_MEDIA_CONNECT_FLOW +} from '../src/lib/constants.mjs'; +import * as sdkResources from '../src/lib/sdkResources/index.mjs'; +import {generate} from "./generator.mjs"; + +const EU_WEST_2 = 'eu-west-2'; +const EU_WEST_2_A = EU_WEST_2 + 'a'; +const US_WEST_2 = 'us-west-2'; + +const ACCESS_KEY_X = 'accessKeyIdX'; +const ACCESS_KEY_Z = 'accessKeyIdz'; + +const ACCOUNT_X = 'xxxxxxxxxxxx'; +const ACCOUNT_Z = 'zzzzzzzzzzzz'; + +describe('getAllSdkResources', () => { + + const credentialsX = {accessKeyId: ACCESS_KEY_X, secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken'}; + const credentialsZ = {accessKeyId: ACCESS_KEY_Z, secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken'}; + + const mockAwsClient = { + createAppSyncClient(){ + return { + listDataSources: async ()=> [], + listResolvers: async ()=> [], + } + }, + createIamClient() { + return { + getAllAttachedAwsManagedPolices: async () => [], + } + }, + createElbV2Client() { + return { + describeTargetHealth: async arn => [], + getAllTargetGroups: async arn => [] + } + }, + createEc2Client() { + return { + getAllSpotInstanceRequests: async () => [], + getAllSpotFleetRequests: async () => [] + } + }, + createEcsClient() { + return { + getAllClusterInstances: async arn => [], + getAllServiceTasks: async () => [] + } + }, + createEksClient() { + return { + listNodeGroups: async arn => [] + } + }, + createApiGatewayClient(accountId, credentials, region) { + return { + getResources: async () => [], + getAuthorizers: async () => [] + } + }, + createDynamoDBStreamsClient(credentials, region) { + return { + describeStream: async (streamArn) => [], + } + }, + createMediaConnectClient(credentials, region) { + return { + getAllFlows: async (streamArn) => [], + } + }, + createOpenSearchClient(credentials, region) { + return { + getAllOpenSearchDomains: async (streamArn) => [] + } + }, + createServiceCatalogAppRegistryClient(credentials, region) { + return { + getAllApplications: async (streamArn) => [] + } + } + }; + + describe('getAdditionalResources', () => { + + const getAllSdkResources = sdkResources.getAllSdkResources(new Map( + [[ + ACCOUNT_X, + { + credentials: credentialsX, + regions: [ + 'eu-west-2' + ] + } + ], [ + ACCOUNT_Z, + { + credentials: credentialsZ, + regions: [ + 'us-west-2' + ] + } + ]] + )); + + describe(AWS_IAM_AWS_MANAGED_POLICY, () => { + + it('should discover AWS managed policy resources', async () => { + const {default: {euWest2, usWest2}} = await import('./fixtures/additionalResources/iam/awsManagedPolicy.json', {with: {type: 'json' }}); + + const mockIamClient = { + createIamClient(credentials, region) { + return { + async getAllAttachedAwsManagedPolices() { + if(credentials.accessKeyId === ACCESS_KEY_X) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z) { + return usWest2; + } + } + }; + } + } + + const arn1 = 'managedPolicyArn1' + const arn2 = 'managedPolicyArn2' + + const actual = await getAllSdkResources({...mockAwsClient, ...mockIamClient}, []); + + const actualRole1 = actual.find(x => x.arn === arn1); + const actualRole2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualRole1, { + id: arn1, + accountId: AWS.toLowerCase(), + arn: arn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: { + Arn: arn1, + PolicyName: 'policyName1' + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: 'policyName1', + resourceType: AWS_IAM_AWS_MANAGED_POLICY, + tags: [], + relationships: [] + }); + + assert.deepEqual(actualRole2, { + id: arn2, + accountId: AWS.toLowerCase(), + arn: arn2, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: { + Arn: arn2, + PolicyName: 'policyName2' + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: 'policyName2', + resourceType: AWS_IAM_AWS_MANAGED_POLICY, + tags: [], + relationships: [] + }); + }); + + it('should discover AWS managed policy resources when some regions fail', async () => { + const {default: {euWest2}} = await import('./fixtures/additionalResources/iam/awsManagedPolicy.json', {with: {type: 'json' }}); + + const mockIamClient = { + createIamClient(credentials, region) { + return { + async getAllAttachedAwsManagedPolices() { + if(credentials.accessKeyId === ACCESS_KEY_X) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z) { + throw new Error(); + } + } + }; + } + } + + const arn1 = 'managedPolicyArn1' + + const actual = await getAllSdkResources({...mockAwsClient, ...mockIamClient}, []); + + const actualRole1 = actual.find(x => x.arn === arn1); + + assert.strictEqual(actual.length, 1); + assert.deepEqual(actualRole1, { + id: arn1, + accountId: AWS.toLowerCase(), + arn: arn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: { + Arn: arn1, + PolicyName: 'policyName1' + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: 'policyName1', + resourceType: AWS_IAM_AWS_MANAGED_POLICY, + tags: [], + relationships: [] + }); + + }); + + }); + + describe(AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, () => { + + it('should discover ALB target groups', async () => { + const {default: {euWest2, usWest2}} = await import('./fixtures//additionalResources/alb/targetGroups.json', {with: {type: 'json' }}); + + const mockElbV2Client = { + createElbV2Client(credentials, region) { + return { + describeTargetHealth: async arn => [], + async getAllTargetGroups() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + return usWest2; + } + } + } + } + } + + const arn1 = 'targetGroupArn1'; + const arn2 = 'targetGroupArn2'; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockElbV2Client}, []); + + const actualTg1 = actual.find(x => x.arn === arn1); + const actualTg2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualTg1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + TargetGroupArn: arn1 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + tags: [], + relationships: [] + }); + + assert.deepEqual(actualTg2, { + id: arn2, + accountId: ACCOUNT_Z, + arn: arn2, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: US_WEST_2, + configuration: { + TargetGroupArn: arn2 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: arn2, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + tags: [], + relationships: [] + }); + + }); + + it('should discover ALB target groups when some regions fail', async () => { + const {default: {euWest2}} = await import('./fixtures//additionalResources/alb/targetGroups.json', {with: {type: 'json' }}); + + const mockElbV2Client = { + createElbV2Client(credentials, region) { + return { + describeTargetHealth: async arn => [], + async getAllTargetGroups() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + throw new Error(); + } + } + } + } + } + + const arn1 = 'targetGroupArn1'; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockElbV2Client}, []); + + const actualTg1 = actual.find(x => x.arn === arn1); + + assert.strictEqual(actual.length, 1); + assert.deepEqual(actualTg1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + TargetGroupArn: arn1 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + tags: [], + relationships: [] + }); + + }); + + }); + + describe(AWS_EC2_SPOT, () => { + + it('should discover spot instances', async () => { + const {default: {instanceRequests}} = await import('./fixtures//additionalResources/spot/instance.json', {with: {type: 'json' }}); + + const mockEc2Client = { + createEc2Client(credentials, region) { + return { + async getAllSpotInstanceRequests() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return instanceRequests.euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + return instanceRequests.usWest2; + } + }, + async getAllSpotFleetRequests() { + return []; + } + } + } + } + + const spotInstanceRequestId1 = 'spotInstanceRequestId1'; + const spotInstanceRequestId2 = 'spotInstanceRequestId2'; + + const arn1 = `arn:aws:ec2:${EU_WEST_2}:${ACCOUNT_X}:spot-instance-request/${spotInstanceRequestId1}`; + const arn2 = `arn:aws:ec2:${US_WEST_2}:${ACCOUNT_Z}:spot-instance-request/${spotInstanceRequestId2}`; + + const instanceId1 = "instanceId1"; + const instanceId2 = "instanceId2"; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEc2Client}, []); + + const actualSpotFleet1 = actual.find(x => x.arn === arn1); + const actualSpotFleet2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualSpotFleet1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + InstanceId: instanceId1, + SpotInstanceRequestId: spotInstanceRequestId1, + Tags: [] + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_EC2_SPOT, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: instanceId1, + resourceType: AWS_EC2_INSTANCE + } + ] + }); + + assert.deepEqual(actualSpotFleet2, { + id: arn2, + accountId: ACCOUNT_Z, + arn: arn2, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: US_WEST_2, + configuration: { + InstanceId: instanceId2, + SpotInstanceRequestId: spotInstanceRequestId2, + Tags: [] + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: arn2, + resourceType: AWS_EC2_SPOT, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: instanceId2, + resourceType: AWS_EC2_INSTANCE + } + ] + }); + + }); + + it('should discover spot instances when some regions fail', async () => { + const {default: {instanceRequests}} = await import('./fixtures//additionalResources/spot/instance.json', {with: {type: 'json' }}); + + const mockEc2lient = { + createEc2Client(credentials, region) { + return { + async getAllSpotInstanceRequests() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return instanceRequests.euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + throw new Error(); + } + }, + async getAllSpotFleetRequests() { + return []; + } + } + } + } + + const spotInstanceRequestId1 = 'spotInstanceRequestId1'; + + const arn1 = `arn:aws:ec2:${EU_WEST_2}:${ACCOUNT_X}:spot-instance-request/${spotInstanceRequestId1}`; + + const instanceId1 = "instanceId1"; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEc2lient}, []); + + const actualSpotFleet1 = actual.find(x => x.arn === arn1); + + assert.strictEqual(actual.length, 1); + assert.deepEqual(actualSpotFleet1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + InstanceId: instanceId1, + SpotInstanceRequestId: spotInstanceRequestId1, + Tags: [] + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_EC2_SPOT, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: instanceId1, + resourceType: AWS_EC2_INSTANCE + } + ] + }); + }); + + }); + + describe(AWS_EC2_SPOT_FLEET, () => { + + it('should discover spot fleets', async () => { + const {default: {fleetRequests, instanceRequests}} = await import('./fixtures//additionalResources/spot/fleet.json', {with: {type: 'json' }}); + + const mockEc2lient = { + createEc2Client(credentials, region) { + return { + async getAllSpotInstanceRequests() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return instanceRequests.euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + return instanceRequests.usWest2; + } + }, + async getAllSpotFleetRequests() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return fleetRequests.euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + return fleetRequests.usWest2; + } + } + } + } + } + + const arn1 = `arn:aws:ec2:${EU_WEST_2}:${ACCOUNT_X}:spot-fleet-request/spotFleetRequestId1`; + const arn2 = `arn:aws:ec2:${US_WEST_2}:${ACCOUNT_Z}:spot-fleet-request/spotFleetRequestId2`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEc2lient}, []); + + const actualSpotFleet1 = actual.find(x => x.arn === arn1); + const actualSpotFleet2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualSpotFleet1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + SpotFleetRequestId: 'spotFleetRequestId1', + SpotFleetRequestConfig: { + OnDemandFulfilledCapacity: 0 + } + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_EC2_SPOT_FLEET, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: 'instanceId1', + resourceType: AWS_EC2_INSTANCE + } + ] + }); + + assert.deepEqual(actualSpotFleet2, { + id: arn2, + accountId: ACCOUNT_Z, + arn: arn2, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: US_WEST_2, + configuration: { + SpotFleetRequestId: 'spotFleetRequestId2', + SpotFleetRequestConfig: { + OnDemandFulfilledCapacity: 1 + } + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: arn2, + resourceType: AWS_EC2_SPOT_FLEET, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: 'instanceId2', + resourceType: AWS_EC2_INSTANCE + } + ] + }); + + }); + + it('should discover spot fleets if some regions fail', async () => { + const {default: {fleetRequests, instanceRequests}} = await import('./fixtures//additionalResources/spot/fleet.json', {with: {type: 'json' }}); + + const mockEc2lient = { + createEc2Client(credentials, region) { + return { + async getAllSpotInstanceRequests() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return instanceRequests.euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + throw new Error(); + } + }, + async getAllSpotFleetRequests() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return fleetRequests.euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + return fleetRequests.usWest2; + } + } + } + } + } + + const arn1 = `arn:aws:ec2:${EU_WEST_2}:${ACCOUNT_X}:spot-fleet-request/spotFleetRequestId1`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEc2lient}, []); + + const actualSpotFleet1 = actual.find(x => x.arn === arn1); + + assert.strictEqual(actual.length, 1); + assert.deepEqual(actualSpotFleet1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + SpotFleetRequestId: 'spotFleetRequestId1', + SpotFleetRequestConfig: { + OnDemandFulfilledCapacity: 0 + } + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_EC2_SPOT_FLEET, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: 'instanceId1', + resourceType: AWS_EC2_INSTANCE + } + ] + }); + }); + + }); + + describe(AWS_API_GATEWAY_RESOURCE, () => { + + it('should discover API Gateway resources', async () => { + const {default: schema} = await import('./fixtures//additionalResources/apigateway/resources.json', {with: {type: 'json' }}); + const {restApi, apiGwResource} = generate(schema); + + const mockApiGatewayClient = { + createApiGatewayClient(credentials, region) { + return { + getAuthorizers: async restApi => [], + async getResources() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [apiGwResource]; + } + }, + async getMethod() { + const notFoundError = new Error(); + notFoundError.name = NOT_FOUND_EXCEPTION; + throw notFoundError; + } + } + } + } + + const arn = `arn:aws:apigateway:${EU_WEST_2}::/restapis/${restApi.configuration.id}/resources/${apiGwResource.id}`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockApiGatewayClient}, [restApi]); + + const actualApiGwResource = actual.find(x => x.arn === arn); + + assert.deepEqual(actualApiGwResource, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + RestApiId: restApi.configuration.id, + id: apiGwResource.id + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: arn, + resourceType: AWS_API_GATEWAY_RESOURCE, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: restApi.configuration.id, + resourceType: AWS_API_GATEWAY_REST_API + } + ] + }); + + }); + + }); + + describe(AWS_API_GATEWAY_METHOD, () => { + + it('should handle resources that have unsupported http verbs', async () => { + const {default: schema} = await import('./fixtures//additionalResources/apigateway/method.json', {with: {type: 'json' }}); + const {restApi, apiGwResource, getMethod, postMethod} = generate(schema); + + const mockApiGatewayClient = { + createApiGatewayClient(accountId, credentials, region) { + return { + getResources: async restApi => { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [apiGwResource]; + } + }, + getAuthorizers: async restApi => [], + async getMethod(httpMethod) { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + const notFoundError = new Error(); + notFoundError.name = NOT_FOUND_EXCEPTION; + throw notFoundError; + } + } + } + } + } + + const actual = await getAllSdkResources({...mockAwsClient, ...mockApiGatewayClient}, [restApi]); + + assert.deepEqual(actual.filter(x => x.resourceType === AWS_API_GATEWAY_METHOD), []); + }); + + it('should discover API Gateway methods', async () => { + const {default: schema} = await import('./fixtures//additionalResources/apigateway/method.json', {with: {type: 'json' }}); + const {restApi, apiGwResource, getMethod, postMethod} = generate(schema); + + const mockApiGatewayClient = { + createApiGatewayClient(credentials, region) { + return { + getResources: async restApi => { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [apiGwResource]; + } + }, + getAuthorizers: async restApi => [], + async getMethod(httpMethod) { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + if(httpMethod === GET) { + return getMethod; + } else if(httpMethod === POST) { + return postMethod; + } else { + const notFoundError = new Error(); + notFoundError.name = 'NotFoundException'; + throw notFoundError; + } + } + } + } + } + } + + const apiGatewayResourceArn = `arn:aws:apigateway:${EU_WEST_2}::/restapis/${restApi.configuration.id}/resources/${apiGwResource.id}`; + const arn1 = `arn:aws:apigateway:${EU_WEST_2}::/restapis/${restApi.configuration.id}/resources/${apiGwResource.id}/methods/${GET}`; + const arn2 = `arn:aws:apigateway:${EU_WEST_2}::/restapis/${restApi.configuration.id}/resources/${apiGwResource.id}/methods/${POST}`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockApiGatewayClient}, [restApi]); + + const actualGetMethod = actual.find(x => x.arn === arn1); + const actualPostMethod = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualGetMethod, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + RestApiId: restApi.configuration.id, + ResourceId: apiGwResource.id, + httpMethod: GET + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_API_GATEWAY_METHOD, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: apiGatewayResourceArn, + resourceType: AWS_API_GATEWAY_RESOURCE + } + ] + }); + + assert.deepEqual(actualPostMethod, { + id: arn2, + accountId: ACCOUNT_X, + arn: arn2, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + RestApiId: restApi.configuration.id, + ResourceId: apiGwResource.id, + httpMethod: POST + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: arn2, + resourceType: AWS_API_GATEWAY_METHOD, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: apiGatewayResourceArn, + resourceType: AWS_API_GATEWAY_RESOURCE + } + ] + }); + + }); + + }); + + describe(AWS_API_GATEWAY_AUTHORIZER, () => { + + it('should discover API Gateway authorizers with no providers', async () => { + const {default: schema} = await import('./fixtures/additionalResources/apigateway/authorizerNoProvider.json', {with: { type: 'json' }}); + const {restApi, apiGwAuthorizer} = generate(schema); + + const mockApiGatewayClient = { + createApiGatewayClient(credentials, region) { + return { + getResources: async restApi => [], + async getAuthorizers(restApi) { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [apiGwAuthorizer]; + } + } + } + } + } + + const arn = `arn:aws:apigateway:${EU_WEST_2}::/restapis/${restApi.configuration.id}/authorizers/${apiGwAuthorizer.id}`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockApiGatewayClient}, [restApi]); + + const actualApiGwResource = actual.find(x => x.arn === arn); + + assert.deepEqual(actualApiGwResource, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + RestApiId: restApi.configuration.id, + id: apiGwAuthorizer.id + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: arn, + resourceType: AWS_API_GATEWAY_AUTHORIZER, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: restApi.configuration.id, + resourceType: AWS_API_GATEWAY_REST_API + } + ] + }); + + }); + + it('should discover API Gateway Cognito authorizers', async () => { + const {default: schema} = await import('./fixtures/additionalResources/apigateway/authorizer.json', {with: {type: 'json' }}); + const {restApi, cognito, apiGwAuthorizer} = generate(schema); + + const mockApiGatewayClient = { + createApiGatewayClient(credentials, region) { + return { + getResources: async restApi => [], + async getAuthorizers(restApi) { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [apiGwAuthorizer]; + } + } + } + } + } + + const arn = `arn:aws:apigateway:${EU_WEST_2}::/restapis/${restApi.configuration.id}/authorizers/${apiGwAuthorizer.id}`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockApiGatewayClient}, [restApi]); + + const actualApiGwResource = actual.find(x => x.arn === arn); + + assert.deepEqual(actualApiGwResource, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + RestApiId: restApi.configuration.id, + id: apiGwAuthorizer.id, + providerARNs: [ + 'cognitoArn' + ] + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: arn, + resourceType: AWS_API_GATEWAY_AUTHORIZER, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: restApi.configuration.id, + resourceType: AWS_API_GATEWAY_REST_API + }, + { + relationshipName: IS_ASSOCIATED_WITH, + arn: cognito.arn + } + ] + }); + + }); + + }); + + describe(AWS_ECS_TASK, () => { + + it('should discover ECS tasks', async () => { + const {default: schema} = await import('./fixtures/additionalResources/ecs/task.json', {with: {type: 'json' }}); + const {ecsService, ecsTask} = generate(schema); + + const mockEcsClientClient = { + createEcsClient(credentials, region) { + return { + async getAllServiceTasks() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [ecsTask]; + } + } + } + }, + } + + const arn = ecsTask.taskArn; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEcsClientClient}, [ecsService]); + + const actualEcsTask = actual.find(x => x.arn === arn); + + assert.deepEqual(actualEcsTask, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: EU_WEST_2_A , + awsRegion: EU_WEST_2, + configuration: { + availabilityZone: EU_WEST_2_A , + taskArn: arn + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: arn, + resourceType: AWS_ECS_TASK, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: ecsService.resourceId, + resourceType: AWS_ECS_SERVICE + } + ] + }); + + }); + + }); + + describe(AWS_EKS_NODE_GROUP, () => { + + it('should discover EKS node groups', async () => { + const {default: schema} = await import('./fixtures/additionalResources/eks/nodeGroup.json', {with: {type: 'json' }}); + const {eksCluster, nodeGroup} = generate(schema); + + const mockEksClientClient = { + createEksClient(credentials, region) { + return { + async listNodeGroups() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [nodeGroup]; + } + } + } + } + } + + const arn = nodeGroup.nodegroupArn; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEksClientClient}, [eksCluster]); + + const actualEksNodeGroup = actual.find(x => x.arn === arn); + + assert.deepEqual(actualEksNodeGroup, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + nodegroupArn: nodeGroup.nodegroupArn, + nodegroupName: nodeGroup.nodegroupName + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: nodeGroup.nodegroupName, + resourceType: AWS_EKS_NODE_GROUP, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: eksCluster.resourceId, + resourceType: AWS_EKS_CLUSTER + } + ] + }); + + }); + + }); + + describe(AWS_MEDIA_CONNECT_FLOW, () => { + + it('should discover Media Connect flows', async () => { + const {default: {euWest2, usWest2}} = await import('./fixtures/additionalResources/mediaconnect/flows.json', {with: {type: 'json' }}); + + const mockMediaConnectClient = { + createMediaConnectClient(credentials, region) { + return { + getAllFlows: async () => { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + return usWest2; + } + } + } + } + } + + const arn1 = 'flowArn1'; + const name1 = 'flowName1'; + const arn2 = 'flowArn2'; + const name2 = 'flowName2'; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockMediaConnectClient}, []); + + const actualFlow1 = actual.find(x => x.arn === arn1); + const actualFlow2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualFlow1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: EU_WEST_2 + 'a', + awsRegion: EU_WEST_2, + configuration: { + FlowArn: arn1, + AvailabilityZone: EU_WEST_2 + 'a', + Name: name1 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: name1, + resourceType: AWS_MEDIA_CONNECT_FLOW, + tags: [], + relationships: [] + }); + + assert.deepEqual(actualFlow2, { + id: arn2, + accountId: ACCOUNT_Z, + arn: arn2, + availabilityZone: US_WEST_2 + 'a', + awsRegion: US_WEST_2, + configuration: { + FlowArn: arn2, + AvailabilityZone: US_WEST_2 + 'a', + Name: name2 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: name2, + resourceType: AWS_MEDIA_CONNECT_FLOW, + tags: [], + relationships: [] + }); + + }); + + it('should discover Media Connect flows some regions fail', async () => { + const {default: {euWest2}} = await import('./fixtures/additionalResources/mediaconnect/flows.json', {with: {type: 'json' }}); + + const mockMediaConnectClient = { + createMediaConnectClient(credentials, region) { + return { + getAllFlows: async () => { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + throw new Error(); + } + } + } + } + } + + const arn1 = 'flowArn1'; + const name1 = 'flowName1'; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockMediaConnectClient}, []); + + const actualPool1 = actual.find(x => x.arn === arn1); + + assert.deepEqual(actualPool1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: EU_WEST_2 + 'a', + awsRegion: EU_WEST_2, + configuration: { + FlowArn: arn1, + AvailabilityZone: EU_WEST_2 + 'a', + Name: name1 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: name1, + resourceType: AWS_MEDIA_CONNECT_FLOW, + tags: [], + relationships: [] + }); + + }); + + }); + + describe(AWS_OPENSEARCH_DOMAIN, () => { + + it('should discover OpenSearch domains', async () => { + const {default: schema} = await import('./fixtures/additionalResources/opensearch/domain.json', {with: {type: 'json' }}); + const {domain} = generate(schema); + + const mockOpenSearchClientClient = { + createOpenSearchClient(credentials, region) { + return { + async getAllOpenSearchDomains() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [domain]; + } + } + } + } + } + + const arn = domain.ARN; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockOpenSearchClientClient}, []); + + const actualDomain = actual.find(x => x.arn === arn); + + assert.deepEqual(actualDomain, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + ARN: domain.ARN, + DomainName: domain.DomainName + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: domain.DomainName, + resourceName: domain.DomainName, + resourceType: AWS_OPENSEARCH_DOMAIN, + tags: [], + relationships: [] + }); + + }); + + }); + + describe(AWS_IAM_INLINE_POLICY, () => { + + it('should create inline iam policy from iam role', async () => { + const {default: schema} = await import('./fixtures/additionalResources/iam/inlinePolicy/role.json', {with: {type: 'json' }}); + const {inlinePolicy1, inlinePolicy2, role} = generate(schema); + + const actual = await getAllSdkResources(mockAwsClient, [role]); + + const arn1 = `${role.arn}/inlinePolicy/${inlinePolicy1.policyName}`; + const arn2 = `${role.arn}/inlinePolicy/${inlinePolicy2.policyName}`; + + const actualInlinePolcy1 = actual.find(x => x.arn === arn1); + const actualInlinePolcy2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualInlinePolcy1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: { + ...inlinePolicy1 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_IAM_INLINE_POLICY, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceName: role.resourceName, + resourceType: AWS_IAM_ROLE + } + ] + }); + + assert.deepEqual(actualInlinePolcy2, { + id: arn2, + accountId: ACCOUNT_X, + arn: arn2, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: { + ...inlinePolicy2 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: arn2, + resourceType: AWS_IAM_INLINE_POLICY, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceName: role.resourceName, + resourceType: AWS_IAM_ROLE + } + ] + }); + + }); + + it('should create inline iam policy from iam uder', async () => { + const {default: schema} = await import('./fixtures/additionalResources/iam/inlinePolicy/user.json', {with: {type: 'json' }}); + const {inlinePolicy, user} = generate(schema); + + const actual = await getAllSdkResources(mockAwsClient, [user]); + + const arn = `${user.arn}/inlinePolicy/${inlinePolicy.policyName}`; + + const actualInlinePolcy = actual.find(x => x.arn === arn); + + assert.deepEqual(actualInlinePolcy, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: { + ...inlinePolicy + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: arn, + resourceType: AWS_IAM_INLINE_POLICY, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceName: user.resourceName, + resourceType: AWS_IAM_USER + } + ] + }); + }); + + }); + + describe(AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, () => { + + it('should discover App Registry applications', async () => { + const {default: {euWest2, usWest2}} = await import('./fixtures/additionalResources/appregistry/application.json', {with: {type: 'json' }}); + + const mockAppRegistryClient = { + createServiceCatalogAppRegistryClient(credentials, region) { + return { + getAllApplications: async () => { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + return usWest2; + } + } + } + } + } + + const arn1 = 'applicationArn1' + const arn2 = 'applicationArn2' + + const name1 = 'applicationName1' + const name2 = 'applicationName2' + + const actual = await getAllSdkResources({...mockAwsClient, ...mockAppRegistryClient}, []); + + const actualApplication1 = actual.find(x => x.arn === arn1); + const actualApplication2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualApplication1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + arn: arn1, + name: name1 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: name1, + resourceType: AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + tags: [], + relationships: [] + }); + + assert.deepEqual(actualApplication2, { + id: arn2, + accountId: ACCOUNT_Z, + arn: arn2, + availabilityZone: NOT_APPLICABLE, + awsRegion: US_WEST_2, + configuration: { + arn: arn2, + name: name2 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: name2, + resourceType: AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + tags: [], + relationships: [] + }); + + }); + + it('should discover App Registry applications even when some regions fail', async () => { + const {default: {euWest2}} = await import('./fixtures/additionalResources/appregistry/application.json', {with: {type: 'json' }}); + + const mockAppRegistryClient = { + createServiceCatalogAppRegistryClient(credentials, region) { + return { + getAllApplications: async () => { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return euWest2; + } else if(credentials.accessKeyId === ACCESS_KEY_Z && region === US_WEST_2) { + throw new Error(); + } + } + } + } + } + + const arn1 = 'applicationArn1' + const name1 = 'applicationName1' + + const actual = await getAllSdkResources({...mockAwsClient, ...mockAppRegistryClient}, []); + + assert.strictEqual(actual.length, 1); + + const actualApplication1 = actual.find(x => x.arn === arn1); + + assert.deepEqual(actualApplication1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + arn: arn1, + name: name1 + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: name1, + resourceType: AWS_SERVICE_CATALOG_APP_REGISTRY_APPLICATION, + tags: [], + relationships: [] + }); + + }); + + }); + + describe(AWS_TAGS_TAG, () => { + + it('should create tags from resources', async () => { + const {default: schema} = await import('./fixtures/additionalResources/tags/tag.json', {with: {type: 'json' }}); + const {tagInfo, ec2Instance, sqsQueue, forecast} = generate(schema); + + const actual = await getAllSdkResources(mockAwsClient, [ec2Instance, sqsQueue, forecast]); + + const arn1 = `arn:aws:tags::${ACCOUNT_X}:tag/${tagInfo.applicationName}=${tagInfo.applicationValue}`; + const arn2 = `arn:aws:tags::${ACCOUNT_X}:tag/${tagInfo.sqsName}=${tagInfo.sqsValue}`; + + const actualTag1 = actual.find(x => x.arn === arn1); + const actualTag2 = actual.find(x => x.arn === arn2); + + assert.deepEqual(actualTag1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: {}, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: `${tagInfo.applicationName}=${tagInfo.applicationValue}`, + resourceType: AWS_TAGS_TAG, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: ec2Instance.resourceId, + resourceName: ec2Instance.resourceName, + resourceType: AWS_EC2_INSTANCE, + awsRegion: EU_WEST_2 + }, + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: sqsQueue.resourceId, + resourceName: sqsQueue.resourceName, + resourceType: AWS_SQS_QUEUE, + awsRegion: EU_WEST_2 + } + ] + }); + + assert.deepEqual(actualTag2, { + id: arn2, + accountId: ACCOUNT_X, + arn: arn2, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: {}, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn2, + resourceName: `${tagInfo.sqsName}=${tagInfo.sqsValue}`, + resourceType: AWS_TAGS_TAG, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: sqsQueue.resourceId, + resourceName: sqsQueue.resourceName, + resourceType: AWS_SQS_QUEUE, + awsRegion: EU_WEST_2 + } + ] + }); + }); + + it('should handle tags field that is an object', async () => { + const {default: schema} = await import('./fixtures/additionalResources/tags/object.json', {with: {type: 'json' }}); + const {eksCluster, nodeGroup} = generate(schema); + + const mockEksClientClient = { + createEksClient(credentials, region) { + return { + async listNodeGroups() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [nodeGroup]; + } + } + } + }, + } + + const arn = nodeGroup.nodegroupArn; + const tagArn1 = `arn:aws:tags::${ACCOUNT_X}:tag/tag1=value1`; + const tagArn2 = `arn:aws:tags::${ACCOUNT_X}:tag/tag2=value2`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEksClientClient}, [eksCluster]); + + const actualEksNodeGroup = actual.find(x => x.arn === arn); + const actualTag1 = actual.find(x => x.arn === tagArn1); + const actualTag2 = actual.find(x => x.arn === tagArn2); + + assert.deepEqual(actualEksNodeGroup, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + nodegroupArn: nodeGroup.nodegroupArn, + nodegroupName: nodeGroup.nodegroupName, + tags: { + tag1: 'value1', + tag2: 'value2' + } + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: nodeGroup.nodegroupName, + resourceType: AWS_EKS_NODE_GROUP, + tags: [ + { + key: 'tag1', + value: 'value1' + }, + { + key: 'tag2', + value: 'value2' + } + ], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: eksCluster.resourceId, + resourceType: AWS_EKS_CLUSTER + } + ] + }); + + assert.deepEqual(actualTag1, { + id: tagArn1, + accountId: ACCOUNT_X, + arn: tagArn1, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: {}, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: tagArn1, + resourceName: 'tag1=value1', + resourceType: AWS_TAGS_TAG, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: actualEksNodeGroup.resourceId, + resourceName: actualEksNodeGroup.resourceName, + resourceType: AWS_EKS_NODE_GROUP, + awsRegion: EU_WEST_2 + } + ] + }); + + assert.deepEqual(actualTag2, { + id: tagArn2, + accountId: ACCOUNT_X, + arn: tagArn2, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: {}, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: tagArn2, + resourceName: 'tag2=value2', + resourceType: AWS_TAGS_TAG, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: actualEksNodeGroup.resourceId, + resourceName: actualEksNodeGroup.resourceName, + resourceType: AWS_EKS_NODE_GROUP, + awsRegion: EU_WEST_2 + } + ] + }); + + }); + + it('should handle Tags field in upper camel case', async () => { + const {default: schema} = await import('./fixtures/additionalResources/tags/camelCase.json', {with: {type: 'json' }}); + const {tagInfo, instanceRequests} = generate(schema); + + const mockEc2Client = { + createEc2Client(credentials, region) { + return { + async getAllSpotInstanceRequests() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return instanceRequests.euWest2; + } + }, + async getAllSpotFleetRequests() { + return []; + } + } + } + } + + const spotInstanceRequestId1 = 'spotInstanceRequestId1'; + + const arn1 = `arn:aws:ec2:${EU_WEST_2}:${ACCOUNT_X}:spot-instance-request/${spotInstanceRequestId1}`; + const tagArn = `arn:aws:tags::${ACCOUNT_X}:tag/${tagInfo.testTagKey}=${tagInfo.testTagValue}`; + + const instanceId1 = "instanceId1"; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockEc2Client}, []); + + const actualSpotFleet1 = actual.find(x => x.arn === arn1); + const actualTag = actual.find(x => x.arn === tagArn); + + assert.deepEqual(actualSpotFleet1, { + id: arn1, + accountId: ACCOUNT_X, + arn: arn1, + availabilityZone: MULTIPLE_AVAILABILITY_ZONES, + awsRegion: EU_WEST_2, + configuration: { + InstanceId: instanceId1, + SpotInstanceRequestId: spotInstanceRequestId1, + Tags: [ + { + Key: tagInfo.testTagKey, + Value: tagInfo.testTagValue + } + ] + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn1, + resourceName: arn1, + resourceType: AWS_EC2_SPOT, + tags: [ + { + key: tagInfo.testTagKey, + value: tagInfo.testTagValue + } + ], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: instanceId1, + resourceType: AWS_EC2_INSTANCE + } + ] + }); + + assert.deepEqual(actualTag, { + id: tagArn, + accountId: ACCOUNT_X, + arn: tagArn, + availabilityZone: NOT_APPLICABLE, + awsRegion: GLOBAL, + configuration: {}, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: tagArn, + resourceName: `${tagInfo.testTagKey}=${tagInfo.testTagValue}`, + resourceType: AWS_TAGS_TAG, + tags: [], + relationships: [ + { + relationshipName: IS_ASSOCIATED_WITH, + resourceId: actualSpotFleet1.resourceId, + resourceName: actualSpotFleet1.resourceName, + resourceType: AWS_EC2_SPOT, + awsRegion: EU_WEST_2 + } + ] + }); + }); + describe(AWS_DYNAMODB_STREAM, () => { + + it('should discover DynamoDB Streams', async () => { + const {default: schema} = await import('./fixtures/additionalResources/dynamodb/stream.json', {with: {type: 'json' }}); + const {table, stream} = generate(schema); + + const mockDynamoDBStreamsClient = { + createDynamoDBStreamsClient(credentials, region) { + return { + async describeStream(streamArn) { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return { StreamArn: stream.arn } + } + } + } + } + } + const arn = `arn:aws:dynamodb:${EU_WEST_2}:${ACCOUNT_X}:table/test/stream`; + + const actual = await getAllSdkResources({...mockAwsClient, ...mockDynamoDBStreamsClient}, [table]); + + const actualDynamoDBStreamResource = actual.find(x => x.arn === arn); + + assert.deepEqual(actualDynamoDBStreamResource, { + id: arn, + accountId: ACCOUNT_X, + awsRegion: EU_WEST_2, + availabilityZone: NOT_APPLICABLE, + arn: arn, + resourceId: arn, + resourceName: arn, + resourceType: AWS_DYNAMODB_STREAM, + relationships: [], + configuration: { + StreamArn: "arn:aws:dynamodb:eu-west-2:xxxxxxxxxxxx:table/test/stream" + }, + configurationItemStatus: "ResourceDiscovered", + tags: [] + }); + + }); + + }); + + describe(AWS_DYNAMODB_TABLE, () => { + + it('should discover DynamoDB Tables without streams', async () => { + const {default: schema} = await import('./fixtures/relationships/dynamodb/table.json', {with: {type: 'json' }}); + const {tableNoStream} = generate(schema); + + const arn = `arn:aws:dynamodb:${EU_WEST_2}:${ACCOUNT_X}:table/test`; + + const actual = await getAllSdkResources({...mockAwsClient}, [tableNoStream]); + const actualDynamoDBTableResource = actual.find(x => x.arn === arn); + + assert.lengthOf(actual, 1); + + assert.deepEqual(actualDynamoDBTableResource, { + id: arn, + accountId: ACCOUNT_X, + awsRegion: EU_WEST_2, + availabilityZone: NOT_APPLICABLE, + arn: arn, + resourceId: arn, + resourceName: arn, + resourceType: AWS_DYNAMODB_TABLE, + relationships: [], + configuration: {} + }); + + }); + + }); + + describe(AWS_APPSYNC_GRAPHQLAPI, () => { + it('should discover GraphQL Data Sources', async () => { + const {default: schema} = await import('./fixtures/additionalResources/appsync/graphQlApi.json', {with: {type: 'json' }}); + const {graphQLApi, dataSource} = generate(schema); + + const mockAppSyncClient = { + createAppSyncClient(credentials, region) { + return { + async listDataSources() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [dataSource]; + } + }, + listResolvers : async () => [], + } + } + } + + const arn = dataSource.dataSourceArn; + const actual = await getAllSdkResources({...mockAwsClient, ...mockAppSyncClient}, [graphQLApi]); + const actualAppSyncDataSource = actual.find(x => x.arn === arn); + + + + assert.deepEqual(actualAppSyncDataSource, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + dataSourceArn: "DataSourceArn", + name: "DataSourceArn", + apiId: "random-id" + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: dataSource.name, + resourceType: AWS_APPSYNC_DATASOURCE, + tags: [], + relationships: [] + }); + + }); + + it('should discover GraphQL Query Resolvers', async () => { + const {default: schema} = await import('./fixtures/additionalResources/appsync/graphQlApi.json', {with: {type: 'json' }}); + const {graphQLApi, queryResolver} = generate(schema); + + const mockAppSyncClient = { + createAppSyncClient(credentials, region) { + return { + async listResolvers() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [queryResolver]; + } + }, + listDataSources : async () => [], + } + } + } + + const arn = queryResolver.resolverArn; + const actual = await getAllSdkResources({...mockAwsClient, ...mockAppSyncClient}, [graphQLApi]); + const actualQueryResolver = actual.find(x => x.arn === arn); + + assert.deepEqual(actualQueryResolver, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + fieldName: "QueryFieldName", + resolverArn: "ResolverArn", + typeName: "Query", + apiId: "random-id", + dataSourceName: "DataSourceName" + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: queryResolver.fieldName, + resourceType: AWS_APPSYNC_RESOLVER, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: graphQLApi.resourceId, + resourceType: AWS_APPSYNC_GRAPHQLAPI + }, + { + relationshipName: IS_ASSOCIATED_WITH, + resourceName: "DataSourceName", + resourceType: AWS_APPSYNC_DATASOURCE + } + ] + }); + }); + + it('should discover GraphQL Mutation Resolvers', async () => { + const {default: schema} = await import('./fixtures/additionalResources/appsync/graphQlApi.json', {with: {type: 'json' }}); + const {graphQLApi, mutationResolver} = generate(schema); + + const mockAppSyncClient = { + createAppSyncClient(credentials, region) { + return { + async listResolvers() { + if(credentials.accessKeyId === ACCESS_KEY_X && region === EU_WEST_2) { + return [mutationResolver]; + } + }, + listDataSources : async () => [], + } + } + } + + const arn = mutationResolver.resolverArn; + const actual = await getAllSdkResources({...mockAwsClient, ...mockAppSyncClient}, [graphQLApi]); + const actualMutationResolver = actual.find(x => x.arn === arn); + + assert.deepEqual(actualMutationResolver, { + id: arn, + accountId: ACCOUNT_X, + arn: arn, + availabilityZone: NOT_APPLICABLE, + awsRegion: EU_WEST_2, + configuration: { + fieldName: "MutationFieldName", + resolverArn: "ResolverArn", + typeName: "Mutation", + apiId: "random-id", + dataSourceName: "DataSourceName" + }, + configurationItemStatus: RESOURCE_DISCOVERED, + resourceId: arn, + resourceName: mutationResolver.fieldName, + resourceType: AWS_APPSYNC_RESOLVER, + tags: [], + relationships: [ + { + relationshipName: IS_CONTAINED_IN, + resourceId: graphQLApi.resourceId, + resourceType: AWS_APPSYNC_GRAPHQLAPI + }, + { + relationshipName: IS_ASSOCIATED_WITH, + resourceName: "DataSourceName", + resourceType: AWS_APPSYNC_DATASOURCE + } + ] + }); + + }); + + }); + + }); + + }); + +}); \ No newline at end of file diff --git a/source/backend/discovery/test/initialisation.test.mjs b/source/backend/discovery/test/initialisation.test.mjs new file mode 100644 index 00000000..16a1dd5c --- /dev/null +++ b/source/backend/discovery/test/initialisation.test.mjs @@ -0,0 +1,134 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import {initialise} from '../src/lib/intialisation.mjs'; +import {AWS_ORGANIZATIONS} from '../src/lib/constants.mjs'; +import {AggregatorNotFoundError, OrgAggregatorValidationError} from '../src/lib/errors.mjs'; + +describe('initialisation', () => { + const ACCOUNT_X = 'xxxxxxxxxxxx'; + const ACCOUNT_Y = 'yyyyyyyyyyyy'; + const EU_WEST_1= 'eu-west-1'; + const US_EAST_1= 'us-east-1'; + + describe('initialise', () => { + + const defaultMockAwsClient = { + createEcsClient() { + return { + getAllClusterTasks: async arn => [ + {taskDefinitionArn: `arn:aws:ecs:eu-west-1:${ACCOUNT_X}:task-definition/workload-discovery-taskgroup:1`} + ] + } + }, + createEc2Client() { + return { + async getAllRegions() { + return [] + } + }; + }, + createConfigServiceClient() { + return {} + }, + createOrganizationsClient() { + return { + async getAllAccounts() { + return [] + }, + async getRootAccount() { + return { + Arn: `arn:aws:organizations::${ACCOUNT_X}:account/o-exampleorgid/:${ACCOUNT_X}` + } + } + } + }, + createStsClient() { + return { + getCurrentCredentials: async () => { + return {accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken'}; + }, + getCredentials: async role => {} + } + } + }; + + const defaultAppSync = () => { + return { + getAccounts: async () => [ + {accountId: ACCOUNT_X, regions: [{name: EU_WEST_1}]} + ] + } + }; + + const defaultConfig = { + region: EU_WEST_1, + rootAccountId: ACCOUNT_X, + cluster: 'testCluster', + configAggregator: 'configAggregator' + }; + + it('should throw if another copy of the ECS task is running', async () => { + const mockAwsClient = { + createEcsClient() { + return { + getAllClusterTasks: async () => [ + {taskDefinitionArn: `arn:aws:ecs:eu-west-1:${ACCOUNT_X}:task-definition/workload-discovery-taskgroup:1`}, + {taskDefinitionArn: `arn:aws:ecs:eu-west-1:${ACCOUNT_X}:task-definition/workload-discovery-taskgroup:2`} + ] + } + } + }; + + return initialise({...defaultMockAwsClient, ...mockAwsClient}, defaultAppSync, defaultConfig) + .catch(err => assert.strictEqual(err.message, 'Discovery process ECS task is already running in cluster.')); + }); + + it('should throw AggregatorNotFoundError if config aggregator does not exist in AWS organization', async () => { + const mockAwsClient = { + createConfigServiceClient() { + return { + async getConfigAggregator() { + const error = new Error(); + error.name = 'NoSuchConfigurationAggregatorException'; + throw error; + } + } + } + }; + + return initialise({...defaultMockAwsClient, ...mockAwsClient}, defaultAppSync, {...defaultConfig, crossAccountDiscovery: AWS_ORGANIZATIONS}) + .then(() => { + throw new Error('Expected error not thrown.'); + }) + .catch(err => { + assert.instanceOf(err, AggregatorNotFoundError); + assert.strictEqual(err.message, `Aggregator ${defaultConfig.configAggregator} was not found`); + }); + }); + + it('should throw OrgAggregatorValidationError if config aggregator is not org wide in AWS organization mode', async () => { + const mockAwsClient = { + createConfigServiceClient() { + return { + async getConfigAggregator() { + return {}; + } + } + } + }; + + return initialise({...defaultMockAwsClient, ...mockAwsClient}, defaultAppSync, {...defaultConfig, crossAccountDiscovery: AWS_ORGANIZATIONS}) + .then(() => { + throw new Error('Expected error not thrown.'); + }) + .catch(err => { + assert.instanceOf(err, OrgAggregatorValidationError); + assert.strictEqual(err.message, 'Config aggregator is not an organization wide aggregator'); + }); + }); + + }); + +}); \ No newline at end of file diff --git a/source/backend/discovery/test/mocks/agents/ConnectionClosed.mjs b/source/backend/discovery/test/mocks/agents/ConnectionClosed.mjs new file mode 100644 index 00000000..fbfd7e2e --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/ConnectionClosed.mjs @@ -0,0 +1,31 @@ +import {MockAgent} from 'undici'; +import { + CONNECTION_CLOSED_PREMATURELY +} from '../../../src/lib/constants.mjs'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, { + errors: [ + {message: CONNECTION_CLOSED_PREMATURELY} + ] + }); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, { + data: { + addRelationships: [] + } + }); + +export default agent; \ No newline at end of file diff --git a/source/backend/discovery/test/mocks/agents/DeleteIndexedResourcesPartialSuccess.mjs b/source/backend/discovery/test/mocks/agents/DeleteIndexedResourcesPartialSuccess.mjs new file mode 100644 index 00000000..7b2aaa14 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/DeleteIndexedResourcesPartialSuccess.mjs @@ -0,0 +1,18 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + deleteIndexedResources: { + unprocessedResources: ['arn1'] + } + }}).persist(); + +export default agent; diff --git a/source/backend/discovery/test/mocks/agents/GenericError.mjs b/source/backend/discovery/test/mocks/agents/GenericError.mjs new file mode 100644 index 00000000..b3e02a90 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/GenericError.mjs @@ -0,0 +1,18 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, { + errors: [ + {message: 'Validation error'} + ] + }).persist(); + +export default agent; \ No newline at end of file diff --git a/source/backend/discovery/test/mocks/agents/GetAccountsOrgsDeleted.mjs b/source/backend/discovery/test/mocks/agents/GetAccountsOrgsDeleted.mjs new file mode 100644 index 00000000..08e49551 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/GetAccountsOrgsDeleted.mjs @@ -0,0 +1,26 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const ACCOUNT_X = 'xxxxxxxxxxxx'; +const ACCOUNT_Y = 'yyyyyyyyyyyy'; +const ACCOUNT_Z = 'zzzzzzzzzzzz'; +const EU_WEST_1= 'eu-west-1'; +const US_EAST_1= 'us-east-1'; + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getAccounts: [ + {accountId: ACCOUNT_X, name: 'Account X', organizationId: "o-exampleorgid", regions: [{name: EU_WEST_1}, {name: US_EAST_1}]}, + {accountId: ACCOUNT_Y, name: 'Account Y', organizationId: "o-exampleorgid", regions: [{name: EU_WEST_1}]}, + {accountId: ACCOUNT_Z, name: 'Account Z', organizationId: "o-exampleorgid", regions: [{name: EU_WEST_1}, {name: US_EAST_1}]} + ] + }}); + +export default agent; diff --git a/source/backend/discovery/test/mocks/agents/GetAccountsOrgsEmpty.mjs b/source/backend/discovery/test/mocks/agents/GetAccountsOrgsEmpty.mjs new file mode 100644 index 00000000..ceef5e05 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/GetAccountsOrgsEmpty.mjs @@ -0,0 +1,16 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getAccounts: [] + }}).persist(); + +export default agent; diff --git a/source/backend/discovery/test/mocks/agents/GetAccountsOrgsLastCrawled.mjs b/source/backend/discovery/test/mocks/agents/GetAccountsOrgsLastCrawled.mjs new file mode 100644 index 00000000..a96da825 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/GetAccountsOrgsLastCrawled.mjs @@ -0,0 +1,22 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const ACCOUNT_X = 'xxxxxxxxxxxx'; +const EU_WEST_1= 'eu-west-1'; +const US_EAST_1= 'us-east-1'; + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getAccounts: [ + {accountId: ACCOUNT_X, name: 'Account X', lastCrawled: new Date('2022-10-25').toISOString(), organizationId: "o-exampleorgid", regions: [{name: EU_WEST_1}, {name: US_EAST_1}]}, + ] + }}); + +export default agent; diff --git a/source/backend/discovery/test/mocks/agents/GetAccountsSelfManaged.mjs b/source/backend/discovery/test/mocks/agents/GetAccountsSelfManaged.mjs new file mode 100644 index 00000000..cace5c31 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/GetAccountsSelfManaged.mjs @@ -0,0 +1,24 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const ACCOUNT_X = 'xxxxxxxxxxxx'; +const ACCOUNT_Y = 'yyyyyyyyyyyy'; +const EU_WEST_1= 'eu-west-1'; +const US_EAST_1= 'us-east-1'; + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getAccounts: [ + {accountId: ACCOUNT_X, name: 'Account X', regions: [{name: EU_WEST_1}, {name: US_EAST_1}]}, + {accountId: ACCOUNT_Y, name: 'Account Y', regions: [{name: EU_WEST_1}]} + ] + }}).persist(); + +export default agent; diff --git a/source/backend/discovery/test/mocks/agents/GetDbRelationshipsMapPagination.mjs b/source/backend/discovery/test/mocks/agents/GetDbRelationshipsMapPagination.mjs new file mode 100644 index 00000000..a8caaa8d --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/GetDbRelationshipsMapPagination.mjs @@ -0,0 +1,40 @@ +import {MockAgent} from 'undici'; +import { + CONTAINS, AWS_EC2_VPC, AWS_EC2_SUBNET +} from '../../../src/lib/constants.mjs'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getRelationships: [ + { + id: 'testId', + label: CONTAINS, + source: { + id: 'sourceArn', + label: AWS_EC2_VPC + }, + target: { + id: 'targetArn', + label: AWS_EC2_SUBNET + }, + } + ] + }}); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getRelationships: [] + }}); + +export default agent; \ No newline at end of file diff --git a/source/backend/discovery/test/mocks/agents/GetDbResourcesMapPagination.mjs b/source/backend/discovery/test/mocks/agents/GetDbResourcesMapPagination.mjs new file mode 100644 index 00000000..3e7b8425 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/GetDbResourcesMapPagination.mjs @@ -0,0 +1,32 @@ +import {MockAgent} from 'undici'; +import { + AWS_LAMBDA_FUNCTION +} from '../../../src/lib/constants.mjs'; +import {generateBaseResource} from '../../generator.mjs'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +const properties = generateBaseResource('xxxxxxxxxxxx', 'eu-west-1', AWS_LAMBDA_FUNCTION, 1); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getResources: [ + {id: properties.arn, label: 'label', md5Hash: '', properties} + ] + }}); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + getResources: [] + }}); + +export default agent; \ No newline at end of file diff --git a/source/backend/discovery/test/mocks/agents/IndexResourcesPartialSuccess.mjs b/source/backend/discovery/test/mocks/agents/IndexResourcesPartialSuccess.mjs new file mode 100644 index 00000000..fed097bd --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/IndexResourcesPartialSuccess.mjs @@ -0,0 +1,18 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + indexResources: { + unprocessedResources: ['arn1'] + } + }}).persist(); + +export default agent; diff --git a/source/backend/discovery/test/mocks/agents/UpdateIndexedResourcesPartialSuccess.mjs b/source/backend/discovery/test/mocks/agents/UpdateIndexedResourcesPartialSuccess.mjs new file mode 100644 index 00000000..553997e8 --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/UpdateIndexedResourcesPartialSuccess.mjs @@ -0,0 +1,18 @@ +import {MockAgent} from 'undici'; + +const agent = new MockAgent(); +agent.disableNetConnect(); + +const client = agent.get('https://www.workload-discovery'); + +client.intercept({ + path: '/graphql', + method: 'POST' +}) + .reply(200, {data: { + updateIndexedResources: { + unprocessedResources: ['arn1'] + } + }}).persist(); + +export default agent; diff --git a/source/backend/discovery/test/mocks/agents/utils.mjs b/source/backend/discovery/test/mocks/agents/utils.mjs new file mode 100644 index 00000000..7b4da61b --- /dev/null +++ b/source/backend/discovery/test/mocks/agents/utils.mjs @@ -0,0 +1,26 @@ +import {MockAgent} from 'undici'; + +export function createSuccessThenError (successResult, errorMsg) { + const agent = new MockAgent(); + agent.disableNetConnect(); + + const client = agent.get('https://www.workload-discovery'); + + client.intercept({ + path: '/graphql', + method: 'POST' + }) + .reply(200, successResult); + + client.intercept({ + path: '/graphql', + method: 'POST' + }) + .reply(200, { + errors: [ + {message: errorMsg} + ] + }); + + return agent; +} \ No newline at end of file diff --git a/source/backend/discovery/test/persistence/index.test.mjs b/source/backend/discovery/test/persistence/index.test.mjs new file mode 100644 index 00000000..6d9615dd --- /dev/null +++ b/source/backend/discovery/test/persistence/index.test.mjs @@ -0,0 +1,147 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import sinon from 'sinon'; +import { + persistResourcesAndRelationships, + processPersistenceFailures +} from '../../src/lib/persistence/index.mjs'; +import {generateBaseResource} from '../generator.mjs'; +import { + AWS_LAMBDA_FUNCTION, + AWS_EC2_VPC, + AWS_EC2_INSTANCE, + AWS_IAM_ROLE, + AWS_RDS_DB_CLUSTER +} from '../../src/lib/constants.mjs'; + +describe('index.mjs', () => { + const mockNoErrors = {errors: []}; + + describe('batching', () => { + const mockApiClient = { + deleteResources: sinon.stub().resolves(mockNoErrors), + updateResources: sinon.stub().resolves(mockNoErrors), + storeResources: sinon.stub().resolves(mockNoErrors), + deleteRelationships: sinon.stub().resolves(mockNoErrors), + storeRelationships: sinon.stub().resolves(mockNoErrors), + }; + + it('should batch requests to the backend', async () => { + await persistResourcesAndRelationships(mockApiClient, { + resourceIdsToDelete: [], resourcesToStore: [], resourcesToUpdate: [], + linksToAdd: [], linksToDelete: [] + }); + + sinon.assert.calledWith(mockApiClient.deleteResources, {concurrency: 5, batchSize: 50}); + sinon.assert.calledWith(mockApiClient.updateResources, {concurrency: 10, batchSize: 10}); + sinon.assert.calledWith(mockApiClient.storeResources, {concurrency: 10, batchSize: 10}); + sinon.assert.calledWith(mockApiClient.deleteRelationships, {concurrency: 5, batchSize: 50}); + sinon.assert.calledWith(mockApiClient.storeRelationships, {concurrency: 10, batchSize: 20}); + }); + }); + + describe('write errors', () => { + it('returns delete failures', async () => { + const mockApiClient = { + deleteResources: sinon.stub().resolves( + {errors: [{item: ['arn1', 'arn2']}, {item: ['arn3']}, {item: ['arn4']}]} + ), + updateResources: sinon.stub().resolves(mockNoErrors), + storeResources: sinon.stub().resolves(mockNoErrors), + deleteRelationships: sinon.stub().resolves(mockNoErrors), + storeRelationships: sinon.stub().resolves(mockNoErrors), + }; + + const {failedDeletes} = await persistResourcesAndRelationships(mockApiClient, { + resourceIdsToDelete: [], resourcesToStore: [], resourcesToUpdate: [], + linksToAdd: [], linksToDelete: [] + }); + + assert.deepEqual(failedDeletes, ['arn1', 'arn2', 'arn3', 'arn4']) + }); + + it('returns store failures', async () => { + const mockApiClient = { + deleteResources: sinon.stub().resolves(mockNoErrors), + updateResources: sinon.stub().resolves(mockNoErrors), + storeResources: sinon.stub().resolves( + {errors: [{item: [{id: 'arn1'}, {id: 'arn2'}]}, {item: [{id: 'arn3'}]}, {item: [{id: 'arn4'}]}]} + ), + deleteRelationships: sinon.stub().resolves(mockNoErrors), + storeRelationships: sinon.stub().resolves(mockNoErrors), + }; + + const {failedStores} = await persistResourcesAndRelationships(mockApiClient, { + resourceIdsToDelete: [], resourcesToStore: [], resourcesToUpdate: [], + linksToAdd: [], linksToDelete: [] + }); + + assert.deepEqual(failedStores, ['arn1', 'arn2', 'arn3', 'arn4']) + }); + + }); + + describe('processPersistenceFailures', () => { + const ACCOUNT_IDX = 'xxxxxxxxxxxx'; + const EU_WEST_1 = 'eu-west-1'; + + it('should removed failed stores', () => { + const dbResources = [ + generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_LAMBDA_FUNCTION, 1), + generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_EC2_VPC, 2), + generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_EC2_INSTANCE, 3), + ]; + + const dbResourcesMap = new Map(dbResources.map(x => [x.id, x])); + const resourceToStore = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_IAM_ROLE, 4); + const resourceToStoreFail = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_RDS_DB_CLUSTER, 5); + + const resources = [ + ...dbResources, + resourceToStoreFail, + resourceToStore + ]; + + const actual = processPersistenceFailures(dbResourcesMap, resources, { + failedDeletes: [], failedStores: [resourceToStoreFail.id] + }); + + assert.deepEqual([ + ...dbResources, + resourceToStore + ], actual); + }); + + it('should keep failed deletes', () => { + const resources = [ + generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_LAMBDA_FUNCTION, 1), + generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_EC2_VPC, 2), + generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_EC2_INSTANCE, 3), + ]; + + const resourceToDelete = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_IAM_ROLE, 4); + const resourceToDeleteFail = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_RDS_DB_CLUSTER, 5); + + const dbResources = [ + ...resources, + resourceToDelete, + resourceToDeleteFail + ]; + + const dbResourcesMap = new Map(dbResources.map(x => [x.id, x])); + + const actual = processPersistenceFailures(dbResourcesMap, resources, { + failedDeletes: [resourceToDeleteFail.id], failedStores: [] + }); + + assert.deepEqual([ + ...resources, + resourceToDeleteFail + ], actual); + }); + + }); + +}); diff --git a/source/backend/discovery/test/persistence/transformers.test.mjs b/source/backend/discovery/test/persistence/transformers.test.mjs new file mode 100644 index 00000000..b2b0372f --- /dev/null +++ b/source/backend/discovery/test/persistence/transformers.test.mjs @@ -0,0 +1,250 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import * as R from 'ramda'; +import {generateBaseResource, generateRandomInt} from '../generator.mjs'; +import {createSaveObject, createResourcesRegionMetadata} from '../../src/lib/persistence/transformers.mjs'; +import { + AWS_API_GATEWAY_METHOD, + AWS_API_GATEWAY_RESOURCE, + AWS_DYNAMODB_STREAM, + AWS_ECS_TASK, + AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, + AWS_EKS_NODE_GROUP, + AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, + AWS_IAM_AWS_MANAGED_POLICY, + AWS_LAMBDA_FUNCTION, + AWS_EC2_SPOT, + AWS_EC2_SPOT_FLEET, + AWS_IAM_INLINE_POLICY, + AWS_OPENSEARCH_DOMAIN, + AWS_AUTOSCALING_AUTOSCALING_GROUP, + AWS_API_GATEWAY_REST_API, + AWS_IAM_ROLE, + AWS_IAM_GROUP, + AWS_IAM_USER, + AWS_IAM_POLICY, + AWS_S3_BUCKET, + AWS_RDS_DB_CLUSTER, + AWS_ECS_CLUSTER, + AWS_EC2_VPC, + AWS_EC2_SUBNET, + AWS_EC2_INSTANCE +} from '../../src/lib/constants.mjs'; + +const ACCOUNT_IDX = 'xxxxxxxxxxxx'; +const EU_WEST_1 = 'eu-west-1'; + +describe('persistence/transformers', () => { + + describe('createSaveObject', () => { + + describe('hashing', () => { + + [ + [AWS_API_GATEWAY_METHOD, 'e77a45b311fc1a9fa083d959fef13cf1'], + [AWS_API_GATEWAY_RESOURCE, '49e48e16ca5f6dc8205d6b4e5a28760d'], + [AWS_ECS_TASK, '24b51c39cf2f472cdb96bcd5bc4bb87a'], + [AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, '0a6a70f85bdac8608bb4b3e0f917ce64'], + [AWS_EKS_NODE_GROUP, '288a15ee94f1278f5f50783b4521e810'], + [AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, 'aeed86caaad0d80172a51d94c5547fe2'], + [AWS_IAM_AWS_MANAGED_POLICY, '1a18a8a6f9f4863fd603c4ce93491a37'], + [AWS_EC2_SPOT, 'ad454ecd37a904ba2b33ff07aac54106'], + [AWS_EC2_SPOT_FLEET, 'e5c1ab47ac15a1e6c39480bb2265a221'], + [AWS_IAM_INLINE_POLICY, '9be3badedb6f62a7a6e48f19dc1702c8'], + [AWS_OPENSEARCH_DOMAIN, '183f22ba18a821428718cd8c1db3d467'], + [AWS_DYNAMODB_STREAM, '6a96a6977befda49b9fab8a1f4917abd'] + ].forEach(([resourceType, hash], i) => { + it(`should hash ${resourceType} resources`, () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, resourceType, i); + + const actual = createSaveObject(resource); + assert.strictEqual(actual.md5Hash, hash); + }); + }); + + }); + + describe('title', () => { + + it('should use name tag for title if present', () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_LAMBDA_FUNCTION, 1); + + const actual = createSaveObject({...resource, tags: [{key: 'Name', value: 'testName'}]}); + assert.strictEqual(actual.properties.title, 'testName'); + }); + + it('should fall back to resource name if name tag not present', () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_LAMBDA_FUNCTION, 1); + + const actual = createSaveObject({...resource, resourceName: 'resourceName'}); + assert.strictEqual(actual.properties.title, 'resourceName'); + }); + + it('should fall back to resource id if resource name or name tag not present', () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_LAMBDA_FUNCTION, 1); + + const actual = createSaveObject(R.omit(['resourceName'], resource)); + assert.strictEqual(actual.properties.title, 'resourceId1'); + }); + + it('should use the target group id for an ALB title', () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_ELASTIC_LOAD_BALANCING_V2_TARGET_GROUP, 1); + + const actual = createSaveObject({...resource, arn: 'arn:aws:elasticloadbalancing:us-west-2:xxxxxxxxxxxx:targetgroup/my-targets/73e2d6bc24d8a067'}); + assert.strictEqual(actual.properties.title, 'targetgroup/my-targets/73e2d6bc24d8a067'); + }); + + it('should use the listener id for an ALB title', () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_ELASTIC_LOAD_BALANCING_V2_LISTENER, 1); + + const actual = createSaveObject({...resource, arn: 'arn:aws:elasticloadbalancing:us-west-2:xxxxxxxxxxxx:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'}); + assert.strictEqual(actual.properties.title, 'listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); + }); + + it('should use the listener id for an ASG title', () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, AWS_AUTOSCALING_AUTOSCALING_GROUP, 1); + + const actual = createSaveObject({...resource, arn: 'arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:123e4567-e89b-12d3-a456-426614174000:autoScalingGroupName/asg-name'}); + assert.strictEqual(actual.properties.title, 'asg-name'); + }); + + }); + + describe('logins', () => { + + [ + [AWS_API_GATEWAY_REST_API, {id: 'restId'}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/apigateway?region=eu-west-1#/apis/restId/resources', + expectedLoggedInURL: 'https://eu-west-1.console.aws.amazon.com/apigateway/home?region=eu-west-1#/apis/restId/resources' + }], + [AWS_API_GATEWAY_RESOURCE, {id: 'apiGwResourceId', RestApiId: 'restId'}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/apigateway?region=eu-west-1#/apis/restId/resources/apiGwResourceId', + expectedLoggedInURL: 'https://eu-west-1.console.aws.amazon.com/apigateway/home?region=eu-west-1#/apis/restId/resources/apiGwResourceId' + }], + [AWS_API_GATEWAY_METHOD, {httpMethod: 'GET', ResourceId: 'apiGwResourceId', RestApiId: 'restId'}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/apigateway?region=eu-west-1#/apis/restId/resources/apiGwResourceId/GET', + expectedLoggedInURL: 'https://eu-west-1.console.aws.amazon.com/apigateway/home?region=eu-west-1#/apis/restId/resources/apiGwResourceId/GET' + }], + [AWS_AUTOSCALING_AUTOSCALING_GROUP, {}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/ec2/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=resourceName3;view=details', + expectedLoggedInURL: 'https://eu-west-1.console.aws.amazon.com/ec2/home/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=resourceName3;view=details' + }], + [AWS_LAMBDA_FUNCTION, {}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/lambda?region=eu-west-1#/functions/resourceName4?tab=graph', + expectedLoggedInURL: 'https://eu-west-1.console.aws.amazon.com/lambda/home?region=eu-west-1#/functions/resourceName4?tab=graph' + }], + [AWS_IAM_ROLE, {}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/roles', + expectedLoggedInURL: 'https://console.aws.amazon.com/iam/home?#/roles' + }], + [AWS_IAM_GROUP, {}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/groups', + expectedLoggedInURL: 'https://console.aws.amazon.com/iam/home?#/groups' + }], + [AWS_IAM_USER, {}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/users', + expectedLoggedInURL: 'https://console.aws.amazon.com/iam/home?#/users' + }], + [AWS_IAM_POLICY, {}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/policies', + expectedLoggedInURL: 'https://console.aws.amazon.com/iam/home?#/policies' + }], + [AWS_S3_BUCKET, {}, { + expectedLoginUrl: 'https://xxxxxxxxxxxx.signin.aws.amazon.com/console/s3?bucket=resourceName9', + expectedLoggedInURL: 'https://s3.console.aws.amazon.com/s3/buckets/resourceName9/?region=eu-west-1' + }] + ].forEach(([resourceType, configuration, {expectedLoginUrl, expectedLoggedInURL}], i) => { + it(`should create logins for ${resourceType}`, () => { + const resource = generateBaseResource(ACCOUNT_IDX, EU_WEST_1, resourceType, i); + + const actual = createSaveObject({...resource, configuration}); + assert.strictEqual(actual.properties.loginURL, expectedLoginUrl); + assert.strictEqual(actual.properties.loggedInURL, expectedLoggedInURL); + }); + }); + + + }); + + describe('json fields', () => { + + it('should JSON stringify configuration, supplementaryConfiguration, tags, state fields', () => { + const resource = { + id: 'arn1', + resourceId: 'resourceId', + resourceName: 'resourceName', + resourceType: 'AWS::S3::Bucket', + accountId: ACCOUNT_IDX, + arn: 'arn1', + awsRegion: EU_WEST_1, + relationships: [], + tags: [], + configuration: {a: 1}, + supplementaryConfiguration: {b: 1}, + state: {c: 1} + }; + + const actual = createSaveObject(resource); + assert.strictEqual(actual.properties.tags, '[]'); + assert.strictEqual(actual.properties.configuration, '{"a":1}'); + assert.strictEqual(actual.properties.supplementaryConfiguration, '{"b":1}'); + assert.strictEqual(actual.properties.state, '{"c":1}'); + }); + + }) + }); + + describe('account metadata', () => { + const ACCOUNT_IDX = 'xxxxxxxxxxxx'; + const ACCOUNT_IDY = 'yyyyyyyyyyyy'; + const ACCOUNT_IDZ = 'zzzzzzzzzzzz'; + const GLOBAL = 'global'; + + const EU_WEST_1 = 'eu-west-1'; + const EU_WEST_2 = 'eu-west-2'; + const US_WEST_2 = 'us-west-2'; + + const resources = [ + [ACCOUNT_IDX, EU_WEST_1, AWS_API_GATEWAY_METHOD, 3], + [ACCOUNT_IDX, EU_WEST_1, AWS_RDS_DB_CLUSTER, 7], + [ACCOUNT_IDX, EU_WEST_2, AWS_API_GATEWAY_RESOURCE, 8], + [ACCOUNT_IDX, US_WEST_2 ,AWS_ECS_CLUSTER, 1], + [ACCOUNT_IDX, US_WEST_2 ,AWS_ECS_TASK, 4], + [ACCOUNT_IDY, EU_WEST_1, AWS_EC2_VPC, 3], + [ACCOUNT_IDY, EU_WEST_1, AWS_LAMBDA_FUNCTION, 10], + [ACCOUNT_IDY, EU_WEST_2, AWS_LAMBDA_FUNCTION, 6], + [ACCOUNT_IDY, GLOBAL, AWS_IAM_ROLE, 15], + [ACCOUNT_IDZ, EU_WEST_1, AWS_EC2_VPC, 2], + [ACCOUNT_IDZ, US_WEST_2, AWS_EC2_VPC, 2], + [ACCOUNT_IDZ, US_WEST_2, AWS_EC2_SUBNET, 9], + [ACCOUNT_IDZ, US_WEST_2, AWS_EC2_INSTANCE, 12], + ].flatMap(([accountId, region, resourceType, count]) => { + const resources = []; + + for(let i = 0; i < count; i++) { + const randomInt = generateRandomInt(0, 100000) + const {id,...properties} = generateBaseResource(accountId, region, resourceType, randomInt) + resources.push({ + id, + label: properties.resourceType.replace(/::/g, '_'), + md5Hash: '', + properties + }); + } + + return resources; + }); + + it('should get resourcesRegionMetadata', async () => { + const {default: expectedResourcesRegionMetadata} = await import('../fixtures/persistence/transformers/accountMetadata/resourcesAccountMetadataExpected.json', {with: {type: 'json' }}); + const expected = new Map(expectedResourcesRegionMetadata.map(x => [x.accountId, x])); + + const resourcesRegionMetadata = createResourcesRegionMetadata(resources); + + assert.deepEqual(resourcesRegionMetadata, expected); + }); + + }); +}); \ No newline at end of file diff --git a/source/backend/discovery/test/utils.test.mjs b/source/backend/discovery/test/utils.test.mjs new file mode 100644 index 00000000..94d6082b --- /dev/null +++ b/source/backend/discovery/test/utils.test.mjs @@ -0,0 +1,89 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import {createArn, createArnWithResourceType} from '../src/lib/utils.mjs'; + +describe('utils.mjs', () => { + + describe('createArn', () => { + + it('should create correct partition for China North region', async () => { + const expected = 'arn:aws-cn:ec2:cn-north-1:xxxxxxxxxxxx:volume/vol-1a2b3c4d'; + const actual = createArn({ + service: 'ec2', region: 'cn-north-1', accountId: 'xxxxxxxxxxxx', resource: 'volume/vol-1a2b3c4d' + }); + + assert.deepEqual(actual, expected); + }); + + it('should create correct partition for China Northwest region', async () => { + const expected = 'arn:aws-cn:ec2:cn-northwest-1:xxxxxxxxxxxx:volume/vol-1a2b3c4d'; + const actual = createArn({ + service: 'ec2', region: 'cn-northwest-1', accountId: 'xxxxxxxxxxxx', resource: 'volume/vol-1a2b3c4d' + }); + + assert.deepEqual(actual, expected); + }); + + + it('should create correct partition for GovCloud East region', async () => { + const expected = 'arn:aws-us-gov:ec2:us-gov-east-1:xxxxxxxxxxxx:volume/vol-1a2b3c4d'; + const actual = createArn({ + service: 'ec2', region: 'us-gov-east-1', accountId: 'xxxxxxxxxxxx', resource: 'volume/vol-1a2b3c4d' + }); + + assert.deepEqual(actual, expected); + }); + + it('should create correct partition for GovCloud West region', async () => { + const expected = 'arn:aws-us-gov:ec2:us-gov-west-1:xxxxxxxxxxxx:volume/vol-1a2b3c4d'; + const actual = createArn({ + service: 'ec2', region: 'us-gov-west-1', accountId: 'xxxxxxxxxxxx', resource: 'volume/vol-1a2b3c4d' + }); + + assert.deepEqual(actual, expected); + }); + + it('should create correct partition for standard regions', async () => { + const expected = 'arn:aws:ec2:us-west-1:xxxxxxxxxxxx:volume/vol-1a2b3c4d'; + const actual = createArn({ + service: 'ec2', region: 'us-west-1', accountId: 'xxxxxxxxxxxx', resource: 'volume/vol-1a2b3c4d' + }); + + assert.deepEqual(actual, expected); + }); + + it('should default to an empty string if account id or region is absent', async () => { + const expected = 'arn:aws:s3:::myBucket'; + const actual = createArn({ + service: 's3', resource: 'myBucket' + }); + + assert.deepEqual(actual, expected); + }); + }); + + describe('createArnWithResourceType', () => { + + it('should create an arn using a resource type and id', () => { + const expected = 'arn:aws:config:us-west-1:xxxxxxxxxxxx:resourcecompliance/resourceId'; + const actual = createArnWithResourceType( + {resourceType: 'AWS::Config::ResourceCompliance', accountId: 'xxxxxxxxxxxx', awsRegion: 'us-west-1', resourceId: 'resourceId'} + ); + + assert.deepEqual(actual, expected); + }); + + it('should default to an empty string if account id or region is absent', () => { + const expected = 'arn:aws:config:::resourcecompliance/resourceId'; + const actual = createArnWithResourceType( + {resourceType: 'AWS::Config::ResourceCompliance', resourceId: 'resourceId'} + ); + + assert.deepEqual(actual, expected); + }); + + }); + +}); \ No newline at end of file diff --git a/source/backend/discovery/vitest.config.mjs b/source/backend/discovery/vitest.config.mjs new file mode 100644 index 00000000..c3e6c5b8 --- /dev/null +++ b/source/backend/discovery/vitest.config.mjs @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: [ + ['lcov', { 'projectRoot': '../../../..' }], + ['html'], + ['text'], + ['json'] + ] + } + } +}); diff --git a/source/backend/functions/account-import-templates-api/src/index.mjs b/source/backend/functions/account-import-templates-api/src/index.mjs new file mode 100644 index 00000000..30c76345 --- /dev/null +++ b/source/backend/functions/account-import-templates-api/src/index.mjs @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from 'node:fs'; +import {Logger} from '@aws-lambda-powertools/logger'; + +const logger = new Logger({serviceName: 'WdAccountImportTemplateApi'}); + +const globalTemplate = fs.readFileSync( + `${import.meta.dirname}/global-resources.template`, + 'utf8' +); +const regionalTemplate = fs.readFileSync( + `${import.meta.dirname}/regional-resources.template`, + 'utf8' +); + +async function replaceGlobalTemplateSubstitutes( + {accountId, region, discoveryRoleArn, externalId, myApplicationsLambdaRoleArn, version}, + template +) { + return template + .replace('<>', accountId) + .replace('<>', discoveryRoleArn) + .replace('<>', externalId) + .replace( + '<>', + myApplicationsLambdaRoleArn + ) + .replace('<>', region) + .replace('', version); +} + +async function replaceRegionalTemplateSubstitutes( + {accountId, region, version}, + template +) { + return template + .replace('<>', accountId) + .replace('<>', region) + .replace('<>', version); +} + +export function _handler(env) { + return event => { + const fieldName = event.info.fieldName; + + const {username} = event.identity; + logger.info(`User ${username} invoked the ${fieldName} operation.`); + + const args = event.arguments; + logger.info( + 'GraphQL arguments:', + {arguments: args, operation: fieldName} + ); + + const { + ACCOUNT_ID: accountId, + DISCOVERY_ROLE_ARN: discoveryRoleArn, + EXTERNAL_ID: externalId, + MY_APPLICATIONS_LAMBDA_ROLE_ARN: myApplicationsLambdaRoleArn, + REGION: region, + SOLUTION_VERSION: version, + } = env; + + switch (fieldName) { + case 'getGlobalTemplate': + return replaceGlobalTemplateSubstitutes( + { + accountId, + region, + discoveryRoleArn, + externalId, + myApplicationsLambdaRoleArn, + version, + }, + globalTemplate + ); + case 'getRegionalTemplate': + return replaceRegionalTemplateSubstitutes( + {accountId, region, version}, + regionalTemplate + ); + default: + return Promise.reject( + new Error( + `Unknown field, unable to resolve ${fieldName}.` + ) + ); + } + }; +} + +export const handler = _handler(process.env); diff --git a/source/backend/functions/account-import-templates-api/test/index.test.mjs b/source/backend/functions/account-import-templates-api/test/index.test.mjs new file mode 100644 index 00000000..04187690 --- /dev/null +++ b/source/backend/functions/account-import-templates-api/test/index.test.mjs @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {yamlParse} from 'yaml-cfn'; +import {assert, describe, it} from 'vitest'; +import {_handler} from '../src/index.mjs'; + +describe('index.js', () => { + const ACCOUNT_ID = 'xxxxxxxxxxxx'; + const REGION = 'eu-west-1'; + const EXTERNAL_ID = 'stsExternalId' + const DISCOVERY_ROLE_ARN = 'discoveryRoleArn'; + const MY_APPLICATIONS_LAMBDA_ROLE_ARN = 'myApplicationsLambdaRoleArn'; + const SOLUTION_VERSION = '9.9.9'; + + describe('handler', () => { + const handler = _handler({ + ACCOUNT_ID, + DISCOVERY_ROLE_ARN, + EXTERNAL_ID, + MY_APPLICATIONS_LAMBDA_ROLE_ARN, + REGION, + SOLUTION_VERSION, + }); + + describe('getGlobalTemplate', () => { + it('should incorporate account id into global template', async () => { + const actual = await handler({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'getGlobalTemplate', + }, + }); + const json = yamlParse(actual); + assert.strictEqual( + json.Parameters.WorkloadDiscoveryAccountId.Default, + ACCOUNT_ID + ); + assert.strictEqual( + json.Parameters.WorkloadDiscoveryAggregationRegion.Default, + REGION + ); + assert.strictEqual( + json.Parameters.WorkloadDiscoveryDiscoveryRoleArn.Default, + DISCOVERY_ROLE_ARN + ); + assert.strictEqual( + json.Parameters.WorkloadDiscoveryExternalId.Default, + EXTERNAL_ID + ); + assert.strictEqual( + json.Parameters.MyApplicationsLambdaRoleArn.Default, + MY_APPLICATIONS_LAMBDA_ROLE_ARN + ); + assert.strictEqual( + json.Description, + `This Cloudformation template sets up the roles needed to import data into Workload Discovery on AWS. (SO0075b) - Solution - Import Account Template (uksb-1r0720e57) (version:9.9.9)` + ); + }); + }); + + describe('getRegionalTemplate', () => { + it('should incorporate account id and region into global template', async () => { + const actual = await handler({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'getRegionalTemplate', + }, + }); + const json = yamlParse(actual); + assert.strictEqual( + json.Parameters.WorkloadDiscoveryAccountId.Default, + ACCOUNT_ID + ); + assert.strictEqual( + json.Parameters.AggregationRegion.Default, + REGION + ); + assert.strictEqual( + json.Description, + `This CloudFormation template sets up AWS Config so that it will start collecting resource information for the region Workload Discovery on AWS will discover. (SO0075c) - Solution - Import Region Template (uksb-1r0720e5f) (version:${SOLUTION_VERSION})` + ); + }); + }); + + describe('unknown field', () => { + it('should reject payloads with unknown query', async () => { + const actual = await handler({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'foo', + }, + }).catch(err => + assert.strictEqual( + err.message, + 'Unknown field, unable to resolve foo.' + ) + ); + }); + }); + }); +}); diff --git a/source/backend/functions/account-import-templates-api/vitest.config.mjs b/source/backend/functions/account-import-templates-api/vitest.config.mjs new file mode 100644 index 00000000..62a28848 --- /dev/null +++ b/source/backend/functions/account-import-templates-api/vitest.config.mjs @@ -0,0 +1,15 @@ +import {defineConfig} from 'vite'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: [ + ['lcov', {projectRoot: '../../../..'}], + ['html'], + ['text'], + ['json'], + ], + }, + }, +}); diff --git a/source/backend/functions/cur-notification/src/cfn-response.mjs b/source/backend/functions/cur-notification/src/cfn-response.mjs new file mode 100644 index 00000000..c318f8a3 --- /dev/null +++ b/source/backend/functions/cur-notification/src/cfn-response.mjs @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import https from 'https'; +import url from 'url'; + +export const SUCCESS = 'SUCCESS'; +export const FAILED = 'FAILED'; + +export function send( + event, + context, + responseStatus, + responseData, + physicalResourceId, + noEcho +) { + const responseBody = JSON.stringify({ + Status: responseStatus, + Reason: + 'See the details in CloudWatch Log Stream: ' + + context.logStreamName, + PhysicalResourceId: physicalResourceId || context.logStreamName, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: noEcho || false, + Data: responseData, + }); + + console.log('Response body:\n', responseBody); + + const parsedUrl = url.parse(event.ResponseURL); + const options = { + hostname: parsedUrl.hostname, + port: 443, + path: parsedUrl.path, + method: 'PUT', + headers: { + 'content-type': '', + 'content-length': responseBody.length, + }, + }; + + const request = https.request(options, function (response) { + console.log('Status code: ' + response.statusCode); + console.log('Status message: ' + response.statusMessage); + context.done(); + }); + + request.on('error', function (error) { + console.log('send(..) failed executing https.request(..): ' + error); + context.done(); + }); + + request.write(responseBody); + request.end(); +} diff --git a/source/backend/functions/cur-notification/src/index.mjs b/source/backend/functions/cur-notification/src/index.mjs new file mode 100644 index 00000000..3e2283a2 --- /dev/null +++ b/source/backend/functions/cur-notification/src/index.mjs @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {S3} from '@aws-sdk/client-s3'; +import * as response from './cfn-response.mjs'; + +const s3 = new S3(); + +export function handler(event, context, callback) { + const putConfigRequest = function (notificationConfiguration) { + return new Promise(function (resolve, reject) { + s3.putBucketNotificationConfiguration( + { + Bucket: event.ResourceProperties.BucketName, + NotificationConfiguration: notificationConfiguration, + }, + function (err, data) { + if (err) + reject({ + msg: this.httpResponse.body.toString(), + error: err, + data: data, + }); + else resolve(data); + } + ); + }); + }; + const newNotificationConfig = {}; + if (event.RequestType !== 'Delete') { + newNotificationConfig.LambdaFunctionConfigurations = [ + { + Events: ['s3:ObjectCreated:*'], + LambdaFunctionArn: + event.ResourceProperties.TargetLambdaArn || 'missing arn', + Filter: { + Key: { + FilterRules: [ + {Name: 'prefix', Value: 'aws-perspective'}, + {Name: 'suffix', Value: '.snappy.parquet'}, + ], + }, + }, + }, + ]; + } + putConfigRequest(newNotificationConfig) + .then(function (result) { + response.send(event, context, response.SUCCESS, result); + callback(null, result); + }) + .catch(function (error) { + response.send(event, context, response.FAILED, error); + console.log(error); + callback(error); + }); +} diff --git a/source/backend/functions/cur-setup/src/cfn-response.mjs b/source/backend/functions/cur-setup/src/cfn-response.mjs new file mode 100644 index 00000000..2283c883 --- /dev/null +++ b/source/backend/functions/cur-setup/src/cfn-response.mjs @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import https from 'https'; +import url from 'url'; + +export const SUCCESS = 'SUCCESS'; +export const FAILED = 'FAILED'; + +export function send( + event, + context, + responseStatus, + responseData, + physicalResourceId, + noEcho +) { + var responseBody = JSON.stringify({ + Status: responseStatus, + Reason: + 'See the details in CloudWatch Log Stream: ' + + context.logStreamName, + PhysicalResourceId: physicalResourceId || context.logStreamName, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + NoEcho: noEcho || false, + Data: responseData, + }); + + console.log('Response body:\n', responseBody); + + var parsedUrl = url.parse(event.ResponseURL); + var options = { + hostname: parsedUrl.hostname, + port: 443, + path: parsedUrl.path, + method: 'PUT', + headers: { + 'content-type': '', + 'content-length': responseBody.length, + }, + }; + + var request = https.request(options, function (response) { + console.log('Status code: ' + response.statusCode); + console.log('Status message: ' + response.statusMessage); + context.done(); + }); + + request.on('error', function (error) { + console.log('send(..) failed executing https.request(..): ' + error); + context.done(); + }); + + request.write(responseBody); + request.end(); +} diff --git a/source/backend/functions/cur-setup/src/index.mjs b/source/backend/functions/cur-setup/src/index.mjs new file mode 100644 index 00000000..5e556289 --- /dev/null +++ b/source/backend/functions/cur-setup/src/index.mjs @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {S3} from '@aws-sdk/client-s3'; +import {Glue} from '@aws-sdk/client-glue'; +import * as response from './cfn-response.mjs'; +import * as R from 'ramda'; + +const s3 = new S3(); + +const {CURCrawlerKey: cURCrawlerKey} = process.env; + +export function handler(event, context, callback) { + if (event.RequestType === 'Delete') { + response.send(event, context, response.SUCCESS); + } else { + if (event.Records) { + R.forEach(record => { + console.log(JSON.stringify(record)); + console.log( + `Downloading from ${record.s3.bucket.name}/${record.s3.object.key}` + ); + const year = decodeURIComponent( + R.split('/', record.s3.object.key)[3] + ); + const month = decodeURIComponent( + R.split('/', record.s3.object.key)[4] + ); + const name = R.last(R.split('/', record.s3.object.key)); + console.log(`Name is ${name}`); + console.log(`Month is ${month}`); + console.log(`Year is ${year}`); + console.log( + `Uploading to ${record.s3.bucket.name}/${cURCrawlerKey}` + ); + if (R.endsWith('.parquet', name)) { + var params = { + Bucket: record.s3.bucket.name, + CopySource: `${record.s3.bucket.name}/${record.s3.object.key}`, + Key: `${cURCrawlerKey}/${year}/${month}/${name}`, + }; + s3.copyObject(params, function (err, data) { + if (err) console.error(err, err.stack); + // an error occurred + else console.log('CUR Copied successfully'); // successful response + }); + } + }, event.Records); + } + + const glue = new Glue(); + glue.startCrawler( + {Name: 'AWSCURCrawler-aws-perspective-cost-and-usage'}, + function (err, data) { + if (err) { + const responseData = JSON.parse(this.httpResponse.body); + if (responseData['__type'] == 'CrawlerRunningException') { + callback(null, responseData.Message); + } else { + const responseString = JSON.stringify(responseData); + if (event.ResponseURL) { + response.send(event, context, response.FAILED, { + msg: responseString, + }); + } else { + callback(responseString); + } + } + } else { + if (event.ResponseURL) { + response.send(event, context, response.SUCCESS); + } else { + callback(null, response.SUCCESS); + } + } + } + ); + } +} diff --git a/source/backend/functions/graph-api/src/index.mjs b/source/backend/functions/graph-api/src/index.mjs new file mode 100644 index 00000000..c56e9b0d --- /dev/null +++ b/source/backend/functions/graph-api/src/index.mjs @@ -0,0 +1,297 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import gremlin from 'gremlin'; +import * as R from 'ramda'; +import {Logger} from '@aws-lambda-powertools/logger'; +import {create as createGremlinClient} from 'neptune-lambda-client'; + +const __ = gremlin.process.statics; +const c = gremlin.process.column; +const p = gremlin.process.P; +const {local} = gremlin.process.scope; +const { + cardinality: {single}, + t, +} = gremlin.process; + +const gremlinClient = createGremlinClient( + process.env.neptuneConnectURL, + process.env.neptunePort +); + +const logger = new Logger({serviceName: 'WdGraphApi'}); + +function getResourceGraph({query}, {ids, pagination: {start, end}}) { + return query(async g => { + if (R.isEmpty(ids)) { + return {nodes: [], edges: []}; + } + + return g + .with_('Neptune#enableResultCacheWithTTL', 30) + .V(...ids) + .aggregate('nodes') + .bothE() + .aggregate('edges') + .otherV() + .aggregate('nodes') + .outE( + 'IS_CONTAINED_IN_VPC', + 'IS_ASSOCIATED_WITH_VPC', + 'IS_CONTAINED_IN_SUBNET', + 'IS_ASSOCIATED_WITH_SUBNET' + ) + .aggregate('edges') + .inV() + .aggregate('nodes') + .cap('nodes', 'edges') + .fold() + .select('nodes', 'edges') + .by( + __.unfold().dedup().elementMap().fold().range(local, start, end) + ) + .by( + __.unfold() + .dedup() + .project('id', 'label', 'target', 'source') + .by(t.id) + .by(t.label) + .by(__.inV()) + .by(__.outV()) + .fold() + .range(local, start, end) + ) + .next() + .then(x => x.value) + .then(({nodes, edges}) => { + return { + edges, + nodes: nodes.map(({id, label, md5Hash, ...properties}) => { + return { + id, + label, + md5Hash, + properties, + }; + }), + }; + }); + }); +} + +function createAccountPredicates(accounts) { + return accounts.map(({accountId, regions}) => { + return regions == null + ? __.has('accountId', accountId) + : __.has('accountId', accountId).has( + 'awsRegion', + p.within(R.pluck('name', regions)) + ); + }); +} + +function getResources( + {query}, + {resourceTypes = [], accounts = [], pagination: {start, end}} +) { + return query(async g => { + let q = g.with_('Neptune#enableResultCacheWithTTL', 60).V(); + + if (!R.isEmpty(resourceTypes)) + q = q.hasLabel(...resourceTypes.map(R.replace(/::/g, '_'))); + + if (!R.isEmpty(accounts)) { + q = q.or(...createAccountPredicates(accounts)); + } + + return q + .range(start, end) + .elementMap() + .toList() + .then( + R.map(({id, label, md5Hash, ...properties}) => { + return { + id, + label: label.replace(/_/g, '::'), + md5Hash, + properties, + }; + }) + ); + }); +} + +function addResources({query}, resources) { + return query(async g => { + return g + .inject(resources) + .unfold() + .as('nodes') + .addV(__.select('nodes').select('label')) + .as('v') + .property(t.id, __.select('nodes').select('id')) + .property('md5Hash', __.select('nodes').select('md5Hash')) + .select('nodes') + .select('properties') + .unfold() + .as('kv') + .select('v') + .property(__.select('kv').by(c.keys), __.select('kv').by(c.values)) + .toList(); + }); +} + +function updateResources({query}, resources) { + return query(async g => { + return resources + .reduce((q, {id, md5Hash, properties}) => { + return Object.entries(properties).reduce( + (acc, [k, v]) => { + acc.property(single, k, v); + return acc; + }, + q.V(id).property(single, 'md5Hash', md5Hash) + ); + }, g) + .next() + .then(() => resources.map(R.pick(['id']))); + }); +} + +function addRelationships({query}, relationships) { + return query(async g => { + if (R.isEmpty(relationships)) return []; + + return relationships + .reduce((q, {source, label, target}) => { + return q + .V(source) + .addE(label) + .to(__.V(target)) + .project('id', 'label', 'target', 'source') + .by(t.id) + .by(t.label) + .by(__.inV()) + .by(__.outV()) + .aggregate('edges'); + }, g) + .select('edges') + .next() + .then(x => x.value); + }); +} + +function getRelationships({query}, {pagination: {start, end}}) { + return query(async g => { + return g + .with_('Neptune#enableResultCacheWithTTL', 60) + .E() + .range(start, end) + .project('id', 'label', 'target', 'source') + .by(t.id) + .by(t.label) + .by(__.inV()) + .by(__.outV()) + .toList(); + }); +} + +function deleteRelationships({query}, relationshipIds) { + return query(async g => { + return g + .E(...relationshipIds) + .drop() + .next() + .then(() => relationshipIds); + }); +} + +function deleteAllResources({query}) { + return query(async g => { + return g.V().drop().next(); + }); +} + +function deleteResources({query}, resourceIds) { + return query(async g => { + return g + .V(...resourceIds) + .drop() + .next() + .then(() => resourceIds); + }); +} + +const isArn = R.test(/arn:(aws|aws-cn|aws-us-gov|aws-iso|aws-iso-b):.*/); +const MAX_PAGE_SIZE = 2500; + +export function _handler(gremlinClient) { + return async (event, context) => { + const fieldName = event.info.fieldName; + + const {username} = event.identity; + logger.info(`User ${username} invoked the ${fieldName} operation.`); + + const args = event.arguments; + logger.info( + 'GraphQL arguments:', + {arguments: args, operation: fieldName} + ); + + const pagination = args?.pagination ?? {start: 0, end: 1000}; + + if (pagination.end - pagination.start > MAX_PAGE_SIZE) { + return Promise.reject( + new Error(`Maximum page size is ${MAX_PAGE_SIZE}.`) + ); + } + + switch (fieldName) { + case 'addRelationships': + return addRelationships(gremlinClient, args.relationships); + case 'deleteRelationships': + return deleteRelationships(gremlinClient, args.relationshipIds); + case 'getRelationships': + return getRelationships(gremlinClient, {pagination}); + case 'addResources': + return addResources(gremlinClient, args.resources); + case 'deleteAllResources': + return deleteAllResources(gremlinClient); + case 'deleteResources': + if (R.isEmpty(args.resourceIds)) return []; + return deleteResources(gremlinClient, args.resourceIds); + case 'getResources': + if (R.isEmpty(args.resourceTypes)) return []; + const resourceTypes = args.resourceTypes ?? []; + const accounts = args.accounts ?? []; + return getResources(gremlinClient, { + pagination, + resourceTypes, + accounts, + }); + case 'getResourceGraph': + const invalidArns = R.filter(id => !isArn(id), args.ids); + if (!R.isEmpty(invalidArns)) { + logger.error('Invalid ARNs provided. ', {invalidArns}); + throw new Error( + 'The following ARNs are invalid: ' + invalidArns + ); + } + return getResourceGraph(gremlinClient, { + ids: args.ids, + pagination, + }); + case 'updateResources': + return updateResources(gremlinClient, args.resources); + default: + return Promise.reject( + new Error( + `Unknown field, unable to resolve ${fieldName}.` + ) + ); + } + }; +} + +export const handler = _handler(gremlinClient); diff --git a/source/backend/functions/graph-api/src/logger.mjs b/source/backend/functions/graph-api/src/logger.mjs new file mode 100644 index 00000000..29b9b124 --- /dev/null +++ b/source/backend/functions/graph-api/src/logger.mjs @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import pino from 'pino'; +import {pinoLambdaDestination} from 'pino-lambda'; + +const level = ('info' ?? process.env.LOG_LEVEL).toLowerCase(); + +const destination = pinoLambdaDestination(); +export const logger = pino({level}, destination); diff --git a/source/backend/functions/graph-api/test/index.test.mjs b/source/backend/functions/graph-api/test/index.test.mjs new file mode 100644 index 00000000..01cb3b99 --- /dev/null +++ b/source/backend/functions/graph-api/test/index.test.mjs @@ -0,0 +1,532 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import sinon from 'sinon'; +import {assert, describe, it} from 'vitest'; +import {_handler} from '../src/index.mjs'; +import getResourcesInput from './fixtures/getResources/lambdas-input.json' with {type: 'json'}; +import getResourcesOutput from './fixtures/getResources/lambdas-output.json' with {type: 'json'}; + +describe('index.js', () => { + describe('handler', () => { + function createMockGremlinClient({ + nextValues = [], + nextValue, + toListValue, + }) { + const nextValuesStub = sinon.stub(); + + nextValues.forEach((value, i) => + nextValuesStub.onCall(i).resolves({value}) + ); + + const g = { + E: sinon.stub().returnsThis(), + V: sinon.stub().returnsThis(), + with_: sinon.stub().returnsThis(), + aggregate: sinon.stub().returnsThis(), + cap: sinon.stub().returnsThis(), + addE: sinon.stub().returnsThis(), + by: sinon.stub().returnsThis(), + both: sinon.stub().returnsThis(), + bothE: sinon.stub().returnsThis(), + or: sinon.stub().returnsThis(), + outE: sinon.stub().returnsThis(), + inV: sinon.stub().returnsThis(), + otherV: sinon.stub().returnsThis(), + to: sinon.stub().returnsThis(), + has: sinon.stub().returnsThis(), + hasLabel: sinon.stub().returnsThis(), + fold: sinon.stub().returnsThis(), + unfold: sinon.stub().returnsThis(), + group: sinon.stub().returnsThis(), + property: sinon.stub().returnsThis(), + groupCount: sinon.stub().returnsThis(), + select: sinon.stub().returnsThis(), + range: sinon.stub().returnsThis(), + elementMap: sinon.stub().returnsThis(), + project: sinon.stub().returnsThis(), + drop: sinon.stub().returnsThis(), + next: + nextValues.length === 0 + ? sinon.stub().resolves({value: nextValue}) + : nextValuesStub, + toList: sinon.stub().resolves(toListValue), + }; + + return { + query: f => f(g), + g, + }; + } + + describe('getResources', () => { + it('should return no resources when resourceTypes is empty', async () => { + const mockGremlinClient = createMockGremlinClient({ + toListValue: {}, + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'getResources', + }, + identity: {username: 'testUser'}, + arguments: { + resourceTypes: [], + }, + }, + {} + ); + + assert.deepEqual(actual, []); + }); + + it('should get resources', async () => { + const mockGremlinClient = createMockGremlinClient({ + toListValue: getResourcesInput, + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'getResources', + }, + identity: {username: 'testUser'}, + arguments: {}, + }, + {} + ); + + assert.deepEqual(actual, getResourcesOutput); + }); + + it('should get resources with accounts and resource filters', async () => { + const mockGremlinClient = createMockGremlinClient({ + toListValue: getResourcesInput, + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'getResources', + }, + identity: {username: 'testUser'}, + arguments: { + resourceTypes: ['AWS::Lambda::Function'], + accounts: [ + { + accountId: 'accountId', + regions: ['eu-west-1'], + }, + ], + }, + }, + {} + ); + + assert.deepEqual(actual, getResourcesOutput); + }); + }); + + describe('deleteResources', () => { + it('should handle empty list of ids', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: {}, + }); + + const ids = []; + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'deleteResources', + }, + identity: {username: 'testUser'}, + arguments: { + resourceIds: ids, + }, + }, + {} + ); + + assert.notStrictEqual(actual, ids); + assert.deepEqual(actual, ids); + }); + + it('should return ids of deleted relationships', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: {}, + }); + + const ids = ['id1', 'id2', 'id3']; + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'deleteResources', + }, + identity: {username: 'testUser'}, + arguments: { + resourceIds: ids, + }, + }, + {} + ); + + assert.deepEqual(actual, ids); + }); + }); + + describe('deleteRelationships', () => { + it('should return ids of deleted relationships', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: {}, + }); + + const ids = ['id1', 'id2', 'id3']; + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'deleteRelationships', + }, + identity: {username: 'testUser'}, + arguments: { + relationshipIds: ids, + }, + }, + {} + ); + + assert.deepEqual(actual, ids); + }); + }); + + describe('addRelationships', () => { + it('should handle empty relationships field', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: {}, + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'addRelationships', + }, + identity: {username: 'testUser'}, + arguments: { + relationships: [], + }, + }, + {} + ); + + assert.deepEqual(actual, []); + }); + + it('should extract value on resolution', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: 'relResult', + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'addRelationships', + }, + identity: {username: 'testUser'}, + arguments: { + relationships: [ + { + source: 'sourceArn', + label: 'CONTAINS', + target: 'targetArn', + }, + ], + }, + }, + {} + ); + + assert.deepEqual(actual, 'relResult'); + }); + }); + + describe('getRelationships', () => { + it('ensure caching is enabled', async () => { + const mockGremlinClient = createMockGremlinClient({ + toListValue: ['toListValue'], + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'getRelationships', + }, + identity: {username: 'testUser'}, + arguments: {}, + }, + {} + ); + + sinon.assert.calledWith( + mockGremlinClient.g.with_, + 'Neptune#enableResultCacheWithTTL' + ); + assert.deepEqual(actual, ['toListValue']); + }); + }); + + describe('updateResources', () => { + it('should return ids after updating resources', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: {}, + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'updateResources', + }, + identity: {username: 'testUser'}, + arguments: { + resources: [ + { + id: 'arn1', + md5Hash: 'hash', + properties: {a: 1}, + }, + {id: 'arn2', md5Hash: '', properties: {b: 2}}, + ], + }, + }, + {} + ); + + assert.deepEqual(actual, [{id: 'arn1'}, {id: 'arn2'}]); + }); + }); + + describe('getResourceGraph', () => { + it('should reject invalid arns', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValues: [], + }); + + return _handler(mockGremlinClient)( + { + info: { + fieldName: 'getResourceGraph', + }, + identity: {username: 'testUser'}, + arguments: { + ids: ['notArn1', 'notArn2'], + }, + }, + {} + ).catch(err => + assert.deepEqual( + err.message, + 'The following ARNs are invalid: notArn1,notArn2' + ) + ); + }); + + it('should handle empty list of ids', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValues: ['should not be returned'], + }); + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'getResourceGraph', + }, + identity: {username: 'testUser'}, + arguments: { + ids: [], + }, + }, + {} + ); + + assert.deepEqual(actual, {nodes: [], edges: []}); + }); + + it('should return nodes and edges related to the supplied ids', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: { + nodes: [ + { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function1', + label: 'AWS_LAMBDA_FUNCTION', + md5Hash: '', + prop1: 'prop1Val', + prop2: 'prop2Val', + }, + { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function2', + label: 'AWS_LAMBDA_FUNCTION', + md5Hash: '', + prop1: 'prop1Val', + prop2: 'prop2Val', + }, + { + id: 'iamRoleArn', + label: 'AWS_IAM_ROLE', + md5Hash: '', + prop1: 'prop1IamVal', + prop2: 'prop2IamVal', + }, + ], + edges: [ + { + id: 'edgeId1', + label: 'CONTAINED_IN', + source: { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function1', + label: 'AWS_LAMBDA_FUNCTION', + }, + target: {id: 'vpcArn', label: 'AWS_EC2_VPC'}, + }, + { + id: 'edgeId2', + label: 'IS_ASSOCIATED_WITH', + source: { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function1', + label: 'AWS_LAMBDA_FUNCTION', + }, + target: { + id: 'iamRoleArn', + label: 'AWS_IAM_ROLE', + }, + }, + ], + }, + }); + + const ids = [ + 'arn:aws:lambda:eu-west-1:xxxxxx:function:function1', + 'arn:aws:lambda:eu-west-1:xxxxxx:function:function2', + ]; + + const actual = await _handler(mockGremlinClient)( + { + info: { + fieldName: 'getResourceGraph', + }, + identity: {username: 'testUser'}, + arguments: { + ids, + }, + }, + {} + ); + + assert.deepEqual(actual.edges, [ + { + id: 'edgeId1', + label: 'CONTAINED_IN', + source: { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function1', + label: 'AWS_LAMBDA_FUNCTION', + }, + target: {id: 'vpcArn', label: 'AWS_EC2_VPC'}, + }, + { + id: 'edgeId2', + label: 'IS_ASSOCIATED_WITH', + source: { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function1', + label: 'AWS_LAMBDA_FUNCTION', + }, + target: {id: 'iamRoleArn', label: 'AWS_IAM_ROLE'}, + }, + ]); + + assert.deepEqual(actual.nodes, [ + { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function1', + label: 'AWS_LAMBDA_FUNCTION', + md5Hash: '', + properties: { + prop1: 'prop1Val', + prop2: 'prop2Val', + }, + }, + { + id: 'arn:aws:lambda:eu-west-1:xxxxxx:function:function2', + label: 'AWS_LAMBDA_FUNCTION', + md5Hash: '', + properties: { + prop1: 'prop1Val', + prop2: 'prop2Val', + }, + }, + { + id: 'iamRoleArn', + label: 'AWS_IAM_ROLE', + md5Hash: '', + properties: { + prop1: 'prop1IamVal', + prop2: 'prop2IamVal', + }, + }, + ]); + }); + }); + + describe('unknown query', () => { + it('should reject payloads with unknown query', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: {}, + }); + + return _handler(mockGremlinClient)( + { + identity: {username: 'testUser'}, + info: { + fieldName: 'foo', + }, + }, + {} + ).catch(err => + assert.strictEqual( + err.message, + 'Unknown field, unable to resolve foo.' + ) + ); + }); + }); + + describe('max page', () => { + it('should reject payloads with page size greater than 2500', async () => { + const mockGremlinClient = createMockGremlinClient({ + nextValue: {}, + }); + + return _handler(mockGremlinClient)( + { + arguments: { + pagination: { + start: 0, + end: 3000, + }, + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResources', + }, + }, + {} + ).catch(err => + assert.strictEqual( + err.message, + 'Maximum page size is 2500.' + ) + ); + }); + }); + }); +}); diff --git a/source/backend/functions/graph-api/vitest.config.mjs b/source/backend/functions/graph-api/vitest.config.mjs new file mode 100644 index 00000000..62a28848 --- /dev/null +++ b/source/backend/functions/graph-api/vitest.config.mjs @@ -0,0 +1,15 @@ +import {defineConfig} from 'vite'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: [ + ['lcov', {projectRoot: '../../../..'}], + ['html'], + ['text'], + ['json'], + ], + }, + }, +}); diff --git a/source/backend/functions/identity-provider/Pipfile b/source/backend/functions/identity-provider/Pipfile new file mode 100644 index 00000000..2ea14502 --- /dev/null +++ b/source/backend/functions/identity-provider/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +moto = "5.0.5" +boto3 = "1.34.91" +pytest-cov = "5.0.0" +crhelper = "2.0.11" +mock = "5.1.0" +pytest-mock = "3.14.0" +joserfc = "0.9.0" +aws-lambda-powertools = "3.1.0" + +[requires] +python_version = "3.12" diff --git a/source/backend/functions/identity-provider/Pipfile.lock b/source/backend/functions/identity-provider/Pipfile.lock new file mode 100644 index 00000000..684f919c --- /dev/null +++ b/source/backend/functions/identity-provider/Pipfile.lock @@ -0,0 +1,658 @@ +{ + "_meta": { + "hash": { + "sha256": "60d923b80de40bca93fe6362a4e7697055254ca6e83d8264a797901ed3caf44f" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "aws-lambda-powertools": { + "hashes": [ + "sha256:758a8e5d668ae759051d064d542decff777d9c7a0a5612f0c05ab78fb6f20365", + "sha256:fdc834678d131e230052ccd684f969be417ce0165d65ee35c053e1a966e46e4c" + ], + "index": "pypi", + "markers": "python_version >= '3.8' and python_full_version < '4.0.0'", + "version": "==3.1.0" + }, + "boto3": { + "hashes": [ + "sha256:5077917041adaaae15eeca340289547ef905ca7e11516e9bd22d394fb5057d2a", + "sha256:97fac686c47647db4b44e4789317e4aeecd38511d71e84f8d20abe33eb630ff1" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.34.91" + }, + "botocore": { + "hashes": [ + "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be", + "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3" + ], + "markers": "python_version >= '3.8'", + "version": "==1.34.162" + }, + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.4.0" + }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433", + "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529", + "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671", + "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e", + "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42", + "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99", + "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327", + "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8", + "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06", + "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874", + "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4", + "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354", + "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1", + "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab", + "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3", + "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b", + "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37", + "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd", + "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f", + "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b", + "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c", + "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b", + "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7", + "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3", + "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808", + "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a", + "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76", + "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469", + "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55", + "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289", + "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc", + "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13", + "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2", + "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30", + "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163", + "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d", + "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c", + "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1", + "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c", + "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2", + "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3", + "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314", + "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0", + "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384", + "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb", + "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c", + "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45", + "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a", + "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24", + "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8", + "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec", + "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56", + "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777", + "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b", + "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f", + "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a", + "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d", + "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9", + "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413", + "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c", + "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b", + "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c" + ], + "markers": "python_version >= '3.9'", + "version": "==7.6.7" + }, + "crhelper": { + "hashes": [ + "sha256:0c1f703a830722379d205d58ca4f0da768c0b10670ddce46af31ba9661bf2d5a", + "sha256:da9efe4fb57d86f0567fddc999ae1c242ea9602c95b165b09e00d435c3845ef0" + ], + "index": "pypi", + "version": "==2.0.11" + }, + "cryptography": { + "hashes": [ + "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", + "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", + "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", + "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", + "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", + "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", + "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", + "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", + "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", + "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", + "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", + "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", + "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", + "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", + "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", + "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", + "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", + "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", + "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", + "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", + "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" + ], + "markers": "python_version >= '3.7'", + "version": "==43.0.3" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "joserfc": { + "hashes": [ + "sha256:4026bdbe2c196cd40574e916fa1e28874d99649412edaab0e373dec3077153fb", + "sha256:eebca7f587b1761ce43a98ffd5327f2b600b9aa5bb0a77b947687f503ad43bc0" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.9.0" + }, + "markupsafe": { + "hashes": [ + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "mock": { + "hashes": [ + "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", + "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==5.1.0" + }, + "moto": { + "hashes": [ + "sha256:2eaca2df7758f6868df420bf0725cd0b93d98709606f1fb8b2343b5bdc822d91", + "sha256:4ecdd4084491a2f25f7a7925416dcf07eee0031ce724957439a32ef764b22874" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.5" + }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "markers": "python_version >= '3.8'", + "version": "==8.3.3" + }, + "pytest-cov": { + "hashes": [ + "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", + "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.0" + }, + "pytest-mock": { + "hashes": [ + "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", + "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.14.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.9.0.post0" + }, + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "responses": { + "hashes": [ + "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb", + "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba" + ], + "markers": "python_version >= '3.8'", + "version": "==0.25.3" + }, + "s3transfer": { + "hashes": [ + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.3" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.10'", + "version": "==2.2.3" + }, + "werkzeug": { + "hashes": [ + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" + ], + "markers": "python_version >= '3.9'", + "version": "==3.1.3" + }, + "xmltodict": { + "hashes": [ + "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", + "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac" + ], + "markers": "python_version >= '3.6'", + "version": "==0.14.2" + } + } +} diff --git a/source/backend/functions/identity-provider/identity_provider.py b/source/backend/functions/identity-provider/identity_provider.py new file mode 100644 index 00000000..8aa59b6b --- /dev/null +++ b/source/backend/functions/identity-provider/identity_provider.py @@ -0,0 +1,106 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import json +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities import parameters +from crhelper import CfnResource +from typing import TypedDict, Optional + + +logger = Logger(service='IdentityProviderCustomResource') + + +helper = CfnResource(json_logging=False, log_level='INFO', + boto_level='CRITICAL') + +cognito_client = boto3.client('cognito-idp') + +ssm_provider = parameters.SecretsProvider() + + +class IdentityProviderProperties(TypedDict): + UserPoolId: str + ProviderName: str + ProviderType: str + ProviderDetails: dict + AttributeMapping: str + IdpIdentifiers: Optional[list[str]] + + +class Event(TypedDict): + RequestType: str + ResponseURL: str + StackId: str + RequestId: str + ResourceType: str + LogicalResourceId: str + ResourceProperties: IdentityProviderProperties + + +@helper.create +def create(event: Event, _) -> None: + logger.info('Creating identity provider') + props: IdentityProviderProperties = event['ResourceProperties'] + provider_name = props['ProviderName'] + client_secret_arn = props['ClientSecretArn'] + attribute_mappings = json.loads(props['AttributeMapping']) + + client_secret = ssm_provider.get(client_secret_arn) + + resp = cognito_client.create_identity_provider( + UserPoolId=props['UserPoolId'], + ProviderName=provider_name, + ProviderType=props['ProviderType'], + ProviderDetails=props['ProviderDetails'] | {'client_secret': client_secret}, + AttributeMapping=attribute_mappings, + IdpIdentifiers=props['IdpIdentifiers'] + ) + + logger.info('Identity provider created') + logger.info(resp['IdentityProvider']) + + helper.Data.update({'ProviderName': provider_name}) + + +@helper.update +def update(event: Event, _) -> None: + logger.info('Updating identity provider') + props: IdentityProviderProperties = event['ResourceProperties'] + provider_name = props['ProviderName'] + client_secret_arn = props['ClientSecretArn'] + attribute_mappings = json.loads(props['AttributeMapping']) + + client_secret = ssm_provider.get(client_secret_arn) + + resp = cognito_client.update_identity_provider( + UserPoolId=props['UserPoolId'], + ProviderName=provider_name, + ProviderDetails=props['ProviderDetails'] | {'client_secret': client_secret}, + AttributeMapping=attribute_mappings, + IdpIdentifiers=props['IdpIdentifiers'] + ) + + logger.info('Identity provider updated.') + logger.info(resp['IdentityProvider']) + + helper.Data.update({'ProviderName': provider_name}) + + +@helper.delete +def delete(event: Event, _) -> None: + logger.info('Deleting identity provider') + props: IdentityProviderProperties = event['ResourceProperties'] + + cognito_client.delete_identity_provider( + UserPoolId=props['UserPoolId'], + ProviderName=props['ProviderName'] + ) + + logger.info('Identity provider deleted.') + + +@logger.inject_lambda_context +def handler(event, _) -> None: + helper(event, _) \ No newline at end of file diff --git a/source/backend/functions/identity-provider/test_identity_provider.py b/source/backend/functions/identity-provider/test_identity_provider.py new file mode 100644 index 00000000..43dfd5c0 --- /dev/null +++ b/source/backend/functions/identity-provider/test_identity_provider.py @@ -0,0 +1,229 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import os +import pytest +import crhelper + +from moto import mock_aws +from mock import patch, MagicMock + + +class MockContext(object): + function_name: str = 'test-function' + ms_remaining: int = 9000 + memory_limit_in_mb: int = 128 + invoked_function_arn: str = 'arn:aws:lambda:eu-west-1:123456789012:function:test' + aws_request_id: str = '52fdfc07-2182-154f-163f-5f0f9a621d72' + + # crhelper depends on this method in the Lambda Context + @staticmethod + def get_remaining_time_in_millis(): + return MockContext.ms_remaining + + +# crhelper will hang without mocking this +@pytest.fixture(autouse=True) +def mocked_send_response(mocker): + + real_send = crhelper.CfnResource._send + + _send_response = mocker.Mock() + + def mocked_send(self, status=None, reason='', send_response=_send_response): + real_send(self, status, reason, send_response) + + crhelper.CfnResource._send = mocked_send + + yield _send_response + + crhelper.CfnResource._send = real_send + + +@pytest.fixture +def identity_provider(): + with patch.dict(os.environ, { + 'AWS_DEFAULT_REGION': 'eu-west-1', + 'AWS_ACCESS_KEY_ID': 'access_key', + 'AWS_SECRET_ACCESS_KEY': 'secret_access_key' + }): + import identity_provider + yield identity_provider + +@pytest.fixture +def mocked_cognito_client(): + with mock_aws(): + with patch.dict(os.environ, { + 'AWS_DEFAULT_REGION': 'eu-west-1', + 'AWS_ACCESS_KEY_ID': 'access_key', + 'AWS_SECRET_ACCESS_KEY': 'secret_access_key' + }): + cognito_client = boto3.client('cognito-idp') + yield cognito_client + +@pytest.fixture +def mocked_secrets_manager_client(): + with mock_aws(): + with patch.dict(os.environ, { + 'AWS_DEFAULT_REGION': 'eu-west-1', + 'AWS_ACCESS_KEY_ID': 'access_key', + 'AWS_SECRET_ACCESS_KEY': 'secret_access_key' + }): + secrets_manager_client = boto3.client('secretsmanager') + yield secrets_manager_client + + +def test_handler_creates_identity_provider(mocked_cognito_client, mocked_secrets_manager_client, identity_provider): + create_user_pool_resp = mocked_cognito_client.create_user_pool(PoolName='pool_name') + + user_pool_id = create_user_pool_resp['UserPool']['Id'] + provider_name = 'OidcProvider' + + client_secret = 'my_secret' + + secret_manager_resp = mocked_secrets_manager_client.create_secret( + Name='CreateTestSecret', SecretString=client_secret + ) + + client_secret_arn = secret_manager_resp['ARN'] + + identity_provider.handler({ + 'RequestType': 'Create', + 'ResponseURL' : 'http://pre-signed-S3-url-for-response', + 'StackId' : 'arn:aws:cloudformation:us-west-2:123456789012:stack/stack-name/guid', + 'RequestId' : 'unique id for this create request', + 'ResourceType' : 'Custom::UserPoolIdentityProvider', + 'LogicalResourceId' : 'MyTestResource', + 'ResourceProperties': { + 'UserPoolId': user_pool_id, + 'ProviderName': provider_name, + 'ProviderType': 'OIDC', + 'ClientSecretArn': client_secret_arn, + 'ProviderDetails': { + 'client_id': 'client_id' + }, + 'AttributeMapping': '{"email": "email", "given_name": "given_name"}', + 'IdpIdentifiers': ['IdpIdentifier'] + } + }, MockContext) + + assert identity_provider.helper.Data == {'ProviderName': 'OidcProvider'} + + resp = mocked_cognito_client.describe_identity_provider( + UserPoolId=user_pool_id, + ProviderName='OidcProvider' + ) + + moto_identity_provider = resp['IdentityProvider'] + + assert moto_identity_provider['UserPoolId'] == user_pool_id + assert moto_identity_provider['ProviderType'] == 'OIDC' + assert moto_identity_provider['ProviderName'] == provider_name + assert moto_identity_provider['ProviderDetails'] == {'client_id': 'client_id', 'client_secret': client_secret} + assert moto_identity_provider['AttributeMapping'] == {'email': 'email', 'given_name': 'given_name'} + assert moto_identity_provider['IdpIdentifiers'] == ['IdpIdentifier'] + + +def test_handler_updates_identity_provider(mocked_cognito_client, mocked_secrets_manager_client, identity_provider): + create_user_pool_resp = mocked_cognito_client.create_user_pool(PoolName='pool_name') + + user_pool_id = create_user_pool_resp['UserPool']['Id'] + + provider_name = 'OidcProviderUpdate' + + mocked_cognito_client.create_identity_provider(UserPoolId=user_pool_id, + ProviderName=provider_name, + ProviderType='OIDC', + ProviderDetails={ + 'client_id': 'client_id' + }) + + provider_name = 'OidcProviderUpdate' + + client_secret = 'my_secret' + + secret_manager_resp = mocked_secrets_manager_client.create_secret( + Name='UpdateTestSecret', SecretString=client_secret + ) + + client_secret_arn = secret_manager_resp['ARN'] + + identity_provider.handler({ + 'RequestType': 'Update', + 'ResponseURL' : 'http://pre-signed-S3-url-for-response', + 'StackId' : 'arn:aws:cloudformation:us-west-2:123456789012:stack/stack-name/guid', + 'RequestId' : 'unique id for this create request', + 'ResourceType' : 'Custom::UserPoolIdentityProvider', + 'LogicalResourceId' : 'MyTestResource', + 'ResourceProperties': { + 'UserPoolId': user_pool_id, + 'ProviderName': provider_name, + 'ProviderType': 'OIDC', + 'ClientSecretArn': client_secret_arn, + 'ProviderDetails': { + 'oidc_issuer': 'oidc_issuer' + }, + 'AttributeMapping': '{"given_name": "given_name"}', + 'IdpIdentifiers': ['IdpIdentifierUpdate'] + } + }, MockContext) + + assert identity_provider.helper.Data == {'ProviderName': 'OidcProviderUpdate'} + + resp = mocked_cognito_client.describe_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name + ) + + moto_identity_provider = resp['IdentityProvider'] + + assert moto_identity_provider['UserPoolId'] == user_pool_id + assert moto_identity_provider['ProviderType'] == 'OIDC' + assert moto_identity_provider['ProviderName'] == provider_name + assert moto_identity_provider['ProviderDetails'] == {'oidc_issuer': 'oidc_issuer', 'client_secret': client_secret} + assert moto_identity_provider['AttributeMapping'] == {'given_name': 'given_name'} + assert moto_identity_provider['IdpIdentifiers'] == ['IdpIdentifierUpdate'] + + +def test_handler_deletes_identity_provider(mocked_cognito_client, identity_provider): + + create_user_pool_resp = mocked_cognito_client.create_user_pool(PoolName='pool_name') + + user_pool_id = create_user_pool_resp['UserPool']['Id'] + + provider_name = 'OidcProviderUpdate' + + mocked_cognito_client.create_identity_provider(UserPoolId=user_pool_id, + ProviderName=provider_name, + ProviderType='OIDC', + ProviderDetails={ + 'client_id': 'client_id' + }) + + provider_name = 'OidcProviderUpdate' + + identity_provider.handler({ + 'RequestType': 'Delete', + 'ResponseURL' : 'http://pre-signed-S3-url-for-response', + 'StackId' : 'arn:aws:cloudformation:us-west-2:123456789012:stack/stack-name/guid', + 'RequestId' : 'unique id for this create request', + 'ResourceType' : 'Custom::UserPoolIdentityProvider', + 'LogicalResourceId' : 'MyTestResource', + 'ResourceProperties': { + 'UserPoolId': user_pool_id, + 'ProviderName': provider_name, + 'ProviderType': 'OIDC', + 'ProviderDetails': { + 'oidc_issuer': 'oidc_issuer' + }, + 'AttributeMapping': '{"given_name": "given_name"}', + 'IdpIdentifiers': ['IdpIdentifierUpdate'] + } + }, MockContext) + + with pytest.raises(mocked_cognito_client.exceptions.ResourceNotFoundException): + mocked_cognito_client.describe_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name + ) diff --git a/source/backend/functions/metrics-subscription-filter/package-lock.json b/source/backend/functions/metrics-subscription-filter/package-lock.json new file mode 100644 index 00000000..0aef6974 --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/package-lock.json @@ -0,0 +1,6669 @@ +{ + "name": "metrics-subscription-filter", + "version": "2.2.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "metrics-subscription-filter", + "version": "2.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-lambda-powertools/logger": "2.1.1", + "ramda": "0.30.1", + "zod": "3.23.8" + }, + "devDependencies": { + "@vitest/coverage-v8": "^2.1.1", + "chai": "^4.3.10", + "msw": "2.3.1", + "rewire": "^7.0.0", + "sinon": "^18.0.0", + "vitest": "^2.1.1" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-lambda-powertools/commons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/commons/-/commons-2.1.1.tgz", + "integrity": "sha512-QlvZLVJM4yXlO6mpYlYwWGaLCZTJg8WfsIH8/eT061n4BdBljW/VHMj59sHp/IljQn8HE/VdHKYHqM6vPJjYJw==" + }, + "node_modules/@aws-lambda-powertools/logger": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/logger/-/logger-2.1.1.tgz", + "integrity": "sha512-OB/ycDef8VD4OpGZcte2dxkdNWQCSxjRu8OJ2nuizh7auqZMI5LX8PYoIBEBRQC138CeJBtKKCwjSi+NOFMY1w==", + "dependencies": { + "@aws-lambda-powertools/commons": "^2.1.1", + "lodash.merge": "^4.6.2" + }, + "peerDependencies": { + "@middy/core": ">=3.x" + }, + "peerDependenciesMeta": { + "@middy/core": { + "optional": true + } + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "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.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "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/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.9.tgz", + "integrity": "sha512-UF09aejxCi4Xqm6N/jJAiFXArXfi9al52AFaSD+2uIHnhZGtd1d6lIGTRMPouVSJxbGEi+HkOWSYaiEY/+szUw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^8.2.2", + "@inquirer/type": "^1.3.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.2.2.tgz", + "integrity": "sha512-K8SuNX45jEFlX3EBJpu9B+S2TISzMPGXZIuJ9ME924SqbdW6Pt6fIkKvXg7mOEOKJ4WxpQsxj0UTfcL/A434Ww==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.3", + "@inquirer/type": "^1.3.3", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.12.13", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.3.tgz", + "integrity": "sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.3.tgz", + "integrity": "sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mswjs/cookies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz", + "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", + "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.6", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.11", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/expect/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/@vitest/expect/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@vitest/expect/node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/@vitest/expect/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.1", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.51.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "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-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "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.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.1.tgz", + "integrity": "sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@inquirer/confirm": "^3.0.0", + "@mswjs/cookies": "^1.1.0", + "@mswjs/interceptors": "^0.29.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.7.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "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/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, + "node_modules/ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", + "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rewire": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rewire/-/rewire-7.0.0.tgz", + "integrity": "sha512-DyyNyzwMtGYgu0Zl/ya0PR/oaunM+VuCuBxCuhYJHHaV0V+YvYa3bBGxb5OZ71vndgmp1pYY8F4YOwQo1siRGw==", + "dev": true, + "dependencies": { + "eslint": "^8.47.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "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.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@mswjs/interceptors": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.8.tgz", + "integrity": "sha512-PFfqpHplKa7KMdoQdj5td03uG05VK2Ng1dG0sP4pT9h0dGSX2v9txYt/AnrzPb/vAmfyBBC0NQV7VaBEX+efgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/vitest/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/vitest/node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/vitest/node_modules/msw": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.9.tgz", + "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^3.0.0", + "@mswjs/interceptors": "^0.35.8", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.3.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@aws-lambda-powertools/commons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/commons/-/commons-2.1.1.tgz", + "integrity": "sha512-QlvZLVJM4yXlO6mpYlYwWGaLCZTJg8WfsIH8/eT061n4BdBljW/VHMj59sHp/IljQn8HE/VdHKYHqM6vPJjYJw==" + }, + "@aws-lambda-powertools/logger": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/logger/-/logger-2.1.1.tgz", + "integrity": "sha512-OB/ycDef8VD4OpGZcte2dxkdNWQCSxjRu8OJ2nuizh7auqZMI5LX8PYoIBEBRQC138CeJBtKKCwjSi+NOFMY1w==", + "requires": { + "@aws-lambda-powertools/commons": "^2.1.1", + "lodash.merge": "^4.6.2" + } + }, + "@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true + }, + "@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "requires": { + "cookie": "^0.7.2" + } + }, + "@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "requires": { + "statuses": "^2.0.1" + } + }, + "@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "@inquirer/confirm": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.9.tgz", + "integrity": "sha512-UF09aejxCi4Xqm6N/jJAiFXArXfi9al52AFaSD+2uIHnhZGtd1d6lIGTRMPouVSJxbGEi+HkOWSYaiEY/+szUw==", + "dev": true, + "requires": { + "@inquirer/core": "^8.2.2", + "@inquirer/type": "^1.3.3" + } + }, + "@inquirer/core": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.2.2.tgz", + "integrity": "sha512-K8SuNX45jEFlX3EBJpu9B+S2TISzMPGXZIuJ9ME924SqbdW6Pt6fIkKvXg7mOEOKJ4WxpQsxj0UTfcL/A434Ww==", + "dev": true, + "requires": { + "@inquirer/figures": "^1.0.3", + "@inquirer/type": "^1.3.3", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.12.13", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "@inquirer/figures": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.3.tgz", + "integrity": "sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==", + "dev": true + }, + "@inquirer/type": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.3.tgz", + "integrity": "sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@mswjs/cookies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz", + "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==", + "dev": true + }, + "@mswjs/interceptors": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", + "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "dev": true, + "requires": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.5.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "requires": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "dev": true, + "optional": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, + "@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "optional": true, + "peer": true + }, + "@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "@vitest/coverage-v8": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.6", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.11", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/expect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "dev": true, + "requires": { + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "dependencies": { + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, + "chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "requires": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + } + }, + "check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true + }, + "deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true + }, + "loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true + } + } + }, + "@vitest/pretty-format": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "dev": true, + "requires": { + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/runner": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "dev": true, + "requires": { + "@vitest/utils": "2.1.1", + "pathe": "^1.1.2" + } + }, + "@vitest/snapshot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + } + }, + "@vitest/spy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "dev": true, + "requires": { + "tinyspy": "^3.0.0" + } + }, + "@vitest/utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.1.1", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "dependencies": { + "loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + } + } + }, + "acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, + "cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true + }, + "cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.51.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "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" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "requires": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "msw": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.1.tgz", + "integrity": "sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==", + "dev": true, + "requires": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@inquirer/confirm": "^3.0.0", + "@mswjs/cookies": "^1.1.0", + "@mswjs/interceptors": "^0.29.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + } + }, + "mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true + }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@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" + } + }, + "outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, + "path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, + "pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "optional": true, + "peer": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "optional": true, + "peer": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", + "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "optional": true, + "peer": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rewire": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rewire/-/rewire-7.0.0.tgz", + "integrity": "sha512-DyyNyzwMtGYgu0Zl/ya0PR/oaunM+VuCuBxCuhYJHHaV0V+YvYa3bBGxb5OZ71vndgmp1pYY8F4YOwQo1siRGw==", + "dev": true, + "requires": { + "eslint": "^8.47.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "@types/estree": "1.0.5", + "fsevents": "~2.3.2" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "dependencies": { + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, + "std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "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, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "dependencies": { + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true + }, + "tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true + }, + "tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true + }, + "tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "optional": true, + "peer": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "vite": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "dev": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + } + }, + "vite-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + } + }, + "vitest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "dev": true, + "requires": { + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.1", + "why-is-node-running": "^2.3.0" + }, + "dependencies": { + "@mswjs/interceptors": { + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.8.tgz", + "integrity": "sha512-PFfqpHplKa7KMdoQdj5td03uG05VK2Ng1dG0sP4pT9h0dGSX2v9txYt/AnrzPb/vAmfyBBC0NQV7VaBEX+efgQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + } + }, + "@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "requires": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + } + }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, + "chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "requires": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + } + }, + "check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true + }, + "deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true + }, + "loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "msw": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.9.tgz", + "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^3.0.0", + "@mswjs/interceptors": "^0.35.8", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.3.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + } + }, + "pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true + } + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==" + } + } +} diff --git a/source/backend/functions/metrics-subscription-filter/package.json b/source/backend/functions/metrics-subscription-filter/package.json new file mode 100644 index 00000000..3a97872d --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/package.json @@ -0,0 +1,34 @@ +{ + "name": "metrics-subscription-filter", + "version": "2.2.0", + "description": "Lambda function used to handle operational metrics subscription filter", + "main": "index.mjs", + "scripts": { + "pretest": "npm i", + "test": "vitest run --coverage", + "pretest:ci": "npm ci", + "test:ci": "vitest run --coverage --allowOnly false", + "clean": "rm -rf dist", + "build:zip": "zip -rq --exclude=test/* --exclude=package-lock.json metrics-subscription-filter.zip node_modules/ && zip -urj metrics-subscription-filter.zip src/", + "build:dist": "mkdir dist && mv metrics-subscription-filter.zip dist/", + "build": "npm run clean && npm ci --omit=dev && npm run build:zip && npm run build:dist" + }, + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com/solutions" + }, + "devDependencies": { + "@vitest/coverage-v8": "^2.1.1", + "chai": "^4.3.10", + "msw": "2.3.1", + "rewire": "^7.0.0", + "sinon": "^18.0.0", + "vitest": "^2.1.1" + }, + "dependencies": { + "@aws-lambda-powertools/logger": "2.1.1", + "ramda": "0.30.1", + "zod": "3.23.8" + } +} diff --git a/source/backend/functions/metrics-subscription-filter/src/index.mjs b/source/backend/functions/metrics-subscription-filter/src/index.mjs new file mode 100644 index 00000000..d2fa6e5c --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/src/index.mjs @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {promisify} from 'node:util'; +import zlib from 'node:zlib'; +import {Logger} from '@aws-lambda-powertools/logger'; +import * as R from 'ramda'; +import {z} from 'zod'; + +const gunzip = promisify(zlib.gunzip); + +const logger = new Logger({serviceName: 'WdMyApplicationLogSubscription'}); + +const envSchema = z.object({ + METRICS_URL: z.string().url(), + METRICS_UUID: z.string().uuid(), + SOLUTION_ID: z.string(), + SOLUTION_VERSION: z.string(), +}); + +async function post(url, options, payload) { + const res = await fetch(url, { + ...options, + method: 'POST', + body: JSON.stringify(payload), + }); + + const body = await res.json(); + + if (!res.ok) { + logger.error(`Error sending post request to ${url}`, {body}); + throw new Error(`Http error ${res.status} received from server`); + } + + return body; +} + +function createMetricPayload( + {metricsUuid, solutionId, solutionVersion}, + {type, ...metric} +) { + return { + event_name: type, + solution: solutionId, + timestamp: new Date().toISOString().replace('T', ' ').substring(0, 21), + uuid: metricsUuid, + version: solutionVersion, + context_version: '1', + context: metric, + }; +} + +export function _handler(env) { + return async event => { + const { + METRICS_URL: metricsUrl, + METRICS_UUID: metricsUuid, + SOLUTION_ID: solutionId, + SOLUTION_VERSION: solutionVersion, + } = envSchema.parse(env); + + const payload = Buffer.from(event.awslogs.data, 'base64'); + + const unzipped = await gunzip(payload); + + const {logEvents = []} = JSON.parse(unzipped.toString()); + + logger.info('Log events parsed successfully', {logEvents}); + + return Promise.resolve(logEvents) + .then( + R.map(logEvent => { + const {metricEvent} = JSON.parse(logEvent.message); + return createMetricPayload( + {metricsUuid, solutionId, solutionVersion}, + metricEvent + ); + }) + ) + .then( + R.map(payload => { + return post( + metricsUrl, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + payload + ); + }) + ) + .then(ps => Promise.allSettled(ps)) + .then(results => { + const [rawFailures, rawSuccesses] = R.partition( + res => res.status === 'rejected', + results + ); + + const failures = rawFailures.map(x => x.reason); + + logger.info( + `There were ${failures.length} errors sending metrics.` + ); + + if (!R.isEmpty(failures)) { + logger.error('Errors:', {errors: failures}); + } + + return {failures, successes: rawSuccesses.map(x => x.value)}; + }); + }; +} + +export const handler = _handler(process.env); diff --git a/source/backend/functions/metrics-subscription-filter/test/contants.mjs b/source/backend/functions/metrics-subscription-filter/test/contants.mjs new file mode 100644 index 00000000..b60c3ae6 --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/test/contants.mjs @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const METRICS_URL = 'https://metrics.awssolutionsbuilder.com/generic'; + +export const METRICS_UUID = 'e88870c0-b832-439e-ad77-d414308150f4'; + +export const SOLUTION_ID = 'SO0075'; + +export const SOLUTION_VERSION = 'v0.0.0'; diff --git a/source/backend/functions/metrics-subscription-filter/test/index.test.mjs b/source/backend/functions/metrics-subscription-filter/test/index.test.mjs new file mode 100644 index 00000000..b67356db --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/test/index.test.mjs @@ -0,0 +1,182 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import zlib from 'node:zlib'; +import sinon from 'sinon'; +import {afterAll, afterEach, describe, it, beforeAll, beforeEach} from 'vitest'; +import {assert} from 'chai'; +import {_handler} from '../src/index.mjs'; +import {server} from './mocks/node.mjs'; +import { + METRICS_URL, + METRICS_UUID, + SOLUTION_ID, + SOLUTION_VERSION, +} from './contants.mjs'; +import {promisify} from 'node:util'; + +const gzip = promisify(zlib.gzip); + +describe('index.js', () => { + beforeAll(() => { + const mockedDate = new Date('2024-01-01'); + sinon.useFakeTimers(mockedDate); + server.listen(); + }); + + beforeEach(() => { + sinon.reset(); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => server.close()); + + const env = { + METRICS_URL, + METRICS_UUID, + SOLUTION_ID, + SOLUTION_VERSION, + }; + + describe('handler', () => { + it('should send multiple operational metrics to the metrics endpoint', async () => { + const eventData = { + logEvents: [ + { + message: JSON.stringify({ + metricEvent: { + type: 'ApplicationCreatedHappyPath1', + resourceCount: 2, + unprocessedResourceCount: 0, + regions: ['eu-west-1', 'us-east-1'], + }, + }), + }, + { + message: JSON.stringify({ + metricEvent: { + type: 'ApplicationCreatedHappyPath2', + resourceCount: 10, + unprocessedResourceCount: 10, + regions: ['eu-west-1', 'us-east-1'], + }, + }), + }, + ], + }; + + const zipped = await gzip(JSON.stringify(eventData)); + + const data = zipped.toString('base64'); + + const {failures, successes} = await _handler(env)({ + awslogs: { + data, + }, + }); + + assert.lengthOf(failures, 0); + + const happyPath1Expected = successes.find( + x => x.event_name === 'ApplicationCreatedHappyPath1' + ); + const happyPath2Expected = successes.find( + x => x.event_name === 'ApplicationCreatedHappyPath2' + ); + + assert.deepEqual(happyPath1Expected, { + event_name: 'ApplicationCreatedHappyPath1', + solution: 'SO0075', + timestamp: '2024-01-01 00:00:00.0', + uuid: 'e88870c0-b832-439e-ad77-d414308150f4', + version: SOLUTION_VERSION, + context_version: '1', + context: { + resourceCount: 2, + unprocessedResourceCount: 0, + regions: ['eu-west-1', 'us-east-1'], + }, + }); + + assert.deepEqual(happyPath2Expected, { + event_name: 'ApplicationCreatedHappyPath2', + solution: 'SO0075', + timestamp: '2024-01-01 00:00:00.0', + uuid: 'e88870c0-b832-439e-ad77-d414308150f4', + version: SOLUTION_VERSION, + context_version: '1', + context: { + resourceCount: 10, + unprocessedResourceCount: 10, + regions: ['eu-west-1', 'us-east-1'], + }, + }); + }); + + it('should handle partial failure when sending multiple operational metrics to the metrics endpoint', async () => { + const eventData = { + logEvents: [ + { + message: JSON.stringify({ + metricEvent: { + type: 'ApplicationCreatedSuccess', + resourceCount: 2, + unprocessedResourceCount: 0, + regions: ['eu-west-1', 'us-east-1'], + }, + }), + }, + { + message: JSON.stringify({ + metricEvent: { + type: 'ApplicationCreatedFailure', + resourceCount: 10, + unprocessedResourceCount: 10, + regions: ['eu-west-1', 'us-east-1'], + }, + }), + }, + ], + }; + + const zipped = await gzip(JSON.stringify(eventData)); + + const data = zipped.toString('base64'); + + const {failures, successes} = await _handler(env)({ + awslogs: { + data, + }, + }); + + assert.lengthOf(successes, 1); + assert.lengthOf(failures, 1); + + const successExpected = successes.find( + x => x.event_name === 'ApplicationCreatedSuccess' + ); + const failureExpected = failures.find( + x => x.message === 'Http error 400 received from server' + ); + + assert.instanceOf(failureExpected, Error); + + assert.deepEqual(successExpected, { + event_name: 'ApplicationCreatedSuccess', + solution: 'SO0075', + timestamp: '2024-01-01 00:00:00.0', + uuid: 'e88870c0-b832-439e-ad77-d414308150f4', + version: SOLUTION_VERSION, + context_version: '1', + context: { + resourceCount: 2, + unprocessedResourceCount: 0, + regions: ['eu-west-1', 'us-east-1'], + }, + }); + }); + }); +}); diff --git a/source/backend/functions/metrics-subscription-filter/test/mocks/handlers.mjs b/source/backend/functions/metrics-subscription-filter/test/mocks/handlers.mjs new file mode 100644 index 00000000..bf5107de --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/test/mocks/handlers.mjs @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {http, HttpResponse} from 'msw'; +import {METRICS_URL} from '../contants.mjs'; + +export const handlers = [ + http.post(METRICS_URL, async ({request}) => { + const json = await request.clone().json(); + + if (json.event_name === 'ApplicationCreatedFailure') { + return HttpResponse.json(json, {status: 400}); + } + + return HttpResponse.json(json, {status: 200}); + }), +]; diff --git a/source/backend/functions/metrics-subscription-filter/test/mocks/node.mjs b/source/backend/functions/metrics-subscription-filter/test/mocks/node.mjs new file mode 100644 index 00000000..024faf31 --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/test/mocks/node.mjs @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {setupServer} from 'msw/node'; +import {handlers} from './handlers.mjs'; + +export const server = setupServer(...handlers); diff --git a/source/backend/functions/metrics-subscription-filter/vitest.config.mjs b/source/backend/functions/metrics-subscription-filter/vitest.config.mjs new file mode 100644 index 00000000..62a28848 --- /dev/null +++ b/source/backend/functions/metrics-subscription-filter/vitest.config.mjs @@ -0,0 +1,15 @@ +import {defineConfig} from 'vite'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: [ + ['lcov', {projectRoot: '../../../..'}], + ['html'], + ['text'], + ['json'], + ], + }, + }, +}); diff --git a/source/backend/functions/metrics-uuid/Pipfile b/source/backend/functions/metrics-uuid/Pipfile new file mode 100644 index 00000000..fcecef89 --- /dev/null +++ b/source/backend/functions/metrics-uuid/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +moto = "5.0.9" +boto3 = "1.34.118" +pytest-cov = "5.0.0" +crhelper = "2.0.11" +mock = "5.1.0" +pytest-mock = "3.14.0" +joserfc = "0.11.1" +aws-lambda-powertools = "3.2.0" + +[requires] +python_version = "3.12" \ No newline at end of file diff --git a/source/backend/functions/metrics-uuid/Pipfile.lock b/source/backend/functions/metrics-uuid/Pipfile.lock new file mode 100644 index 00000000..17a7c7c2 --- /dev/null +++ b/source/backend/functions/metrics-uuid/Pipfile.lock @@ -0,0 +1,658 @@ +{ + "_meta": { + "hash": { + "sha256": "ef71688b1eb88ded032a643493d581713b278ec8ff55d6462394c9b12267d609" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "aws-lambda-powertools": { + "hashes": [ + "sha256:abef10817c247ac656e12107f9665a06e88089f0fe2b255d1c37fab54c7e767a", + "sha256:bc0affecdf73365cae919db5bab7decc236421279ee6c2f272edbefa4e3ce546" + ], + "index": "pypi", + "markers": "python_version >= '3.8' and python_full_version < '4.0.0'", + "version": "==3.2.0" + }, + "boto3": { + "hashes": [ + "sha256:4eb8019421cb664a6fcbbee6152aa95a28ce8bbc1c4ee263871c09cdd58bf8ee", + "sha256:e9edaf979fbe59737e158f2f0f3f0861ff1d61233f18f6be8ebb483905f24587" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.34.118" + }, + "botocore": { + "hashes": [ + "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be", + "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3" + ], + "markers": "python_version >= '3.8'", + "version": "==1.34.162" + }, + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.1" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.4.0" + }, + "coverage": { + "extras": [ + "toml" + ], + "hashes": [ + "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376", + "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", + "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111", + "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", + "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", + "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", + "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", + "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", + "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", + "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c", + "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", + "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", + "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", + "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0", + "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", + "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", + "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", + "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", + "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", + "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", + "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", + "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", + "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", + "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", + "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", + "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", + "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", + "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", + "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", + "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901", + "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", + "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", + "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", + "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", + "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", + "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", + "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", + "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", + "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3", + "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", + "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076", + "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", + "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", + "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", + "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", + "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", + "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", + "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09", + "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", + "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", + "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f", + "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", + "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", + "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", + "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", + "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", + "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", + "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", + "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", + "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", + "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", + "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858" + ], + "markers": "python_version >= '3.9'", + "version": "==7.6.4" + }, + "crhelper": { + "hashes": [ + "sha256:0c1f703a830722379d205d58ca4f0da768c0b10670ddce46af31ba9661bf2d5a", + "sha256:da9efe4fb57d86f0567fddc999ae1c242ea9602c95b165b09e00d435c3845ef0" + ], + "index": "pypi", + "version": "==2.0.11" + }, + "cryptography": { + "hashes": [ + "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", + "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", + "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", + "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", + "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", + "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", + "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", + "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", + "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", + "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", + "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", + "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", + "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", + "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", + "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", + "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", + "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", + "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", + "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", + "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", + "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" + ], + "markers": "python_version >= '3.7'", + "version": "==43.0.3" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "joserfc": { + "hashes": [ + "sha256:229e7e06b1ae4df88c3c7174f5848457b63e7b27a6a968b81dfd0988b8a3fbce", + "sha256:d1151cdf9a64241b8cb46e7d67c5bfba10aecf364ef53b3a9109e90e8a621dca" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.11.1" + }, + "markupsafe": { + "hashes": [ + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "mock": { + "hashes": [ + "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", + "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==5.1.0" + }, + "moto": { + "hashes": [ + "sha256:21a13e02f83d6a18cfcd99949c96abb2e889f4bd51c4c6a3ecc8b78765cb854e", + "sha256:eb71f1cba01c70fff1f16086acb24d6d9aeb32830d646d8989f98a29aeae24ba" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.9" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "markers": "python_version >= '3.8'", + "version": "==8.3.3" + }, + "pytest-cov": { + "hashes": [ + "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", + "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.0" + }, + "pytest-mock": { + "hashes": [ + "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", + "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.14.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "responses": { + "hashes": [ + "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb", + "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba" + ], + "markers": "python_version >= '3.8'", + "version": "==0.25.3" + }, + "s3transfer": { + "hashes": [ + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.3" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.10'", + "version": "==2.2.3" + }, + "werkzeug": { + "hashes": [ + "sha256:4f7d1a5de312c810a8a2c6f0b47e9f6a7cffb7c8322def35e4d4d9841ff85597", + "sha256:f471a4cd167233077e9d2a8190c3471c5bc520c636a9e3c1e9300c33bced03bc" + ], + "markers": "python_version >= '3.9'", + "version": "==3.1.2" + }, + "xmltodict": { + "hashes": [ + "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", + "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac" + ], + "markers": "python_version >= '3.6'", + "version": "==0.14.2" + } + } +} diff --git a/source/backend/functions/metrics-uuid/metrics_uuid.py b/source/backend/functions/metrics-uuid/metrics_uuid.py new file mode 100644 index 00000000..e945d8e7 --- /dev/null +++ b/source/backend/functions/metrics-uuid/metrics_uuid.py @@ -0,0 +1,54 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +from uuid import uuid4 +from aws_lambda_powertools import Logger +from crhelper import CfnResource +from typing import TypedDict, Dict + + +logger = Logger(service='MetricsUuidCustomResource') + + +helper = CfnResource(json_logging=False, log_level='INFO', + boto_level='CRITICAL') + +ssm_client = boto3.client('ssm') + +metrics_parameter_name = '/Solutions/WorkloadDiscovery/anonymous_metrics_uuid' + + +class Event(TypedDict): + RequestType: str + ResponseURL: str + StackId: str + RequestId: str + ResourceType: str + LogicalResourceId: str + ResourceProperties: Dict + + +@helper.create +@helper.update +def create(event: Event, _) -> None: + logger.info('Creating metrics uuid') + + try: + get_resp = ssm_client.get_parameter(Name=metrics_parameter_name) + logger.info('Metrics uuid already exists') + helper.Data.update({'MetricsUuid': get_resp['Parameter']['Value']}) + except ssm_client.exceptions.ParameterNotFound: + uuid = str(uuid4()) + ssm_client.put_parameter( + Name=metrics_parameter_name, + Description='Unique Id for anonymous metrics collection', + Value=uuid, + Type='String' + ) + logger.info(f'Metrics uuid created: {uuid}') + helper.Data.update({'MetricsUuid': uuid}) + + +def handler(event, _) -> None: + helper(event, _) \ No newline at end of file diff --git a/source/backend/functions/metrics-uuid/test_metrics_uuid.py b/source/backend/functions/metrics-uuid/test_metrics_uuid.py new file mode 100644 index 00000000..16650f4a --- /dev/null +++ b/source/backend/functions/metrics-uuid/test_metrics_uuid.py @@ -0,0 +1,106 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import boto3 +import os +import pytest +import crhelper + +from moto import mock_aws +from mock import patch +from uuid import uuid4 + + +# crhelper depends on this method in the Lambda Context +class MockContext(object): + function_name = 'test-function' + ms_remaining = 9000 + + @staticmethod + def get_remaining_time_in_millis(): + return MockContext.ms_remaining + + +# crhelper will hang without mocking this +@pytest.fixture(autouse=True) +def mocked_send_response(mocker): + + real_send = crhelper.CfnResource._send + + _send_response = mocker.Mock() + + def mocked_send(self, status=None, reason='', send_response=_send_response): + real_send(self, status, reason, send_response) + + crhelper.CfnResource._send = mocked_send + + yield _send_response + + crhelper.CfnResource._send = real_send + + +@pytest.fixture(autouse=True) +def mocked_aws_env_vars(): + with patch.dict(os.environ, { + 'AWS_DEFAULT_REGION': 'eu-west-1', + 'AWS_ACCESS_KEY_ID': 'mocked', + 'AWS_SECRET_ACCESS_KEY': 'mocked', + 'AWS_SECURITY_TOKEN': 'mocked', + 'AWS_SESSION_TOKEN': 'mocked', + }): + yield + + +@pytest.fixture(autouse=True) +def mocked_ssm_client(): + with mock_aws(): + ssm_client = boto3.client('ssm') + yield ssm_client + + +@pytest.fixture(autouse=True) +def metrics_uuid(mocked_aws_env_vars): + import metrics_uuid + yield metrics_uuid + + +@pytest.mark.parametrize('request_type', ['Create', 'Update']) +def test_handler_creates_new_uid_if_one_does_not_exist(request_type, mocked_ssm_client, metrics_uuid): + metrics_uuid.handler({ + 'RequestType': request_type, + 'ResponseURL' : 'http://pre-signed-S3-url-for-response', + 'StackId' : 'arn:aws:cloudformation:us-west-2:123456789012:stack/stack-name/guid', + 'RequestId' : 'unique id for this create request', + 'ResourceType' : 'Custom::MetricsUuid', + 'LogicalResourceId' : 'MyTestResource', + 'ResourceProperties': {} + }, MockContext) + + uuid = mocked_ssm_client.get_parameter(Name=metrics_uuid.metrics_parameter_name)['Parameter']['Value'] + + assert metrics_uuid.helper.Data['MetricsUuid'] == uuid + + +@pytest.mark.parametrize('request_type', ['Create', 'Update']) +def test_handler_does_not_overwrite_uid_if_one_exists(request_type, mocked_ssm_client, metrics_uuid): + uuid = str(uuid4()) + + mocked_ssm_client.put_parameter( + Name=metrics_uuid.metrics_parameter_name, + Description='Unique Id for anonymous metrics collection', + Value=uuid, + Type='String' + ) + + metrics_uuid.handler({ + 'RequestType': request_type, + 'ResponseURL' : 'http://pre-signed-S3-url-for-response', + 'StackId' : 'arn:aws:cloudformation:us-west-2:123456789012:stack/stack-name/guid', + 'RequestId' : 'unique id for this create request', + 'ResourceType' : 'Custom::MetricsUuid', + 'LogicalResourceId' : 'MyTestResource', + 'ResourceProperties': {} + }, MockContext) + + assert mocked_ssm_client.get_parameter(Name=metrics_uuid.metrics_parameter_name)['Parameter']['Value'] == uuid + assert metrics_uuid.helper.Data['MetricsUuid'] == uuid diff --git a/source/backend/functions/metrics/test/constants.ts b/source/backend/functions/metrics/test/constants.ts new file mode 100644 index 00000000..b60c3ae6 --- /dev/null +++ b/source/backend/functions/metrics/test/constants.ts @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const METRICS_URL = 'https://metrics.awssolutionsbuilder.com/generic'; + +export const METRICS_UUID = 'e88870c0-b832-439e-ad77-d414308150f4'; + +export const SOLUTION_ID = 'SO0075'; + +export const SOLUTION_VERSION = 'v0.0.0'; diff --git a/source/backend/functions/metrics/test/mocks/handlers.ts b/source/backend/functions/metrics/test/mocks/handlers.ts new file mode 100644 index 00000000..ebc49594 --- /dev/null +++ b/source/backend/functions/metrics/test/mocks/handlers.ts @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import {graphql, http, HttpResponse} from 'msw'; +import {factory, primaryKey} from '@mswjs/data'; +import {METRICS_URL} from '../constants.js'; +import {AccountMetadata} from '../../src/index.js'; + +const db = factory({ + account: { + accountId: primaryKey(String), + count: Number, + }, +}); + +R.range(0, 100).forEach(i => { + db.account.create({ + accountId: String(i).padStart(12, '0'), + count: i * 100, + }); +}); + +db.account.create({ + accountId: 'aws', + count: 50, +}); + +export const handlers = [ + graphql.query('GetAccounts', () => { + return HttpResponse.json({ + data: { + getAccounts: db.account.getAll().map(({accountId}) => { + return { + accountId, + }; + }), + }, + }); + }), + graphql.query('GetResourcesAccountMetadata', ({variables}) => { + const {accounts} = variables; + const result = + accounts == null + ? db.account.getAll() + : db.account.findMany({ + where: { + accountId: { + in: accounts.map( + (x: AccountMetadata) => x.accountId + ), + }, + }, + }); + + return HttpResponse.json({ + data: { + getResourcesAccountMetadata: result, + }, + }); + }), + http.post(METRICS_URL, async ({request}) => { + return HttpResponse.json({}, {status: 200}); + }), +]; diff --git a/source/backend/functions/metrics/test/mocks/node.ts b/source/backend/functions/metrics/test/mocks/node.ts new file mode 100644 index 00000000..00b9e553 --- /dev/null +++ b/source/backend/functions/metrics/test/mocks/node.ts @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {setupServer} from 'msw/node'; +import {handlers} from './handlers.js'; + +export const server = setupServer(...handlers); diff --git a/source/backend/functions/metrics/vitest.config.ts b/source/backend/functions/metrics/vitest.config.ts new file mode 100644 index 00000000..62a28848 --- /dev/null +++ b/source/backend/functions/metrics/vitest.config.ts @@ -0,0 +1,15 @@ +import {defineConfig} from 'vite'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: [ + ['lcov', {projectRoot: '../../../..'}], + ['html'], + ['text'], + ['json'], + ], + }, + }, +}); diff --git a/source/backend/functions/myapplications/package-lock.json b/source/backend/functions/myapplications/package-lock.json new file mode 100644 index 00000000..ea903439 --- /dev/null +++ b/source/backend/functions/myapplications/package-lock.json @@ -0,0 +1,6057 @@ +{ + "name": "wd-export-myapplications", + "version": "2.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wd-export-myapplications", + "version": "2.2.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-lambda-powertools/logger": "2.1.1", + "@aws-sdk/client-resource-groups-tagging-api": "3.621.0", + "@aws-sdk/client-service-catalog-appregistry": "3.621.0", + "@aws-sdk/client-sts": "3.621.0", + "@aws-sdk/credential-providers": "3.624.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "ramda": "0.30.0", + "zod": "3.23.8" + }, + "devDependencies": { + "@aws-sdk/client-directory-service": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/types": "3.0.0", + "@types/aws-lambda": "^8.10.137", + "@types/node": "^20.12.12", + "@types/ramda": "^0.30.0", + "@vitest/coverage-v8": "^2.1.1", + "chai": "^4.4.1", + "rewire": "7.0.0", + "sinon": "^18.0.0", + "typescript": "^5.4.5", + "vitest": "^2.1.1" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-lambda-powertools/commons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/commons/-/commons-2.1.1.tgz", + "integrity": "sha512-QlvZLVJM4yXlO6mpYlYwWGaLCZTJg8WfsIH8/eT061n4BdBljW/VHMj59sHp/IljQn8HE/VdHKYHqM6vPJjYJw==" + }, + "node_modules/@aws-lambda-powertools/logger": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-lambda-powertools/logger/-/logger-2.1.1.tgz", + "integrity": "sha512-OB/ycDef8VD4OpGZcte2dxkdNWQCSxjRu8OJ2nuizh7auqZMI5LX8PYoIBEBRQC138CeJBtKKCwjSi+NOFMY1w==", + "dependencies": { + "@aws-lambda-powertools/commons": "^2.1.1", + "lodash.merge": "^4.6.2" + }, + "peerDependencies": { + "@middy/core": ">=3.x" + }, + "peerDependenciesMeta": { + "@middy/core": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.624.0.tgz", + "integrity": "sha512-imw3bNptHdhcogU3lwSVlQJsRpTxnkT4bQbchS/qX6+fF0Pk6ERZ+Q0YjzitPqTjkeyAWecUT4riyqv2djo+5w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/client-sts": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/client-sso": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.624.0.tgz", + "integrity": "sha512-EX6EF+rJzMPC5dcdsu40xSi2To7GSvdGQNIpe97pD9WvZwM9tRNQnNM4T6HA4gjV1L6Jwk8rBlG/CnveXtLEMw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.624.0.tgz", + "integrity": "sha512-Ki2uKYJKKtfHxxZsiMTOvJoVRP6b2pZ1u3rcUb2m/nVgBPUfLdl8ZkGpqE29I+t5/QaS/sEdbn6cgMUZwl+3Dg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.624.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/client-sts": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.624.0.tgz", + "integrity": "sha512-k36fLZCb2nfoV/DKK3jbRgO/Yf7/R80pgYfMiotkGjnZwDmRvNN08z4l06L9C+CieazzkgRxNUzyppsYcYsQaw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/core": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.624.0.tgz", + "integrity": "sha512-WyFmPbhRIvtWi7hBp8uSFy+iPpj8ccNV/eX86hwF4irMjfc/FtsGVIAeBXxXM/vGCjkdfEzOnl+tJ2XACD4OXg==", + "dependencies": { + "@smithy/core": "^2.3.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.622.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.622.0.tgz", + "integrity": "sha512-VUHbr24Oll1RK3WR8XLUugLpgK9ZuxEm/NVeVqyFts1Ck9gsKpRg1x4eH7L7tW3SJ4TDEQNMbD7/7J+eoL2svg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.624.0.tgz", + "integrity": "sha512-mMoNIy7MO2WTBbdqMyLpbt6SZpthE6e0GkRYpsd0yozPt0RZopcBhEh+HG1U9Y1PVODo+jcMk353vAi61CfnhQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.624.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.624.0.tgz", + "integrity": "sha512-vYyGK7oNpd81BdbH5IlmQ6zfaQqU+rPwsKTDDBeLRjshtrGXOEpfoahVpG9PX0ibu32IOWp4ZyXBNyVrnvcMOw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.624.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.624.0.tgz", + "integrity": "sha512-A02bayIjU9APEPKr3HudrFHEx0WfghoSPsPopckDkW7VBqO4wizzcxr75Q9A3vNX+cwg0wCN6UitTNe6pVlRaQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.624.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-directory-service": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-directory-service/-/client-directory-service-3.621.0.tgz", + "integrity": "sha512-auzZUs1sSDhD6Gjte1ia1oer++bsPjALxOF6UsWMo+29r/BEWG/3gFbVRlsACWeJU/lmmCCv7sMB4T/33IrG5Q==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.621.0", + "@aws-sdk/client-sts": "3.621.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-directory-service/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-resource-groups-tagging-api": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-resource-groups-tagging-api/-/client-resource-groups-tagging-api-3.621.0.tgz", + "integrity": "sha512-cCb/qSK53cjgSVOWZ1R38g2aom6fDDH1anMWQiSa4mdpXnBnP4/83/6PXTCXBBRG/gXKIMWm8WQfNmZHSir4iQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.621.0", + "@aws-sdk/client-sts": "3.621.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-resource-groups-tagging-api/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-service-catalog-appregistry": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-service-catalog-appregistry/-/client-service-catalog-appregistry-3.621.0.tgz", + "integrity": "sha512-AiqT3VNCOuG56MhZiG/UKFwgxKnz5FAOt/e2FSjf+Nt5Hefq9/4Whjf0uWByRYyI3IJhON2sVzbRz0pzyToeYA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.621.0", + "@aws-sdk/client-sts": "3.621.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-service-catalog-appregistry/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.621.0.tgz", + "integrity": "sha512-xpKfikN4u0BaUYZA9FGUMkkDmfoIP0Q03+A86WjqDWhcOoqNA1DkHsE4kZ+r064ifkPUfcNuUvlkVTEoBZoFjA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.621.0.tgz", + "integrity": "sha512-mMjk3mFUwV2Y68POf1BQMTF+F6qxt5tPu6daEUCNGC9Cenk3h2YXQQoS4/eSyYzuBiYk3vx49VgleRvdvkg8rg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.621.0.tgz", + "integrity": "sha512-707uiuReSt+nAx6d0c21xLjLm2lxeKc7padxjv92CIrIocnQSlJPxSCM7r5zBhwiahJA6MNQwmTl2xznU67KgA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.621.0", + "@aws-sdk/core": "3.621.0", + "@aws-sdk/credential-provider-node": "3.621.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.1", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.13", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.13", + "@smithy/util-defaults-mode-node": "^3.0.13", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.621.0.tgz", + "integrity": "sha512-CtOwWmDdEiINkGXD93iGfXjN0WmCp9l45cDWHHGa8lRgEDyhuL7bwd/pH5aSzj0j8SiQBG2k0S7DHbd5RaqvbQ==", + "dependencies": { + "@smithy/core": "^2.3.1", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/core/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.624.0.tgz", + "integrity": "sha512-gbXaxZP29yzMmEUzsGqUrHpKBnfMBtemvrlufJbaz/MGJNIa5qtJQp7n1LMI5R49DBVUN9s/e9Rf5liyMvlHiw==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.624.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.620.1.tgz", + "integrity": "sha512-ExuILJ2qLW5ZO+rgkNRj0xiAipKT16Rk77buvPP8csR7kkCflT/gXTyzRe/uzIiETTxM7tr8xuO9MP/DQXqkfg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.621.0.tgz", + "integrity": "sha512-/jc2tEsdkT1QQAI5Dvoci50DbSxtJrevemwFsm0B73pwCcOQZ5ZwwSdVqGsPutzYzUVx3bcXg3LRL7jLACqRIg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.11", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.621.0.tgz", + "integrity": "sha512-0EWVnSc+JQn5HLnF5Xv405M8n4zfdx9gyGdpnCmAmFqEDHA8LmBdxJdpUk1Ovp/I5oPANhjojxabIW5f1uU0RA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.621.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.621.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.621.0.tgz", + "integrity": "sha512-4JqpccUgz5Snanpt2+53hbOBbJQrSFq7E1sAAbgY6BKVQUsW5qyXqnjvSF32kDeKa5JpBl3bBWLZl04IadcPHw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.621.0", + "@aws-sdk/credential-provider-ini": "3.621.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.621.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.620.1.tgz", + "integrity": "sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.621.0.tgz", + "integrity": "sha512-Kza0jcFeA/GEL6xJlzR2KFf1PfZKMFnxfGzJzl5yN7EjoGdMijl34KaRyVnfRjnCWcsUpBWKNIDk9WZVMY9yiw==", + "dependencies": { + "@aws-sdk/client-sso": "3.621.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.621.0.tgz", + "integrity": "sha512-w7ASSyfNvcx7+bYGep3VBgC3K6vEdLmlpjT7nSIHxxQf+WSdvy+HynwJosrpZax0sK5q0D1Jpn/5q+r5lwwW6w==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.624.0.tgz", + "integrity": "sha512-SX+F5x/w8laQkhXLd1oww2lTuBDJSxzXWyxuOi25a9s4bMDs0V/wOj885Vr6h8QEGi3F8jZ8aWLwpsm2yuk9BA==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.624.0", + "@aws-sdk/client-sso": "3.624.0", + "@aws-sdk/client-sts": "3.624.0", + "@aws-sdk/credential-provider-cognito-identity": "3.624.0", + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sso": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.624.0.tgz", + "integrity": "sha512-EX6EF+rJzMPC5dcdsu40xSi2To7GSvdGQNIpe97pD9WvZwM9tRNQnNM4T6HA4gjV1L6Jwk8rBlG/CnveXtLEMw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.624.0.tgz", + "integrity": "sha512-Ki2uKYJKKtfHxxZsiMTOvJoVRP6b2pZ1u3rcUb2m/nVgBPUfLdl8ZkGpqE29I+t5/QaS/sEdbn6cgMUZwl+3Dg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.624.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/client-sts": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.624.0.tgz", + "integrity": "sha512-k36fLZCb2nfoV/DKK3jbRgO/Yf7/R80pgYfMiotkGjnZwDmRvNN08z4l06L9C+CieazzkgRxNUzyppsYcYsQaw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/core": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.624.0.tgz", + "integrity": "sha512-WyFmPbhRIvtWi7hBp8uSFy+iPpj8ccNV/eX86hwF4irMjfc/FtsGVIAeBXxXM/vGCjkdfEzOnl+tJ2XACD4OXg==", + "dependencies": { + "@smithy/core": "^2.3.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.622.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.622.0.tgz", + "integrity": "sha512-VUHbr24Oll1RK3WR8XLUugLpgK9ZuxEm/NVeVqyFts1Ck9gsKpRg1x4eH7L7tW3SJ4TDEQNMbD7/7J+eoL2svg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.624.0.tgz", + "integrity": "sha512-mMoNIy7MO2WTBbdqMyLpbt6SZpthE6e0GkRYpsd0yozPt0RZopcBhEh+HG1U9Y1PVODo+jcMk353vAi61CfnhQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.624.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.624.0.tgz", + "integrity": "sha512-vYyGK7oNpd81BdbH5IlmQ6zfaQqU+rPwsKTDDBeLRjshtrGXOEpfoahVpG9PX0ibu32IOWp4ZyXBNyVrnvcMOw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.624.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.624.0.tgz", + "integrity": "sha512-A02bayIjU9APEPKr3HudrFHEx0WfghoSPsPopckDkW7VBqO4wizzcxr75Q9A3vNX+cwg0wCN6UitTNe6pVlRaQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.624.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.620.0.tgz", + "integrity": "sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.609.0.tgz", + "integrity": "sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.620.0.tgz", + "integrity": "sha512-nh91S7aGK3e/o1ck64sA/CyoFw+gAYj2BDOnoNa6ouyCrVJED96ZXWbhye/fz9SgmNUZR2g7GdVpiLpMKZoI5w==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.620.0.tgz", + "integrity": "sha512-bvS6etn+KsuL32ubY5D3xNof1qkenpbJXf/ugGXbg0n98DvDFQ/F+SMLxHgbnER5dsKYchNnhmtI6/FC3HFu/A==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.614.0.tgz", + "integrity": "sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.614.0.tgz", + "integrity": "sha512-okItqyY6L9IHdxqs+Z116y5/nda7rHxLvROxtAJdLavWTYDydxrZstImNgGWTeVdmc0xX2gJCI77UYUTQWnhRw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.614.0" + } + }, + "node_modules/@aws-sdk/token-providers/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", + "integrity": "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/types/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.568.0.tgz", + "integrity": "sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.614.0.tgz", + "integrity": "sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", + "integrity": "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.609.0.tgz", + "integrity": "sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.614.0.tgz", + "integrity": "sha512-15ElZT88peoHnq5TEoEtZwoXTXRxNrk60TZNdpl/TUBJ5oNJ9Dqb5Z4ryb8ofN6nm9aFf59GVAerFDz8iUoHBA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-user-agent-node/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "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.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "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/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@smithy/abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", + "integrity": "sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/abort-controller/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.5.tgz", + "integrity": "sha512-SkW5LxfkSI1bUC74OtfBbdz+grQXYiPYolyu8VfpLIjEoN/sHVBlLeGXMQ1vX4ejkgfv6sxVbQJ32yF2cl1veA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/config-resolver/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.3.2.tgz", + "integrity": "sha512-in5wwt6chDBcUv1Lw1+QzZxN9fBffi+qOixfb65yK4sDuKG7zAUO9HAFqmVzsZM3N+3tTyvZjtnDXePpvp007Q==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.0.tgz", + "integrity": "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.4.tgz", + "integrity": "sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/fetch-http-handler/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.3.tgz", + "integrity": "sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-node/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz", + "integrity": "sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/invalid-dependency/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.5.tgz", + "integrity": "sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw==", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-content-length/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.0.tgz", + "integrity": "sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.14.tgz", + "integrity": "sha512-7ZaWZJOjUxa5hgmuMspyt8v/zVsh0GXYuF7OvCmdcbVa/xbnKQoYC+uYKunAqRGTkxjOyuOCw9rmFUFOqqC0eQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", + "integrity": "sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-serde/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz", + "integrity": "sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-stack/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.4.tgz", + "integrity": "sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-config-provider/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.4.tgz", + "integrity": "sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-http-handler/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.3.tgz", + "integrity": "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/property-provider/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.0.tgz", + "integrity": "sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/protocol-http/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz", + "integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-builder/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz", + "integrity": "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-parser/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz", + "integrity": "sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==", + "dependencies": { + "@smithy/types": "^3.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/service-error-classification/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.4.tgz", + "integrity": "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.0.tgz", + "integrity": "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.12.tgz", + "integrity": "sha512-wtm8JtsycthkHy1YA4zjIh2thJgIQ9vGkoR639DBx5lLlLNU0v4GARpQZkr2WjXue74nZ7MiTSWfVrLkyD8RkA==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/smithy-client/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.0.0.tgz", + "integrity": "sha512-VvWuQk2RKFuOr98gFhjca7fkBS+xLLURT8bUjk5XQoV0ZLm7WPwWPPY3/AwzTLuUBDeoKDCthfe1AsTUWaSEhw==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.3.tgz", + "integrity": "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/url-parser/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.14.tgz", + "integrity": "sha512-0iwTgKKmAIf+vFLV8fji21Jb2px11ktKVxbX6LIDPAUJyWQqGqBVfwba7xwa1f2FZUoolYQgLvxQEpJycXuQ5w==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.14.tgz", + "integrity": "sha512-e9uQarJKfXApkTMMruIdxHprhcXivH1flYCe8JRDTzkkLx8dA3V5J8GZlST9yfDiRWkJpZJlUXGN9Rc9Ade3OQ==", + "dependencies": { + "@smithy/config-resolver": "^3.0.5", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.5.tgz", + "integrity": "sha512-ReQP0BWihIE68OAblC/WQmDD40Gx+QY1Ez8mTdFMXpmjfxSyz2fVQu3A4zXRfQU9sZXtewk3GmhfOHswvX+eNg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-endpoints/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.3.tgz", + "integrity": "sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-middleware/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.3.tgz", + "integrity": "sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-retry/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.3.tgz", + "integrity": "sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream/node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.137", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.137.tgz", + "integrity": "sha512-YNFwzVarXAOXkjuFxONyDw1vgRNzyH8AuyN19s0bM+ChSu/bzxb5XPxYFLXoqoM+tvgzwR3k7fXcEOW125yJxg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ramda": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.0.tgz", + "integrity": "sha512-DQtfqUbSB18iM9NHbQ++kVUDuBWHMr6T2FpW1XTiksYRGjq4WnNPZLt712OEHEBJs7aMyJ68Mf2kGMOP1srVVw==", + "dev": true, + "dependencies": { + "types-ramda": "^0.30.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz", + "integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.6", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.11", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.1", + "vitest": "2.1.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", + "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/expect/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/@vitest/expect/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@vitest/expect/node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/@vitest/expect/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", + "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.1", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", + "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", + "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.1", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", + "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", + "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", + "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.1", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, + "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/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.51.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "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-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "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.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, + "node_modules/ramda": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.0.tgz", + "integrity": "sha512-13Y0iMhIQuAm/wNGBL/9HEqIfRGmNmjKnTPlKWfA9f7dnDkr8d45wQ+S7+ZLh/Pq9PdcGxkqKUEA7ySu1QSd9Q==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rewire": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rewire/-/rewire-7.0.0.tgz", + "integrity": "sha512-DyyNyzwMtGYgu0Zl/ya0PR/oaunM+VuCuBxCuhYJHHaV0V+YvYa3bBGxb5OZ71vndgmp1pYY8F4YOwQo1siRGw==", + "dev": true, + "dependencies": { + "eslint": "^8.47.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "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.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/types-ramda": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.30.0.tgz", + "integrity": "sha512-oVPw/KHB5M0Du0txTEKKM8xZOG9cZBRdCVXvwHYuNJUVkAiJ9oWyqkA+9Bj2gjMsHgkkhsYevobQBWs8I2/Xvw==", + "dev": true, + "dependencies": { + "ts-toolbelt": "^9.6.0" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", + "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", + "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.1", + "@vitest/mocker": "2.1.1", + "@vitest/pretty-format": "^2.1.1", + "@vitest/runner": "2.1.1", + "@vitest/snapshot": "2.1.1", + "@vitest/spy": "2.1.1", + "@vitest/utils": "2.1.1", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.1", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.1", + "@vitest/ui": "2.1.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/vitest/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/vitest/node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/vitest/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/source/backend/functions/myapplications/package.json b/source/backend/functions/myapplications/package.json new file mode 100644 index 00000000..07c02495 --- /dev/null +++ b/source/backend/functions/myapplications/package.json @@ -0,0 +1,47 @@ +{ + "name": "wd-export-myapplications", + "version": "2.2.0", + "description": "Lambda function that exports to myApplications", + "main": "index.mjs", + "type": "module", + "scripts": { + "typecheck": "tsc", + "pretest": "npm i && npm run typecheck", + "test": "vitest run --coverage", + "pretest:ci": "npm ci && npm run typecheck", + "test:ci": "vitest run --coverage --allowOnly false", + "clean": "rm -rf dist", + "build:zip": "zip -rq --exclude=test/* --exclude=package-lock.json myapplications.zip node_modules/ && zip -urj myapplications.zip src/", + "build:dist": "mkdir dist && mv myapplications.zip dist/", + "build": "npm run clean && npm ci --omit=dev && npm run build:zip && npm run build:dist" + }, + "license": "Apache-2.0", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com/solutions" + }, + "dependencies": { + "@aws-lambda-powertools/logger": "2.1.1", + "@aws-sdk/client-resource-groups-tagging-api": "3.621.0", + "@aws-sdk/client-service-catalog-appregistry": "3.621.0", + "@aws-sdk/client-sts": "3.621.0", + "@aws-sdk/credential-providers": "3.624.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "ramda": "0.30.0", + "zod": "3.23.8" + }, + "devDependencies": { + "@aws-sdk/client-directory-service": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/types": "3.0.0", + "@types/aws-lambda": "^8.10.137", + "@types/node": "^20.12.12", + "@types/ramda": "^0.30.0", + "@vitest/coverage-v8": "^2.1.1", + "chai": "^4.4.1", + "rewire": "7.0.0", + "sinon": "^18.0.0", + "typescript": "^5.4.5", + "vitest": "^2.1.1" + } +} diff --git a/source/backend/functions/myapplications/src/index.mjs b/source/backend/functions/myapplications/src/index.mjs new file mode 100644 index 00000000..b988e772 --- /dev/null +++ b/source/backend/functions/myapplications/src/index.mjs @@ -0,0 +1,472 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {ServiceCatalogAppRegistry} from '@aws-sdk/client-service-catalog-appregistry'; +import {ResourceGroupsTaggingAPI} from '@aws-sdk/client-resource-groups-tagging-api'; +import {Logger} from '@aws-lambda-powertools/logger'; +import {build as buildArn} from '@aws-sdk/util-arn-parser'; +import * as R from 'ramda'; +import z from 'zod'; +import {fromTemporaryCredentials} from '@aws-sdk/credential-providers'; + +const logger = new Logger({serviceName: 'WdMyApplicationsExport'}); + +const ACCESS_DENIED = 'AccessDenied'; +const ROLE_SESSION_DURATION_SECONDS = 3600; + +const AWS_REGIONS = [ + 'af-south-1', + 'ap-east-1', + 'ap-northeast-1', + 'ap-northeast-2', + 'ap-northeast-3', + 'ap-south-1', + 'ap-south-2', + 'ap-southeast-1', + 'ap-southeast-2', + 'ap-southeast-3', + 'ap-southeast-4', + 'ca-central-1', + 'ca-west-1', + 'cn-north-1', + 'cn-northwest-1', + 'eu-central-1', + 'eu-central-2', + 'eu-north-1', + 'eu-south-1', + 'eu-south-2', + 'eu-west-1', + 'eu-west-2', + 'eu-west-3', + 'me-central-1', + 'me-south-1', + 'sa-east-1', + 'us-east-1', + 'us-east-2', + 'us-gov-east-1', + 'us-gov-west-1', + 'us-west-1', + 'us-west-2', +]; + +class TypeGuardError extends Error { + /** + * @param {string} message + * @param {{cause?: Error}} [options] + */ + constructor(message, options) { + super(message, options); + this.name = 'TypeGuardError'; + } +} + +class AccessDeniedError extends Error { + /** + * @param {string} message + * @param {string} accountId + * @param {{cause?: Error}} [options] + */ + constructor(message, accountId, options) { + super(message, options); + this.name = 'AccessDeniedError'; + this.accountId = accountId; + } +} + +/** @type {Partition}*/ +const AWS_PARTITION = 'aws'; +/** @type {Partition}*/ +const AWS_CN_PARTITION = 'aws-cn'; +/** @type {Partition}*/ +const AWS_US_GOV_PARTITION = 'aws-us-gov'; + +/** @type {Map}*/ +const partitions = new Map([ + ['cn-north-1', AWS_CN_PARTITION], + ['cn-northwest-1', AWS_CN_PARTITION], + ['us-gov-east-1', AWS_US_GOV_PARTITION], + ['us-gov-west-1', AWS_US_GOV_PARTITION], +]); + +/** @type { (wdMetadata: WdMetadata, applicationMetadta: ApplicationMetadata) => Arn }*/ +function createMyApplicationsRoleArn( + {wdAccountId, wdRegion}, + {accountId, region} +) { + /** @type {Partition}*/ + const partition = partitions.get(region) ?? AWS_PARTITION; + const resource = `role/WorkloadDiscoveryMyApplicationsRole-${wdAccountId}-${wdRegion}`; + + return buildArn({ + service: 'iam', + partition, + region: '', + accountId, + resource, + }); +} + +const tagResources = R.curry( + /** @type {(tagResourcesDependencies: TagResourcesDependencies, wdMetadata: WdMetadata, applicationTag: Record, resourceTuple: RegionResourceTuple) => Promise} */ + async ( + {ResourceGroupsTaggingAPI, credentialProvider}, + {wdAccountId, wdRegion, externalId}, + applicationTag, + [accRegion, resources] + ) => { + const [accountId, region] = accRegion.split('|'); + + const taggingClient = new ResourceGroupsTaggingAPI({ + credentials: await credentialProvider( + {wdAccountId, wdRegion, externalId}, + {accountId, region} + ), + region, + }); + + const BATCH_SIZE = 20; + + return Promise.resolve(R.splitEvery(BATCH_SIZE, resources)) + .then( + R.map(chunk => { + return taggingClient + .tagResources({ + ResourceARNList: chunk.map(x => x.id), + Tags: { + ...applicationTag, + }, + }) + .then(({FailedResourcesMap = {}}) => { + const unprocessedResources = + Object.keys(FailedResourcesMap); + + if (!R.isEmpty(unprocessedResources)) { + logger.error( + 'There were errors tagging resources.', + { + unprocessedResources: + FailedResourcesMap, + } + ); + } + + return unprocessedResources; + }); + }) + ) + .then(ps => Promise.all(ps)) + .then(ps => ({unprocessedResources: ps.flat()})) + .catch(err => { + logger.error( + 'There was an error preventing any resources being tagged.', + {error: err, accountId, region} + ); + return {unprocessedResources: resources.map(x => x.id)}; + }); + } +); + +/** + * A type guard to validate the response from CreateApplication. The applicationTag field + * should always be present but if it isn't it is an unrecoverable error and we should throw. + * + * @type {(application: Application) => asserts application is VerifiedApplication} */ +function hasApplicationTag(application) { + if (application.applicationTag == null) { + throw new TypeGuardError('awsApplication tag is missing'); + } +} + +/** + * @type { (dependencies: CreateApplicationDependencies, wdMetadata: WdMetadata, applicationMetadata: ApplicationMetadata, name: string, resources: NonEmptyArray) => Promise } + * @throws {TypeGuardError} + * */ +async function createApplication( + {ServiceCatalogAppRegistry, tagResources, credentialProvider}, + {wdAccountId, wdRegion, externalId}, + {accountId, region}, + name, + resources +) { + const appRegistryClient = new ServiceCatalogAppRegistry({ + credentials: await credentialProvider( + {wdAccountId, wdRegion, externalId}, + {accountId, region} + ), + region, + }); + + const applicationTag = await appRegistryClient + .createApplication({name}) + .then(R.tap(() => logger.info('Empty application created.'))) + .then(({application = {}}) => { + hasApplicationTag(application); + return application.applicationTag; + }) + .catch(err => { + logger.error(`There was an error creating the application: ${err}`); + if (err.message === `You already own an application '${name}'`) { + throw new Error( + `An application with the name ${name} already exists.`, + {cause: err} + ); + } + throw err; + }); + + const grouped = R.groupBy(x => `${x.accountId}|${x.region}`, resources); + + return ( + Promise.resolve(grouped) + .then(R.toPairs) + // The resource array in RegionResourceTuple cannot be undefined because the Zod validation + // done in the lambda handler ensures that there will always be at least one element of + // type Resource in the resources array passed to R.groupBy + // @ts-expect-error + .then(R.map(tagResources(applicationTag))) + .then(ps => Promise.all(ps)) + .then(x => { + const unprocessedResources = x.flatMap( + x => x.unprocessedResources + ); + logger.info( + `There were ${unprocessedResources.length} unprocessed resources.`, + {unprocessedResources} + ); + return {name, applicationTag, unprocessedResources}; + }) + .then( + R.tap(({unprocessedResources}) => { + logger.info('Application successfully created', { + metricEvent: { + type: 'ApplicationCreated', + resourceCount: resources.length, + unprocessedResourceCount: + unprocessedResources.length, + regions: Object.keys( + R.groupBy(x => x.region, resources) + ), + }, + }); + }) + ) + ); +} + +class EnvironmentVariableError extends Error { + /** + * @param {string} message + * @param {{cause?: Error}} [options] + */ + constructor(message, options) { + super(message, options); + this.name = 'EnvironmentVariableError'; + } +} + +class ValidationError extends Error { + /** + * @param {string} message + * @param {{cause?: Error}} [options] + */ + constructor(message, options) { + super(message, options); + this.name = 'ValidationError'; + } +} + +/** @type {(prettyTypeName: string) => {errorMap: z.ZodErrorMap}} */ +function createRegexStringErrorMap(prettyTypeName) { + return { + errorMap: (issue, ctx) => { + if (issue.code === z.ZodIssueCode.invalid_string) { + return {message: `Not a valid ${prettyTypeName}`}; + } + return {message: ctx.defaultError}; + }, + }; +} + +// z.enum expects a non-empty array, which is modeled [T, ...T[]] in the type system, hence we must +// supply the array in this form to satisfy the constraint +const regionEnum = z.enum([AWS_REGIONS[0], ...AWS_REGIONS.slice(1)]); + +const createApplicationArgumentsSchema = z.object({ + accountId: z + .string(createRegexStringErrorMap('account ID')) + .regex(/^(\d{12})$/), + region: regionEnum, + name: z.string().regex(/[-.\w]+/, { + message: `Application name must satisfy the following pattern: [-.\\w]+`, + }), + resources: z + .array( + z.object({ + id: z + .string(createRegexStringErrorMap('ARN')) + .max(4096) + .regex(/arn:(aws|aws-cn|aws-us-gov):.*/), + accountId: z + .string(createRegexStringErrorMap('account ID')) + .regex(/^(\d{12})$/), + region: regionEnum, + }) + ) + .nonempty(), +}); + +/** + * @type { (args: CreateApplicationArguments) => z.infer } + * @throws {ValidationError} + * */ +function validateCreateApplicationArguments(args) { + const {data, error, success} = + createApplicationArgumentsSchema.safeParse(args); + + if (!success) { + const message = error.issues + .map(({path, message}) => { + return `Validation error for ${path.join('/')}: ${message}`; + }) + .join('\n'); + throw new ValidationError(message, {cause: error}); + } + + return data; +} + +const envSchema = z.object({ + AWS_ACCOUNT_ID: z.string(), + AWS_REGION: z.string(), + EXTERNAL_ID: z.string(), +}); + +/** + * Wraps the fromTemporaryCredentials Provider to provide a customised error message + * @type {(wdMetadata: WdMetadata, applicationMetadata: ApplicationMetadata) => AwsCredentialIdentityProvider} + */ +export function wrappedCredentialProvider( + {wdAccountId, wdRegion, externalId}, + {accountId, region} +) { + const RoleArn = createMyApplicationsRoleArn( + {wdAccountId, wdRegion, externalId}, + {accountId, region} + ); + + return () => { + const provider = fromTemporaryCredentials({ + params: { + RoleArn, + RoleSessionName: 'myApplicationDiscovery', + DurationSeconds: ROLE_SESSION_DURATION_SECONDS, + ExternalId: externalId + }, + }); + + return provider().catch(e => { + logger.error(`Error in wrappedCredentialProvider: ${e}`); + if (e.Code === ACCESS_DENIED) { + throw new AccessDeniedError( + `Error assuming ${RoleArn}. Ensure the global-resources template is deployed in account: ${accountId}.`, + accountId + ); + } + + throw e; + }); + }; +} + +/** + * A type guard to validate ensure the username variable exists in the AppSync resolver identity field. + * This will always be there as the system is only configured to use IAM and Cognito authentication + * but we need to inform the compiler of this. + * + * @type {(identity: AppSyncIdentity) => identity is AppSyncIdentityCognito | AppSyncIdentityIAM} */ +function hasUsername(identity) { + return identity != null && ('username' in identity); +} + +/** + * @type { (dependencies: Dependencies, env: NodeJS.ProcessEnv) => (event: MyApplicationResolverEvent, context: LambdaContext) => Promise } + * @throws {EnvironmentVariableError} + * @throws {TypeGuardError} + * @throws {ValidationError} + * */ +export function _handler( + {ResourceGroupsTaggingAPI, ServiceCatalogAppRegistry, credentialProvider}, + env +) { + return async (event, context) => { + const fieldName = event.info.fieldName; + if(hasUsername(event.identity)) { + logger.info(`User ${event.identity.username} invoked the ${fieldName} operation.`); + } + + const {data: parsedEnv, error, success} = envSchema.safeParse(env); + if (!success) { + logger.error('Unable to retrieve environment variables', { + error, + env, + }); + throw new EnvironmentVariableError( + 'Unable to retrieve environment variables', + {cause: error} + ); + } + + const { + AWS_ACCOUNT_ID: wdAccountId, + AWS_REGION: wdRegion, + EXTERNAL_ID: externalId, + } = parsedEnv; + + const args = event.arguments; + logger.info( + 'GraphQL arguments:', + {arguments: args, operation: fieldName} + ); + + switch (fieldName) { + case 'createApplication': { + const {accountId, region, name, resources} = + validateCreateApplicationArguments(args); + + const tagResourcesPartial = tagResources( + {credentialProvider, ResourceGroupsTaggingAPI}, + {wdAccountId, wdRegion, externalId} + ); + + return createApplication( + { + ServiceCatalogAppRegistry, + credentialProvider, + tagResources: tagResourcesPartial, + }, + {wdAccountId, wdRegion, externalId}, + {accountId, region}, + name, + resources + ); + } + default: { + return Promise.reject( + new Error( + `Unknown field, unable to resolve ${fieldName}.` + ) + ); + } + } + }; +} + +/** @type {(event: MyApplicationResolverEvent, context: LambdaContext) => Promise} */ +export const handler = _handler( + { + ServiceCatalogAppRegistry, + ResourceGroupsTaggingAPI, + credentialProvider: wrappedCredentialProvider, + }, + process.env +); diff --git a/source/backend/functions/myapplications/src/types.d.ts b/source/backend/functions/myapplications/src/types.d.ts new file mode 100644 index 00000000..37feb283 --- /dev/null +++ b/source/backend/functions/myapplications/src/types.d.ts @@ -0,0 +1,104 @@ +declare type NonEmptyArray = import('ramda').NonEmptyArray; + +declare type AppSyncResolverEvent = + import('aws-lambda').AppSyncResolverEvent; + +declare type AppSyncIdentity = import('aws-lambda').AppSyncIdentity; + +declare type AppSyncIdentityCognito = import('aws-lambda').AppSyncIdentityCognito; + +declare type AppSyncIdentityIAM = import('aws-lambda').AppSyncIdentityIAM; + +declare type LambdaContext = import('aws-lambda').Context; + +declare type STS = import('@aws-sdk/client-sts').STS; + +declare type ServiceCatalogAppRegistryCls = + typeof import('@aws-sdk/client-service-catalog-appregistry').ServiceCatalogAppRegistry; + +declare type ResourceGroupsTaggingAPICls = + typeof import('@aws-sdk/client-resource-groups-tagging-api').ResourceGroupsTaggingAPI; + +declare type Application = + import('@aws-sdk/client-service-catalog-appregistry').Application; + +declare type AwsCredentialIdentityProvider = + import('@smithy/types').AwsCredentialIdentityProvider; +declare type AwsCredentialidentityProviderFn = ( + arg0: WdMetadata, + arg01: ApplicationMetadata +) => AwsCredentialIdentityProvider; +declare type Credentials = import('@aws-sdk/client-sts').Credentials; + +declare type Arn = string; + +declare type Partition = 'aws' | 'aws-cn' | 'aws-us-gov'; + +declare type VerifiedCredentials = { + AccessKeyId: string; + SecretAccessKey: string; + SessionToken: string; + Expiration: Date; +}; + +declare type VerifiedApplication = { + applicationTag: Record; +}; + +declare type TagResourcesDependencies = { + ResourceGroupsTaggingAPI: ResourceGroupsTaggingAPICls; + credentialProvider: AwsCredentialidentityProviderFn; +}; + +declare type Resource = { + id: string; + region: string; + accountId: string; +}; + +declare type RegionResourceTuple = [string, Resource[]]; + +declare type WdMetadata = { + externalId: string; + wdAccountId: string; + wdRegion: string; +}; + +declare type ApplicationMetadata = { + region: string; + accountId: string; +}; + +declare type CreateApplicationResponse = { + name: string; + applicationTag: Record; + unprocessedResources: string[]; +}; + +declare type UnprocessedResources = { + unprocessedResources: string[]; +}; + +declare type CreateApplicationArguments = { + accountId: string; + region: string; + name: string; + resources: Resource[]; +}; + +declare type MyApplicationResolverEvent = + AppSyncResolverEvent; + +declare type CreateApplicationDependencies = { + tagResources: ( + applicationTag: Record + ) => (resourceTuple: RegionResourceTuple) => Promise; + credentialProvider: AwsCredentialidentityProviderFn; + ServiceCatalogAppRegistry: ServiceCatalogAppRegistryCls; +}; + +declare type Dependencies = { + ServiceCatalogAppRegistry: ServiceCatalogAppRegistryCls; + ResourceGroupsTaggingAPI: ResourceGroupsTaggingAPICls; + credentialProvider: AwsCredentialidentityProviderFn; +}; diff --git a/source/backend/functions/myapplications/test/index.test.mjs b/source/backend/functions/myapplications/test/index.test.mjs new file mode 100644 index 00000000..c61dfb51 --- /dev/null +++ b/source/backend/functions/myapplications/test/index.test.mjs @@ -0,0 +1,525 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {describe, it, vi} from 'vitest'; +import {assert} from 'chai'; +import {_handler} from '../src/index.mjs'; +import * as R from 'ramda'; + +const AWS_ACCOUNT_ID_1 = '111111111111'; +const AWS_ACCOUNT_ID_2 = '222222222222'; +const AWS_ACCOUNT_ID_3 = '333333333333'; +const AWS_ACCOUNT_ID_4 = '444444444444'; +const EU_WEST_1 = 'eu-west-1'; +const EU_WEST_2 = 'eu-west-2'; + +const EXTERNAL_ID = 'stsExternalId' + +const APPLICATION_NAME = 'testApplication'; +const APPLICATION_TAG = 'myApplicationTag'; + +describe('index.js', () => { + const mockLambdaContext = {}; + + const mockEnv = { + AWS_ACCOUNT_ID: AWS_ACCOUNT_ID_1, + AWS_REGION: EU_WEST_1, + EXTERNAL_ID, + }; + + class defaultMockServiceCatalogAppRegistry { + async createApplication() { + return { + application: {applicationTag: APPLICATION_TAG}, + }; + } + } + + class defaultMockResourceGroupsTaggingAPI { + async tagResources() { + return { + FailedResourcesMap: {}, + }; + } + } + + function defaultCredentialProvider() { + return { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + sessionToken: 'sessionToken', + }; + } + + const defaultMockDependencies = { + ServiceCatalogAppRegistry: defaultMockServiceCatalogAppRegistry, + ResourceGroupsTaggingAPI: defaultMockResourceGroupsTaggingAPI, + credentialProvider: defaultCredentialProvider, + }; + + describe('handler', () => { + describe('createApplication', () => { + it('should throw if any required environment variables are missing', async () => { + return _handler(defaultMockDependencies, {})( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: 'sameNameApplication', + resources: [], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ).catch(err => { + assert.deepEqual( + err.message, + 'Unable to retrieve environment variables' + ); + }); + }); + + it('should reject payloads with empty resource arrays', async () => { + return _handler(defaultMockDependencies, mockEnv)( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: 'sameNameApplication', + resources: [], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ).catch(err => { + assert.deepEqual( + err.message, + 'Validation error for resources: Array must contain at least 1 element(s)' + ); + }); + }); + + it('should validate the payload fields are present', async () => { + return _handler(defaultMockDependencies, mockEnv)( + { + arguments: {}, + info: { + fieldName: 'createApplication', + }, + }, + {} + ).catch(err => { + const errorMessages = err.message.split('\n'); + + assert.deepEqual(errorMessages, [ + 'Validation error for accountId: Required', + 'Validation error for region: Required', + 'Validation error for name: Required', + 'Validation error for resources: Required', + ]); + }); + }); + + it('should validate the payload types when present', async () => { + return _handler(defaultMockDependencies, mockEnv)( + { + arguments: { + accountId: 'xxxx', + region: 'ddss', + name: '^%$%', + resources: [ + { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET', + }, + { + accountId: 'yyyy', + region: 'fdfvdf', + id: 'notArn', + }, + {}, + { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET/' + 'x'.repeat(5000), + }, + ], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ).catch(err => { + const errorMessages = err.message.split('\n'); + + assert.deepEqual(errorMessages, [ + 'Validation error for accountId: Not a valid account ID', + "Validation error for region: Invalid enum value. Expected 'af-south-1' | 'ap-east-1' | 'ap-northeast-1' | 'ap-northeast-2' | 'ap-northeast-3' | 'ap-south-1' | 'ap-south-2' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-southeast-3' | 'ap-southeast-4' | 'ca-central-1' | 'ca-west-1' | 'cn-north-1' | 'cn-northwest-1' | 'eu-central-1' | 'eu-central-2' | 'eu-north-1' | 'eu-south-1' | 'eu-south-2' | 'eu-west-1' | 'eu-west-2' | 'eu-west-3' | 'me-central-1' | 'me-south-1' | 'sa-east-1' | 'us-east-1' | 'us-east-2' | 'us-gov-east-1' | 'us-gov-west-1' | 'us-west-1' | 'us-west-2', received 'ddss'", + 'Validation error for name: Application name must satisfy the following pattern: [-.\\w]+', + 'Validation error for resources/1/id: Not a valid ARN', + 'Validation error for resources/1/accountId: Not a valid account ID', + "Validation error for resources/1/region: Invalid enum value. Expected 'af-south-1' | 'ap-east-1' | 'ap-northeast-1' | 'ap-northeast-2' | 'ap-northeast-3' | 'ap-south-1' | 'ap-south-2' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-southeast-3' | 'ap-southeast-4' | 'ca-central-1' | 'ca-west-1' | 'cn-north-1' | 'cn-northwest-1' | 'eu-central-1' | 'eu-central-2' | 'eu-north-1' | 'eu-south-1' | 'eu-south-2' | 'eu-west-1' | 'eu-west-2' | 'eu-west-3' | 'me-central-1' | 'me-south-1' | 'sa-east-1' | 'us-east-1' | 'us-east-2' | 'us-gov-east-1' | 'us-gov-west-1' | 'us-west-1' | 'us-west-2', received 'fdfvdf'", + 'Validation error for resources/2/id: Required', + 'Validation error for resources/2/accountId: Required', + 'Validation error for resources/2/region: Required', + 'Validation error for resources/3/id: String must contain at most 4096 character(s)', + ]); + }); + }); + + it('should handle error when application with same name already exists', async () => { + class mockErrorServiceCatalogAppRegistry { + async createApplication() { + throw new Error( + "You already own an application 'sameNameApplication'" + ); + } + } + + return _handler( + { + ...defaultMockDependencies, + ServiceCatalogAppRegistry: + mockErrorServiceCatalogAppRegistry, + }, + mockEnv + )( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: 'sameNameApplication', + resources: [ + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET', + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + }, + ], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ).catch(err => { + assert.strictEqual( + err.message, + 'An application with the name sameNameApplication already exists.' + ); + }); + }); + + it('should assume role using external ID', async () => { + const mockCredentialProvider = ( + {wdAccountId, wdRegion, externalId}, + {accountId, region} + ) => { + if (externalId == null) { + throw new Error('External ID missing'); + } + + return { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + sessionToken: 'sessionToken', + }; + }; + + return _handler( + { + ...defaultMockDependencies, + credentialProvider: mockCredentialProvider, + }, + mockEnv + )( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: APPLICATION_NAME, + resources: [ + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET', + region: EU_WEST_1, + accountId: AWS_ACCOUNT_ID_1, + } + ], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ); + }); + + it('should fail the operation for multiple accounts if one role tagging cannot be assumed', async () => { + const mockCredentialProvider = ( + {wdAccountId, wdRegion, externalId}, + {accountId, region} + ) => { + if (accountId !== AWS_ACCOUNT_ID_1) { + throw new Error('Unable to assume role'); + } + + return { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + sessionToken: 'sessionToken', + }; + }; + + const ps = _handler( + { + ...defaultMockDependencies, + credentialProvider: mockCredentialProvider, + }, + mockEnv + )( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: APPLICATION_NAME, + resources: [ + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET', + region: EU_WEST_1, + accountId: AWS_ACCOUNT_ID_1, + }, + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET1', + region: EU_WEST_1, + accountId: AWS_ACCOUNT_ID_1, + }, + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET2', + region: EU_WEST_2, + accountId: AWS_ACCOUNT_ID_3, + }, + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET3', + region: EU_WEST_2, + accountId: AWS_ACCOUNT_ID_3, + }, + ], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ); + + return ps + .then(result => { + throw Error( + `Function should throw an error. Got result ${result}` + ); + }) + .catch(err => { + assert.equal(err.message, 'Unable to assume role'); + }); + }); + + it('should handle partial failures of tagging operation', async () => { + class partialFailureMockResourceGroupsTaggingAPI { + async tagResources({ResourceARNList}) { + const failedArn = ResourceARNList.find(x => + [ + 'arn:aws:s3:::DOC-EXAMPLE-BUCKET', + 'arn:aws:s3:::DOC-EXAMPLE-BUCKET2', + ].includes(x) + ); + const FailedResourcesMap = + failedArn == null ? {} : {[failedArn]: {}}; + + return { + FailedResourcesMap, + }; + } + } + + const actual = await _handler( + { + ...defaultMockDependencies, + ResourceGroupsTaggingAPI: + partialFailureMockResourceGroupsTaggingAPI, + }, + mockEnv + )( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: APPLICATION_NAME, + resources: [ + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET', + region: EU_WEST_1, + accountId: AWS_ACCOUNT_ID_1, + }, + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET1', + region: EU_WEST_1, + accountId: AWS_ACCOUNT_ID_1, + }, + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET2', + region: EU_WEST_2, + accountId: AWS_ACCOUNT_ID_2, + }, + { + id: 'arn:aws:s3:::DOC-EXAMPLE-BUCKET3', + region: EU_WEST_2, + accountId: AWS_ACCOUNT_ID_2, + }, + ], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ); + + assert.deepEqual(actual, { + applicationTag: APPLICATION_TAG, + name: APPLICATION_NAME, + unprocessedResources: [ + 'arn:aws:s3:::DOC-EXAMPLE-BUCKET', + 'arn:aws:s3:::DOC-EXAMPLE-BUCKET2', + ], + }); + }); + + it('should support china and gov-cloud regions', async () => { + const actual = await _handler( + { + ...defaultMockDependencies, + }, + mockEnv + )( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: APPLICATION_NAME, + resources: [ + { + accountId: AWS_ACCOUNT_ID_3, + region: 'cn-north-1', + id: 'arn:aws-cn:s3:::DOC-EXAMPLE-BUCKET', + }, + { + accountId: AWS_ACCOUNT_ID_3, + region: 'cn-northwest-1', + id: 'arn:aws-cn:s3:::DOC-EXAMPLE-BUCKET1', + }, + { + accountId: AWS_ACCOUNT_ID_4, + region: 'us-gov-west-1', + id: 'arn:aws-us-gov:s3:::DOC-EXAMPLE-BUCKET2', + }, + { + accountId: AWS_ACCOUNT_ID_4, + region: 'us-gov-east-1', + id: 'arn:aws-us-gov:s3:::DOC-EXAMPLE-BUCKET3', + }, + ], + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ); + + assert.deepEqual(actual, { + applicationTag: APPLICATION_TAG, + name: APPLICATION_NAME, + unprocessedResources: [], + }); + }); + }); + it('should support >20 resources in a diagram', async () => { + const MockResourceGroupsTaggingAPI = vi.fn(); + MockResourceGroupsTaggingAPI.prototype.tagResources = vi + .fn() + .mockImplementation(() => + Promise.resolve({ + FailedResourcesMap: {}, + }) + ); + + const RESOURCE_COUNT = 30; + + const actual = await _handler( + { + ServiceCatalogAppRegistry: + defaultMockServiceCatalogAppRegistry, + credentialProvider: defaultCredentialProvider, + ResourceGroupsTaggingAPI: MockResourceGroupsTaggingAPI, + }, + mockEnv + )( + { + arguments: { + accountId: AWS_ACCOUNT_ID_1, + region: EU_WEST_1, + name: APPLICATION_NAME, + resources: R.times( + i => ({ + accountId: AWS_ACCOUNT_ID_1, + region: 'eu-west-1', + id: `arn:aws-cn:s3:::DOC-EXAMPLE-BUCKET${i}`, + }), + RESOURCE_COUNT + ), + }, + info: { + fieldName: 'createApplication', + }, + }, + {} + ); + + assert.deepEqual(actual, { + applicationTag: APPLICATION_TAG, + name: APPLICATION_NAME, + unprocessedResources: [], + }); + + const {calls} = + MockResourceGroupsTaggingAPI.prototype.tagResources.mock; + + assert.equal(calls.length, 2); + }); + + describe('unknown field', () => { + it('should reject payloads with unknown query', async () => { + const actual = await _handler(defaultMockDependencies, mockEnv)( + { + arguments: {}, + info: { + fieldName: 'foo', + }, + }, + mockLambdaContext + ).catch(err => + assert.strictEqual( + err.message, + 'Unknown field, unable to resolve foo.' + ) + ); + }); + }); + }); +}); diff --git a/source/backend/functions/myapplications/tsconfig.json b/source/backend/functions/myapplications/tsconfig.json new file mode 100644 index 00000000..47cb22c3 --- /dev/null +++ b/source/backend/functions/myapplications/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": ["es2023"], + "moduleResolution": "NodeNext", + "module": "NodeNext", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true + }, + "include": ["src/**/*.mjs", "src/**/*.d.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/source/backend/functions/myapplications/vitest.config.mjs b/source/backend/functions/myapplications/vitest.config.mjs new file mode 100644 index 00000000..62a28848 --- /dev/null +++ b/source/backend/functions/myapplications/vitest.config.mjs @@ -0,0 +1,15 @@ +import {defineConfig} from 'vite'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: [ + ['lcov', {projectRoot: '../../../..'}], + ['html'], + ['text'], + ['json'], + ], + }, + }, +}); diff --git a/source/backend/functions/search-api/src/index.mjs b/source/backend/functions/search-api/src/index.mjs new file mode 100644 index 00000000..c45cc4b2 --- /dev/null +++ b/source/backend/functions/search-api/src/index.mjs @@ -0,0 +1,259 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {Client} from '@opensearch-project/opensearch'; +import createAwsOpensearchConnector from 'aws-opensearch-connector'; +import {Logger} from '@aws-lambda-powertools/logger'; +import * as R from 'ramda'; + +const domain = process.env.ES_DOMAIN; // e.g. search-domain.region.es.amazonaws.com + +const INDEX = 'data'; + +const logger = new Logger({serviceName: 'WdSearchApi'}); + +const osClient = new Client({ + ...createAwsOpensearchConnector({}), + node: `https://${domain}`, +}); + +function unprocessedResourcesHandler(key, items) { + return items.reduce((acc, {[key]: {error, _id}}) => { + if (error != null) { + console.log( + 'Error writing item to index: ' + JSON.stringify(error) + ); + acc.push(_id); + } + return acc; + }, []); +} + +async function indexResources(osClient, resources) { + const body = resources.flatMap(doc => { + return [{index: {_index: INDEX, _id: doc.id}}, doc]; + }); + + const { + body: {errors, items}, + } = await osClient.bulk({body}); + + const unprocessedResources = + errors === false ? [] : unprocessedResourcesHandler('index', items); + + return {unprocessedResources}; +} + +async function updateResources(osClient, resources) { + const body = resources.flatMap(doc => { + return [{update: {_index: INDEX, _id: doc.id}}, {doc}]; + }); + + const { + body: {errors, items}, + } = await osClient.bulk({body}); + + const unprocessedResources = + errors === false ? [] : unprocessedResourcesHandler('update', items); + + return {unprocessedResources}; +} + +async function deleteIndexedResources(osClient, resourceIds) { + const body = resourceIds.map(id => { + return {delete: {_index: INDEX, _id: id}}; + }); + + const { + body: {errors, items}, + } = await osClient.bulk({body}); + + const unprocessedResources = + errors === false ? [] : unprocessedResourcesHandler('delete', items); + + return {unprocessedResources}; +} + +function createProperties(properties) { + return { + accountId: properties.accountId, + arn: properties.arn, + availabilityZone: properties.availabilityZone, + awsRegion: properties.awsRegion, + configuration: properties.configuration ?? '{}', + loggedInURL: properties.loggedInURL ?? 'N/A', + loginURL: properties.loginURL ?? 'N/A', + resourceId: properties.resourceId, + private: properties.private, + resourceName: properties.resourceName, + resourceType: properties.resourceType, + resourceValue: properties.resourceValue, + state: properties.state ?? 'N/A', + subnetId: properties.subnetId, + tags: properties.tags, + title: properties.title, + vpcId: properties.vpcId, + }; +} + +async function searchResources( + osClient, + text, + {start = 0, end = 25}, + accounts, + resourceTypes +) { + const accountsBoolQuery = accounts.map(({accountId, regions}) => { + const regionQuery = R.isNil(regions) + ? [] + : [ + { + terms: { + 'properties.awsRegion.keyword': regions.map( + x => x.name + ), + }, + }, + ]; + + return { + bool: { + must: [ + { + term: { + 'properties.accountId.keyword': accountId, + }, + }, + ...regionQuery, + ], + }, + }; + }); + + const accountsQuery = R.isEmpty(accountsBoolQuery) + ? [] + : [ + { + bool: { + should: accountsBoolQuery, + }, + }, + ]; + + const resourceTypeQuery = R.isEmpty(resourceTypes) + ? [] + : [{terms: {'properties.resourceType.keyword': resourceTypes}}]; + + return osClient + .search({ + index: INDEX, + from: start, + size: end - start, + body: { + min_score: 0.1, + query: { + bool: { + should: [ + { + multi_match: {query: text}, + }, + { + wildcard: { + 'properties.resourceId': `*${text}*`, + }, + }, + { + wildcard: { + 'properties.resourceName': `*${text}*`, + }, + }, + { + wildcard: { + 'properties.arn': `*${text}*`, + }, + }, + { + wildcard: { + label: `*${text}*`, + }, + }, + ], + filter: [...accountsQuery, ...resourceTypeQuery], + }, + }, + }, + }) + .then(({body: {hits}}) => { + const resources = (hits.hits ?? []).map(({_source}) => { + const {id, label, md5hash = '', properties} = _source; + return { + id, + label, + md5hash, + properties: createProperties(properties), + }; + }); + + return { + count: hits.total.value, + resources, + }; + }); +} + +function deleteIndex(osClient, index) { + return osClient.indices.delete({index}); +} + +const MAX_PAGE_SIZE = 1000; + +export function _handler(osClient) { + return async event => { + const fieldName = event.info.fieldName; + + const {username} = event.identity; + logger.info(`User ${username} invoked the ${fieldName} operation.`); + + const args = event.arguments; + logger.info( + 'GraphQL arguments:', + {arguments: args, operation: fieldName} + ); + + switch (fieldName) { + case 'indexResources': + return indexResources(osClient, args.resources); + case 'deleteIndex': + return deleteIndex(osClient, INDEX); + case 'deleteIndexedResources': + return deleteIndexedResources(osClient, args.resourceIds); + case 'searchResources': + const pagination = args.pagination ?? {start: 0, end: 25}; + + if (pagination.end - pagination.start > MAX_PAGE_SIZE) { + return Promise.reject( + new Error(`Maximum page size is ${MAX_PAGE_SIZE}.`) + ); + } + const resourceTypes = args.resourceTypes ?? []; + const accounts = args.accounts ?? []; + return searchResources( + osClient, + args.text, + pagination, + accounts, + resourceTypes + ); + case 'updateIndexedResources': + return updateResources(osClient, args.resources); + default: + return Promise.reject( + new Error( + `Unknown field, unable to resolve ${fieldName}.` + ) + ); + } + }; +} + +export const handler = _handler(osClient); diff --git a/source/backend/functions/search-api/test/index.test.mjs b/source/backend/functions/search-api/test/index.test.mjs new file mode 100644 index 00000000..ecfb17b1 --- /dev/null +++ b/source/backend/functions/search-api/test/index.test.mjs @@ -0,0 +1,255 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {assert, describe, it} from 'vitest'; +import searchResources from './fixtures/searchResources.json' with {type: 'json'}; +import {_handler} from '../src/index.mjs'; + +describe('index.js', () => { + describe('handler', () => { + describe('indexResources', () => { + it('should handle errors from indexing resources', async () => { + const handler = _handler({ + bulk: async () => { + return { + body: { + errors: true, + items: [ + { + index: { + _index: 'index1', + _id: '1', + error: {}, + }, + }, + { + index: { + _index: 'index1', + _id: '2', + error: {}, + }, + }, + ], + }, + }; + }, + }); + + const actual = await handler({ + arguments: { + resources: [ + {id: 1, foo: '1'}, + {id: 2, bar: '2'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'indexResources', + }, + }); + + assert.deepEqual(actual, { + unprocessedResources: ['1', '2'], + }); + }); + }); + + describe('deleteIndexedResources', () => { + it('should handle errors from deleting resources', async () => { + const handler = _handler({ + bulk: async () => { + return { + body: { + errors: true, + items: [ + { + delete: { + _index: 'index1', + _id: '1', + error: {}, + }, + }, + { + delete: { + _index: 'index1', + _id: '2', + error: {}, + }, + }, + ], + }, + }; + }, + }); + + const actual = await handler({ + arguments: { + resourceIds: [1, 2], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteIndexedResources', + }, + }); + + assert.deepEqual(actual, { + unprocessedResources: ['1', '2'], + }); + }); + }); + + describe('updateIndexedResources', () => { + it('should handle errors from updating resources', async () => { + const handler = _handler({ + bulk: async () => { + return { + body: { + errors: true, + items: [ + { + update: { + _index: 'index1', + _id: '1', + error: {}, + }, + }, + { + update: { + _index: 'index1', + _id: '2', + error: {}, + }, + }, + ], + }, + }; + }, + }); + + const actual = await handler({ + arguments: { + resources: [ + {id: 1, foo: '1'}, + {id: 2, bar: '2'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'updateIndexedResources', + }, + }); + + assert.deepEqual(actual, { + unprocessedResources: ['1', '2'], + }); + }); + }); + + describe('searchResources', () => { + it('should reject requests with a page size of over 1000', async () => { + const handler = _handler({ + search: async () => { + return {}; + }, + }); + + return handler({ + arguments: { + text: 'lambdaArn', + accounts: [ + { + accountId: 'xxxxxxxxxxx', + regions: [{name: 'eu-west-1'}], + }, + ], + resourceTypes: ['AWS::Lambda::Function'], + pagination: {start: 0, end: 2000}, + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'searchResources', + }, + }).catch(err => + assert.strictEqual( + err.message, + 'Maximum page size is 1000.' + ) + ); + }); + + it('should return search values in same format as neptune', async () => { + const handler = _handler({ + search: async () => { + return searchResources; + }, + }); + + const actual = await handler({ + arguments: { + text: 'lambdaArn', + accounts: [ + { + accountId: 'xxxxxxxxxxx', + regions: [{name: 'eu-west-1'}], + }, + ], + resourceTypes: ['AWS::Lambda::Function'], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'searchResources', + }, + }); + + assert.deepEqual(actual, { + count: 1, + resources: [ + { + id: 'lambdaArn', + label: 'AWS_Lambda_Function', + md5hash: '', + properties: { + accountId: 'xxxxxxxxxxxx', + arn: 'lambdaArn', + availabilityZone: 'eu-west-1a,eu-west-1b', + awsRegion: 'eu-west-1', + configuration: '{}', + loggedInURL: 'N/A', + private: void 0, + loginURL: 'lambdaLoginUrl', + resourceId: 'lambdaResourceId', + resourceName: 'lambdaResourceName', + resourceType: 'AWS::Lambda::Function', + resourceValue: void 0, + state: 'N/A', + tags: '[]', + title: 'lambdaTitle', + vpcId: 'lambdaVpcId', + subnetId: void 0, + }, + }, + ], + }); + }); + }); + + describe('unknown field', () => { + it('should reject payloads with unknown query', async () => { + const handler = _handler({}); + + return handler({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'foo', + }, + }).catch(err => + assert.strictEqual( + err.message, + 'Unknown field, unable to resolve foo.' + ) + ); + }); + }); + }); +}); diff --git a/source/backend/functions/search-api/vitest.config.mjs b/source/backend/functions/search-api/vitest.config.mjs new file mode 100644 index 00000000..62a28848 --- /dev/null +++ b/source/backend/functions/search-api/vitest.config.mjs @@ -0,0 +1,15 @@ +import {defineConfig} from 'vite'; + +export default defineConfig({ + test: { + coverage: { + provider: 'v8', + reporter: [ + ['lcov', {projectRoot: '../../../..'}], + ['html'], + ['text'], + ['json'], + ], + }, + }, +}); diff --git a/source/backend/functions/settings/src/index.mjs b/source/backend/functions/settings/src/index.mjs new file mode 100644 index 00000000..091ae263 --- /dev/null +++ b/source/backend/functions/settings/src/index.mjs @@ -0,0 +1,839 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as R from 'ramda'; +import dynoexpr from '@tuplo/dynoexpr'; +import {Logger} from '@aws-lambda-powertools/logger'; +import AWSXRay from 'aws-xray-sdk-core'; +import {ConfigService} from '@aws-sdk/client-config-service'; +import {DynamoDB} from '@aws-sdk/client-dynamodb'; +import {EC2} from '@aws-sdk/client-ec2'; +import {DynamoDBDocument} from '@aws-sdk/lib-dynamodb'; + +const {CUSTOM_USER_AGENT: customUserAgent} = process.env; + +const configService = new ConfigService({customUserAgent}); + +const ec2Client = new EC2({customUserAgent}); + +const logger = new Logger({serviceName: 'WdSettingsApi'}); + +const dbClient = AWSXRay.captureAWSv3Client(new DynamoDB({customUserAgent})); +const docClient = DynamoDBDocument.from(dbClient); + +const AWS_ORGANIZATIONS = 'AWS_ORGANIZATIONS'; +const DUPLICATE_ACCOUNTS_ERROR = + 'Your configuration aggregator contains duplicate accounts. Delete the duplicate accounts and try again.'; + +function handleAwsConfigErrors(err) { + if ( + [DUPLICATE_ACCOUNTS_ERROR].includes(err.message) + ) { + logger.error(err); + } else { + throw err; + } +} + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); +const all = ps => Promise.all(ps); + +const query = R.curry(async (docClient, TableName, query) => { + function query_(params, items = []) { + return docClient.query(params).then(({Items, LastEvaluatedKey}) => { + items.push(...Items); + return LastEvaluatedKey == null + ? {Items: items} + : query_( + { + ExclusiveStartKey: LastEvaluatedKey, + TableName, + ...query, + }, + items + ); + }); + } + + return query_({TableName, ...query}); +}); + +const batchWrite = R.curry((docClient, retryDelay, writes) => { + function batchWrite_(writes, attempt) { + return docClient.batchWrite(writes).then(async ({UnprocessedItems}) => { + if (attempt > 3 || R.isEmpty(R.keys(UnprocessedItems))) { + return {UnprocessedItems}; + } + await sleep(attempt * retryDelay); + return batchWrite_({RequestItems: UnprocessedItems}, attempt + 1); + }); + } + + return batchWrite_(writes, 0); +}); + +const batchGet = R.curry((docClient, retryDelay, gets) => { + function batchGet_(gets, attempt, Items = []) { + return docClient + .batchGet(gets) + .then(async ({Responses, UnprocessedKeys}) => { + Items.push(...Object.values(Responses).flat()); + if (attempt > 3 || R.isEmpty(R.keys(UnprocessedKeys))) { + return {Items, UnprocessedKeys}; + } + await sleep(attempt * retryDelay); + return batchGet_( + {RequestItems: UnprocessedKeys}, + attempt + 1, + Items + ); + }); + } + + return batchGet_(gets, 0); +}); + +const createDeleteRequest = ({PK, SK}) => ({ + DeleteRequest: {Key: {PK, SK}}, +}); + +const createPutRequest = query => ({PutRequest: {Item: query}}); + +const createBatchWriteRequest = TableName => writes => ({ + RequestItems: {[TableName]: writes}, +}); + +const getUnprocessedItems = TableName => + R.pathOr([], ['UnprocessedItems', TableName]); + +const DEFAULT_ACCOUNT_PROJECTION_EXPR = + 'accountId, #name, regions, isIamRoleDeployed, organizationId, isManagementAccount, lastCrawled'; + +const DEFAULT_EXPRESSION_ATT_NAMES = { + '#name': 'name', +}; + +function getAllAccountsFromDb( + docClient, + TableName, + {ProjectionExpression, ExpressionAttributeNames} +) { + return Promise.resolve({ + KeyConditionExpression: 'PK = :PK', + ProjectionExpression, + ...(R.isEmpty(ExpressionAttributeNames) + ? ExpressionAttributeNames + : {ExpressionAttributeNames}), + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }) + .then(query(docClient, TableName)) + .then(R.prop('Items')); +} + +function getFilteredAccountsFromDb( + docClient, + TableName, + { + ProjectionExpression, + ExpressionAttributeNames = {}, + accountFilters = [], + retryTime, + } +) { + return Promise.resolve(R.splitEvery(100, accountFilters)) + .then( + R.map(accountIds => { + return { + RequestItems: { + [TableName]: { + Keys: accountIds.map(accountId => ({ + PK: 'Account', + SK: accountId, + })), + ProjectionExpression, + ...(R.isEmpty(ExpressionAttributeNames) + ? ExpressionAttributeNames + : {ExpressionAttributeNames}), + }, + }, + }; + }) + ) + .then(R.map(batchGet(docClient, retryTime))) + .then(all) + .then(R.chain(x => x.Items)); +} + +function getAccountsFromDb( + docClient, + TableName, + { + ProjectionExpression, + ExpressionAttributeNames = {}, + accountFilters = [], + retryTime, + } +) { + return R.isEmpty(accountFilters) + ? getAllAccountsFromDb(docClient, TableName, { + ProjectionExpression, + ExpressionAttributeNames, + }) + : getFilteredAccountsFromDb(docClient, TableName, { + ProjectionExpression, + ExpressionAttributeNames, + accountFilters: accountFilters, + retryTime, + }); +} + +function deleteAccounts( + docClient, + configService, + TableName, + {defaultAccountId, defaultRegion, configAggregator, isUsingOrganizations, accountIds, retryTime} +) { + return getAccountsFromDb(docClient, TableName, { + ProjectionExpression: DEFAULT_ACCOUNT_PROJECTION_EXPR, + ExpressionAttributeNames: DEFAULT_EXPRESSION_ATT_NAMES, + }) + .then(dbAccounts => { + const accountsToDelete = new Set(accountIds); + return R.reject( + ({accountId}) => accountsToDelete.has(accountId), + dbAccounts + ); + }) + .then(accounts => { + const AccountIds = R.pluck('accountId', accounts); + const AwsRegions = R.uniq( + accounts.flatMap(x => R.pluck('name', x.regions)) + ); + return {AccountIds, AwsRegions}; + }) + .then(async ({AccountIds, AwsRegions}) => { + if(isUsingOrganizations) return; + + // The putConfigurationAggregator API requires that AccountIds and AsRegions be arrays of at least + // length 1. If a user deletes all their accounts an error occurs and the accounts are not deleted. + // To mitigate this, we supply the default region and account where the config aggregator is deployed. + return configService.putConfigurationAggregator({ + ConfigurationAggregatorName: configAggregator, + AccountAggregationSources: [ + { + AccountIds: R.isEmpty(AccountIds) + ? [defaultAccountId] + : AccountIds, + AllAwsRegions: false, + AwsRegions: R.isEmpty(AwsRegions) + ? [defaultRegion] + : AwsRegions, + }, + ], + }); + }) + .catch(handleAwsConfigErrors) + .then(() => accountIds.map(id => ({PK: 'Account', SK: id}))) + .then(R.map(createDeleteRequest)) + .then(R.splitEvery(25)) + .then(R.map(createBatchWriteRequest(TableName))) + .then(R.map(batchWrite(docClient, retryTime))) + .then(all) + .then(R.chain(getUnprocessedItems(TableName))) + .then(R.map(R.path(['DeleteRequest', 'Key', 'SK']))) + .then(unprocessedAccounts => ({unprocessedAccounts})); +} + +function handleUpdateItemNotExistsError(err) { + if (err.code === 'ConditionalCheckFailedException') { + throw new Error('Cannot update item that does not exist'); + } + throw err; +} + +function updateAccount(docClient, TableName, {accountId, ...Update}) { + const dynamoArg = {PK: 'Account', SK: accountId}; + return docClient + .update( + dynoexpr({ + TableName, + Key: dynamoArg, + Condition: dynamoArg, + Update, + }) + ) + .then(() => ({accountId, ...Update})) + .catch(handleUpdateItemNotExistsError); +} + +function updateRegions(docClient, TableName, {accountId, regions}) { + // This is a naive implementation because it doesn't take into consideration + // the race condition that could occur between getting the region list and + // then updating it. This is very unlikely but if it becomes an issue, we + // can make a more robust implementation. + return docClient + .get({ + TableName, + Key: {PK: 'Account', SK: accountId}, + ProjectionExpression: 'regions', + }) + .then(({Item: {regions: dbRegions}}) => { + return R.uniqBy(R.prop('name'), regions).reduce((acc, region) => { + const i = dbRegions.findIndex(r => r.name === region.name); + if (i !== -1) acc.push({i, region}); + return acc; + }, []); + }) + .then(updatedRegions => { + const {PK, SK} = {PK: 'Account', SK: accountId}; + + const ExpressionAttributeValues = updatedRegions.reduce( + (acc, {i, region}) => { + acc[':region' + i] = region; + return acc; + }, + {':PK': PK, ':SK': SK} + ); + + const updateExprssion = updatedRegions.map(({i, region}) => { + return `#regions[${i}] = :region${i}`; + }); + + return docClient.update({ + TableName, + Key: { + PK, + SK, + }, + ConditionExpression: '(#PK = :PK) AND (#SK = :SK)', + ExpressionAttributeNames: { + '#PK': 'PK', + '#SK': 'SK', + '#regions': 'regions', + }, + ExpressionAttributeValues, + UpdateExpression: `SET ${updateExprssion.join(',')}`, + }); + }) + .catch(handleUpdateItemNotExistsError) + .then(() => ({accountId, regions})); +} + +function getAccount(docClient, TableName, {accountId}) { + return Promise.resolve({ + KeyConditionExpression: 'PK = :PK AND SK = :SK', + ProjectionExpression: DEFAULT_ACCOUNT_PROJECTION_EXPR, + ExpressionAttributeNames: { + '#name': 'name', + }, + ExpressionAttributeValues: { + ':PK': 'Account', + ':SK': accountId, + }, + }) + .then(query(docClient, TableName)) + .then(({Items}) => R.head(Items) ?? []); +} + +function getAccounts(docClient, TableName) { + return getAccountsFromDb(docClient, TableName, { + ProjectionExpression: DEFAULT_ACCOUNT_PROJECTION_EXPR, + ExpressionAttributeNames: DEFAULT_EXPRESSION_ATT_NAMES, + }); +} + +function addAccounts( + docClient, + configService, + TableName, + {accounts, configAggregator, isUsingOrganizations, retryTime} +) { + const depudedAccounts = R.map( + R.evolve({ + regions: R.uniqBy(R.prop('name')), + }), + accounts + ); + + return getAccountsFromDb(docClient, TableName, { + ProjectionExpression: DEFAULT_ACCOUNT_PROJECTION_EXPR, + ExpressionAttributeNames: DEFAULT_EXPRESSION_ATT_NAMES, + }) + .then( + R.reduce((acc, {regions, accountId}) => { + acc[accountId] = {regions, accountId}; + return acc; + }, {}) + ) + .then(dbAccounts => { + const newAccounts = depudedAccounts.reduce( + (acc, {regions, accountId}) => { + acc[accountId] = {regions, accountId}; + return acc; + }, + {} + ); + return R.mergeRight(dbAccounts, newAccounts); + }) + .then(accountObj => { + const AccountIds = R.keys(accountObj); + const AwsRegions = R.uniq( + R.values(accountObj).flatMap(x => R.pluck('name', x.regions)) + ); + return {AccountIds, AwsRegions}; + }) + .then(async ({AccountIds, AwsRegions}) => { + if(isUsingOrganizations) return; + + return configService.putConfigurationAggregator({ + ConfigurationAggregatorName: configAggregator, + AccountAggregationSources: [ + { + AccountIds, + AllAwsRegions: false, + AwsRegions, + }, + ], + }); + }) + .catch(handleAwsConfigErrors) + .then(() => depudedAccounts) + .then( + R.map(account => ({ + PK: 'Account', + SK: account.accountId, + type: 'account', + ...account, + })) + ) + .then(R.map(createPutRequest)) + .then(R.splitEvery(25)) + .then(R.map(createBatchWriteRequest(TableName))) + .then(R.map(batchWrite(docClient, retryTime))) + .then(all) + .then(R.chain(getUnprocessedItems(TableName))) + .then(R.map(R.path(['PutRequest', 'Item', 'accountId']))) + .then(unprocessedAccounts => ({unprocessedAccounts})); +} + +function handleRegions(accountHandler) { + return ( + docClient, + configService, + TableName, + {accountId, regions, configAggregator, isUsingOrganizations} + ) => { + return getAccountsFromDb(docClient, TableName, { + ProjectionExpression: DEFAULT_ACCOUNT_PROJECTION_EXPR, + ExpressionAttributeNames: DEFAULT_EXPRESSION_ATT_NAMES, + }) + .then(R.map(accountHandler(regions, accountId))) + .then(accounts => { + const AccountIds = R.pluck('accountId', accounts); + const AwsRegions = R.uniq( + accounts.flatMap(x => R.pluck('name', x.regions)) + ); + return {AccountIds, AwsRegions, accounts}; + }) + .then(async ({AccountIds, AwsRegions, accounts}) => { + if(!isUsingOrganizations) { + await configService.putConfigurationAggregator({ + ConfigurationAggregatorName: configAggregator, + AccountAggregationSources: [ + { + AccountIds, + AllAwsRegions: false, + AwsRegions, + }, + ], + }); + } + + return accounts; + }) + .catch(err => { + console.log(err); + if (err.message !== DUPLICATE_ACCOUNTS_ERROR) throw err; + }) + .then(accounts => { + const dynamoArg = {PK: 'Account', SK: accountId}; + const account = accounts.find(x => x.accountId === accountId); + + return docClient.update( + dynoexpr({ + TableName, + Key: dynamoArg, + Condition: dynamoArg, + Update: {regions: account.regions}, + ReturnValues: 'ALL_NEW', + }) + ); + }) + .catch(handleUpdateItemNotExistsError) + .then(({Attributes}) => + R.pick( + ['accountId', 'regions', 'name', 'lastCrawled'], + Attributes + ) + ); + }; +} + +const addRegions = handleRegions( + R.curry((regions, accountId, account) => { + const newRegions = R.uniqBy(R.prop('name'), [ + ...regions, + ...account.regions, + ]); + return account.accountId === accountId + ? {...account, ...{regions: newRegions}} + : account; + }) +); + +const deleteRegions = handleRegions( + R.curry((regions, accountId, account) => { + const toRemove = new Set(R.pluck('name', regions)); + const newRegions = R.reject( + ({name}) => toRemove.has(name), + account.regions + ); + + if (R.isEmpty(newRegions)) { + throw new Error( + 'Unable to delete region(s), an account must have at least one region.' + ); + } + + return account.accountId === accountId + ? {...account, ...{regions: newRegions}} + : account; + }) +); + +function getResourcesMetadata(docClient, TableName, {retryTime}) { + console.time('getResourcesMetadata elapsed time'); + + return getAccountsFromDb(docClient, TableName, { + ProjectionExpression: 'accountId, regions, resourcesRegionMetadata', + retryTime, + }) + .then(R.reject(x => x.resourcesRegionMetadata == null)) + .then(dbResponse => { + const accounts = R.map( + R.pick(['accountId', 'regions']), + dbResponse + ); + + const resourcesRegionMetadata = dbResponse.map( + x => x.resourcesRegionMetadata + ); + + const count = resourcesRegionMetadata.reduce( + (acc, {count}) => acc + count, + 0 + ); + + const resourceTypesObj = resourcesRegionMetadata.reduce( + (acc, {regions}) => { + regions.forEach(({resourceTypes}) => { + resourceTypes.forEach(({count, type}) => { + if (acc[type] == null) { + acc[type] = { + count: 0, + type, + }; + } + acc[type].count = acc[type].count + count; + }); + }); + return acc; + }, + {} + ); + + return { + count, + accounts, + resourceTypes: Object.values(resourceTypesObj), + }; + }) + .then( + R.tap(() => console.timeEnd('getResourcesMetadata elapsed time')) + ); +} + +function getResourcesAccountMetadata( + docClient, + TableName, + {retryTime, accounts = []} +) { + const accountsMap = new Map( + accounts.map(({accountId, regions = []}) => [ + accountId, + { + accountId, + regions: new Set(regions.map(x => x.name)), + }, + ]) + ); + + console.time('getResourcesAccountMetadata elapsed time'); + return getAccountsFromDb(docClient, TableName, { + ProjectionExpression: 'resourcesRegionMetadata', + accountFilters: accounts.map(x => x.accountId), + retryTime, + }) + .then( + R.chain(({resourcesRegionMetadata}) => { + if (resourcesRegionMetadata == null) return []; + + const {accountId, regions} = resourcesRegionMetadata; + let totalCount = 0; + + const resourceTypesObj = regions + .filter(({name}) => { + if (accountsMap.has(accountId)) { + const regionSet = + accountsMap.get(accountId).regions; + return regionSet.size === 0 || regionSet.has(name); + } + return true; + }) + .reduce((acc, {resourceTypes}) => { + resourceTypes.forEach(({count, type}) => { + if (acc[type] == null) { + acc[type] = { + count: 0, + type, + }; + } + + const resourceType = acc[type]; + + resourceType.count = resourceType.count + count; + totalCount = totalCount + count; + }); + return acc; + }, {}); + + return [ + { + accountId, + count: totalCount, + resourceTypes: Object.values(resourceTypesObj), + }, + ]; + }) + ) + .then( + R.tap(() => + console.timeEnd('getResourcesAccountMetadata elapsed time') + ) + ); +} + +function getResourcesRegionMetadata( + docClient, + TableName, + {retryTime, accounts = []} +) { + const accountsMap = new Map( + accounts.map(({accountId, regions = []}) => [ + accountId, + { + accountId, + regions: new Set(regions.map(x => x.name)), + }, + ]) + ); + + console.time('getResourcesRegionMetadata elapsed time'); + return getAccountsFromDb(docClient, TableName, { + ProjectionExpression: 'resourcesRegionMetadata', + accountFilters: accounts.map(x => x.accountId), + retryTime, + }) + .then(R.reject(R.isEmpty)) + .then( + R.map(({resourcesRegionMetadata}) => { + if (accountsMap.size === 0) return resourcesRegionMetadata; + + const {accountId, regions, count} = resourcesRegionMetadata; + + const {regions: regionsSet} = accountsMap.get(accountId); + + const filteredRegions = + regionsSet.size === 0 + ? regions + : regions.filter(x => regionsSet.has(x.name)); + const filteredCount = + regionsSet.size === 0 + ? count + : filteredRegions.reduce( + (acc, {count}) => acc + count, + 0 + ); + + return { + accountId, + count: filteredCount, + regions: filteredRegions, + }; + }) + ) + .then( + R.tap(() => + console.timeEnd('getResourcesRegionMetadata elapsed time') + ) + ); +} + +const isAccountNumber = R.test(/^(\d{12})$/); + +function validateAccountIds({accountId, accountIds, accounts}) { + if (accountId != null && !isAccountNumber(accountId)) { + throw new Error(`${accountId} is not a valid AWS account id.`); + } + + const invalidAccountIds = ( + accountIds ?? + accounts?.map(x => x.accountId) ?? + [] + ).filter(accountId => { + // this is a special account where AWS managed policies live + if (accountId === 'aws') return false; + return !isAccountNumber(accountId); + }); + + if (!R.isEmpty(invalidAccountIds)) { + throw new Error( + 'The following account ids are invalid: ' + invalidAccountIds + ); + } +} + +function validateRegions(regionSet, {accounts, regions}) { + const invalidRegions = ( + regions ?? + accounts?.flatMap(a => a.regions ?? []) ?? + [] + ) + .map(r => r.name) + .filter(r => !regionSet.has(r)); + + if (!R.isEmpty(invalidRegions)) { + throw new Error('The following regions are invalid: ' + invalidRegions); + } +} + +async function getRegions(ec2Client) { + // make call to aws api to get regions + const {Regions} = await ec2Client.describeRegions({}); + const regionsSet = new Set(R.pluck('RegionName', Regions)); + regionsSet.add('global'); + return regionsSet; +} + +const cache = {}; + +export function _handler( + ec2Client, + docClient, + configService, + { + ACCOUNT_ID: defaultAccountId, + AWS_REGION: defaultRegion, + DB_TABLE: TableName, + CONFIG_AGGREGATOR: configAggregator, + CROSS_ACCOUNT_DISCOVERY: crossAccountDiscovery, + RETRY_TIME: retryTime = 1000, + } +) { + return async (event, _) => { + const fieldName = event.info.fieldName; + + const args = R.reject(R.isNil, event.arguments); + logger.info( + 'GraphQL arguments:', + {arguments: args, operation: fieldName} + ); + + const {username} = event.identity; + logger.info(`User ${username} invoked the ${fieldName} operation.`); + + if (R.isNil(cache.regions)) cache.regions = await getRegions(ec2Client); + + const isUsingOrganizations = crossAccountDiscovery === AWS_ORGANIZATIONS; + + validateAccountIds(args); + validateRegions(cache.regions, args); + + switch (fieldName) { + case 'addAccounts': + return addAccounts(docClient, configService, TableName, { + configAggregator, + isUsingOrganizations, + retryTime, + ...R.evolve( + {accounts: R.uniqBy(R.prop('accountId'))}, + args + ), + }); + case 'addRegions': + return addRegions(docClient, configService, TableName, { + configAggregator, + isUsingOrganizations, + ...args, + }); + case 'deleteAccounts': + return deleteAccounts(docClient, configService, TableName, { + defaultAccountId, + defaultRegion, + configAggregator, + isUsingOrganizations, + retryTime, + ...args, + }); + case 'deleteRegions': + return deleteRegions(docClient, configService, TableName, { + configAggregator, + isUsingOrganizations, + ...args, + }); + case 'getAccount': + return getAccount(docClient, TableName, args); + case 'getAccounts': + return getAccounts(docClient, TableName); + case 'updateAccount': + return updateAccount(docClient, TableName, args); + case 'updateRegions': + return updateRegions(docClient, TableName, args); + case 'getResourcesMetadata': + return getResourcesMetadata(docClient, TableName, {retryTime}); + case 'getResourcesAccountMetadata': + return getResourcesAccountMetadata(docClient, TableName, { + retryTime, + ...args, + }); + case 'getResourcesRegionMetadata': + return getResourcesRegionMetadata(docClient, TableName, { + retryTime, + ...args, + }); + default: + return Promise.reject( + new Error(`Unknown field, unable to resolve ${fieldName}.`) + ); + } + }; +} + +export const handler = _handler( + ec2Client, + docClient, + configService, + process.env +); diff --git a/source/backend/functions/settings/test/index.test.mjs b/source/backend/functions/settings/test/index.test.mjs new file mode 100644 index 00000000..bfa6dd3c --- /dev/null +++ b/source/backend/functions/settings/test/index.test.mjs @@ -0,0 +1,3247 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import dynamoDbLocal from 'dynamo-db-local'; +import sinon from 'sinon'; +import {setTimeout} from 'timers/promises'; +import {DynamoDB, DynamoDBClient} from '@aws-sdk/client-dynamodb'; +import { + DynamoDBDocument, + BatchWriteCommand, + BatchGetCommand, +} from '@aws-sdk/lib-dynamodb'; +import { + afterAll, + afterEach, + assert, + beforeAll, + beforeEach, + describe, + it, +} from 'vitest'; +import {mockClient} from 'aws-sdk-client-mock'; +import {_handler} from '../src/index.mjs'; + +import resourceRegionMetadataInput from './fixtures/resourceRegionMetadata/input.json' with {type: 'json'}; + +const endpoint = `http://localhost:${process.env.CODEBUILD_BUILD_ID == null ? 4567 : 9000}`; + +const dbClient = new DynamoDB({ + region: 'eu-west-1', + endpoint, + credentials: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + }, +}); + +const docClient = DynamoDBDocument.from(dbClient); + +function createTable(TableName) { + return dbClient.createTable({ + AttributeDefinitions: [ + { + AttributeName: 'PK', + AttributeType: 'S', + }, + { + AttributeName: 'SK', + AttributeType: 'S', + }, + ], + KeySchema: [ + { + AttributeName: 'PK', + KeyType: 'HASH', + }, + { + AttributeName: 'SK', + KeyType: 'RANGE', + }, + ], + TableName, + BillingMode: 'PAY_PER_REQUEST', + }); +} + +async function isDynamoHealthy(attempts = 0) { + if (attempts > 10) return false; + + return dbClient + .listTables({}) + .then(() => true) + .catch(async () => { + await setTimeout(500); + return isDynamoHealthy(attempts + 1); + }); +} + +describe('index.js', () => { + + describe('handler', () => { + const mockEc2Client = { + async describeRegions() { + return { + Regions: [ + {RegionName: 'eu-west-1'}, + {RegionName: 'eu-west-2'}, + {RegionName: 'eu-central-1'}, + {RegionName: 'us-east-1'}, + {RegionName: 'us-east-2'}, + ], + }; + }, + }; + + let dynamoDbLocalProcess; + beforeAll(async function () { + dynamoDbLocalProcess = dynamoDbLocal.spawn({port: 4567}); + const isHealthy = await isDynamoHealthy(); + if (!isHealthy) + throw new Error('Could not connect to DynamoDB local'); + }, 5000); + + describe('addAccounts', () => { + const DB_TABLE = 'addAccountsTable'; + + const mockPutConfigurationAggregator = sinon.stub().resolves({}); + + const mockConfig = { + putConfigurationAggregator: mockPutConfigurationAggregator, + }; + + beforeEach(async () => { + await createTable(DB_TABLE); + }); + + it('should reject invalid account ids in accounts field', async () => { + return _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: 'xxx', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }).catch(err => { + assert.strictEqual( + err.message, + 'The following account ids are invalid: xxx' + ); + }); + }); + + it('should reject invalid regions in accounts field', async () => { + return _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'invalid-region', + }, + ], + }, + { + accountId: '222222222222', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }).catch(err => { + assert.strictEqual( + err.message, + 'The following regions are invalid: invalid-region' + ); + }); + }); + + it('should add account', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '222222222222', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111', '222222222222'], + AllAwsRegions: false, + AwsRegions: [ + 'eu-west-1', + 'eu-west-2', + 'us-east-1', + 'us-east-2', + ], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + SK: '111111111111', + name: 'test', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + { + SK: '222222222222', + name: 'test', + accountId: '222222222222', + PK: 'Account', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should add account in AWS Organizations mode', async () => { + const mockConfig = { + putConfigurationAggregator: sinon + .stub() + }; + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator', CROSS_ACCOUNT_DISCOVERY: 'AWS_ORGANIZATIONS'} + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + isManagementAccount: true, + isIamRoleDeployed: true, + organizationId: 'test-org', + lastCrawled: new Date( + '2011-06-21' + ).toISOString(), + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '222222222222', + name: 'test', + isManagementAccount: false, + isIamRoleDeployed: true, + organizationId: 'test-org', + lastCrawled: new Date( + '2014-04-09' + ).toISOString(), + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + sinon.assert.notCalled(mockConfig.putConfigurationAggregator); + + assert.deepEqual(actualDb, [ + { + SK: '111111111111', + name: 'test', + accountId: '111111111111', + PK: 'Account', + isManagementAccount: true, + isIamRoleDeployed: true, + organizationId: 'test-org', + lastCrawled: '2011-06-21T00:00:00.000Z', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + { + SK: '222222222222', + name: 'test', + accountId: '222222222222', + PK: 'Account', + isManagementAccount: false, + isIamRoleDeployed: true, + organizationId: 'test-org', + lastCrawled: '2014-04-09T00:00:00.000Z', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should remove duplicate regions before adding account', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + { + name: 'eu-west-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: ['eu-west-1', 'eu-west-2'], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + SK: '111111111111', + name: 'test', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should overwrite account', async () => { + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '333333333333', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + accountId: '333333333333', + type: 'account', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [ + { + accountId: '333333333333', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['333333333333'], + AllAwsRegions: false, + AwsRegions: ['us-east-1', 'us-east-2'], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + SK: '333333333333', + name: 'test', + accountId: '333333333333', + PK: 'Account', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should ignore duplicate accounts', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: ['eu-west-1', 'eu-west-2'], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + SK: '111111111111', + name: 'test', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should handle unprocessed items that resolve after retry', async () => { + const dynamoDB = new DynamoDBClient({ + region: 'eu-west-1', + endpoint, + credentials: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + }, + }); + + const docClient = DynamoDBDocument.from(dynamoDB); + + const ddbMock = mockClient(docClient); + + ddbMock.on(BatchWriteCommand).resolvesOnce({ + UnprocessedItems: { + addAccountsTable: [ + { + PutRequest: { + Item: { + PK: 'Account', + SK: '111111111111', + type: 'account', + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + }, + }, + { + PutRequest: { + Item: { + PK: 'Account', + SK: '222222222222', + type: 'account', + accountId: '222222222222', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + }, + }, + ], + }, + }); + + ddbMock.send.callThrough(); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator', RETRY_TIME: 10} + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '222222222222', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111', '222222222222'], + AllAwsRegions: false, + AwsRegions: [ + 'eu-west-1', + 'eu-west-2', + 'us-east-1', + 'us-east-2', + ], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + SK: '111111111111', + name: 'test', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + { + SK: '222222222222', + name: 'test', + accountId: '222222222222', + PK: 'Account', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should handle unprocessed items that do not resolve after retry', async () => { + const dynamoDB = new DynamoDBClient({ + region: 'eu-west-1', + endpoint, + credentials: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + }, + }); + + const docClient = DynamoDBDocument.from(dynamoDB); + + const ddbMock = mockClient(docClient); + + ddbMock.on(BatchWriteCommand).resolves({ + UnprocessedItems: { + addAccountsTable: [ + { + PutRequest: { + Item: { + PK: 'Account', + SK: '111111111111', + type: 'account', + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + }, + }, + { + PutRequest: { + Item: { + PK: 'Account', + SK: '222222222222', + type: 'account', + accountId: '222222222222', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + }, + }, + ], + }, + }); + + ddbMock.send.callThrough(); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + { + DB_TABLE, + RETRY_TIME: 10, + CONFIG_AGGREGATOR: 'aggregator', + } + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '222222222222', + name: 'test', + regions: [ + { + name: 'us-east-1', + }, + { + name: 'us-east-2', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: ['111111111111', '222222222222'], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111', '222222222222'], + AllAwsRegions: false, + AwsRegions: [ + 'eu-west-1', + 'eu-west-2', + 'us-east-1', + 'us-east-2', + ], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, []); + }); + + afterEach(async function () { + mockPutConfigurationAggregator.resetHistory(); + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('addRegions', () => { + const DB_TABLE = 'addRegionsTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeAll(async () => { + await createTable(DB_TABLE); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '111111111111', + name: 'testAccount', + lastCrawled: 'new Date()', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + accountId: '111111111111', + type: 'account', + }, + }); + }); + + it('should add regions', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + regions: [{name: 'eu-central-1'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [ + {name: 'eu-central-1'}, + {name: 'eu-west-1'}, + {name: 'eu-west-2'}, + ], + lastCrawled: 'new Date()', + name: 'testAccount', + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: [ + 'eu-central-1', + 'eu-west-1', + 'eu-west-2', + ], + }, + ], + }); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-central-1', + }, + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + lastCrawled: 'new Date()', + name: 'testAccount', + type: 'account', + }); + }); + + it('should add regions in AWS organizations mode', async () => { + const mockConfig = { + putConfigurationAggregator: sinon.stub() + }; + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator', CROSS_ACCOUNT_DISCOVERY: 'AWS_ORGANIZATIONS'} + )({ + arguments: { + accountId: '111111111111', + regions: [{name: 'eu-central-1'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [ + {name: 'eu-central-1'}, + {name: 'eu-west-1'}, + {name: 'eu-west-2'}, + ], + lastCrawled: 'new Date()', + name: 'testAccount', + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + sinon.assert.notCalled(mockConfig.putConfigurationAggregator); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-central-1', + }, + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + lastCrawled: 'new Date()', + name: 'testAccount', + type: 'account', + }); + }); + + it('should ignore duplicates regions', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + regions: [ + {name: 'eu-central-1'}, + {name: 'eu-central-1'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [ + {name: 'eu-central-1'}, + {name: 'eu-west-1'}, + {name: 'eu-west-2'}, + ], + lastCrawled: 'new Date()', + name: 'testAccount', + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: [ + 'eu-central-1', + 'eu-west-1', + 'eu-west-2', + ], + }, + ], + }); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + lastCrawled: 'new Date()', + name: 'testAccount', + regions: [ + { + name: 'eu-central-1', + }, + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }); + }); + + afterAll(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('deleteAccounts', () => { + const DB_TABLE = 'deleteAccountsTable'; + const ACCOUNT_ID = 'xxxxxxxxxxxx'; + const AWS_REGION = 'ap-south-1'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeEach(async () => { + await createTable(DB_TABLE); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + accountId: '111111111111', + type: 'account', + }, + }); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '222222222222', + regions: [ + { + name: 'us-west-1', + }, + { + name: 'us-west-2', + }, + ], + accountId: '222222222222', + type: 'account', + }, + }); + }); + + it('should reject invalid account ids in the accountIds field', async () => { + return _handler(mockEc2Client, docClient, mockConfig, { + ACCOUNT_ID, + AWS_REGION, + DB_TABLE, + RETRY_TIME: 10, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accountIds: ['xxx', '222222222222', 'aws'], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteAccounts', + }, + }).catch(err => { + assert.strictEqual( + err.message, + 'The following account ids are invalid: xxx' + ); + }); + }); + + it('should delete account', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + { + ACCOUNT_ID, + AWS_REGION, + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + } + )({ + arguments: { + accountIds: ['222222222222'], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: ['eu-west-1', 'eu-west-2'], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should delete account in AWS Organizations mode', async () => { + const mockConfig = { + putConfigurationAggregator: sinon + .stub() + }; + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + { + ACCOUNT_ID, + AWS_REGION, + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + CROSS_ACCOUNT_DISCOVERY: 'AWS_ORGANIZATIONS' + } + )({ + arguments: { + accountIds: ['222222222222'], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteAccounts', + }, + }); + + sinon.assert.notCalled(mockConfig.putConfigurationAggregator); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should supply default account and region to aggregator when all accounts removed', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + { + ACCOUNT_ID, + AWS_REGION, + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + } + )({ + arguments: { + accountIds: ['111111111111', '222222222222'], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['xxxxxxxxxxxx'], + AllAwsRegions: false, + AwsRegions: ['ap-south-1'], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, []); + }); + + it('should handle unprocessed items that resolve after retry', async () => { + const dynamoDB = new DynamoDBClient({ + region: 'eu-west-1', + endpoint, + credentials: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + }, + }); + + const docClient = DynamoDBDocument.from(dynamoDB); + + const ddbMock = mockClient(docClient); + + ddbMock.on(BatchWriteCommand).resolvesOnce({ + UnprocessedItems: { + deleteAccountsTable: [ + { + DeleteRequest: { + Key: { + PK: 'Account', + SK: '222222222222', + }, + }, + }, + ], + }, + }); + + ddbMock.send.callThrough(); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + { + ACCOUNT_ID, + AWS_REGION, + DB_TABLE, + RETRY_TIME: 10, + CONFIG_AGGREGATOR: 'aggregator', + } + )({ + arguments: { + accountIds: ['222222222222'], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: [], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: ['eu-west-1', 'eu-west-2'], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + PK: 'Account', + SK: '111111111111', + accountId: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + ]); + }); + + it('should handle unprocessed items that do not resolve after retry', async () => { + const dynamoDB = new DynamoDBClient({ + region: 'eu-west-1', + endpoint, + credentials: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + }, + }); + + const docClient = DynamoDBDocument.from(dynamoDB); + + const ddbMock = mockClient(docClient); + + ddbMock.on(BatchWriteCommand).resolves({ + UnprocessedItems: { + deleteAccountsTable: [ + { + DeleteRequest: { + Key: { + PK: 'Account', + SK: '222222222222', + }, + }, + }, + ], + }, + }); + ddbMock.send.callThrough(); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + { + ACCOUNT_ID, + AWS_REGION, + DB_TABLE, + RETRY_TIME: 10, + CONFIG_AGGREGATOR: 'aggregator', + } + )({ + arguments: { + accountIds: ['222222222222'], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteAccounts', + }, + }); + + assert.deepEqual(actual, { + unprocessedAccounts: ['222222222222'], + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: ['eu-west-1', 'eu-west-2'], + }, + ], + }); + + const {Items: actualDb} = await docClient.query({ + TableName: DB_TABLE, + KeyConditionExpression: 'PK = :PK', + ExpressionAttributeValues: { + ':PK': 'Account', + }, + }); + + assert.deepEqual(actualDb, [ + { + PK: 'Account', + SK: '111111111111', + accountId: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }, + { + PK: 'Account', + SK: '222222222222', + accountId: '222222222222', + regions: [ + { + name: 'us-west-1', + }, + { + name: 'us-west-2', + }, + ], + type: 'account', + }, + ]); + }); + + afterEach(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('deleteRegions', () => { + const DB_TABLE = 'deleteRegionsTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeAll(async () => { + await createTable(DB_TABLE); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + { + name: 'eu-central-1', + }, + ], + accountId: '111111111111', + type: 'account', + }, + }); + }); + + it('should reject invalid account id in accountId field', async () => { + return _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accountId: 'xxx', + regions: [{name: 'eu-west-2'}, {name: 'eu-central-1'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteRegions', + }, + }).catch(err => { + assert.strictEqual( + err.message, + 'xxx is not a valid AWS account id.' + ); + }); + }); + + it('should reject invalid region in regions field', async () => { + return _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accountId: '111111111111', + regions: [ + {name: 'invalid-region'}, + {name: 'eu-central-1'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteRegions', + }, + }).catch(err => { + assert.strictEqual( + err.message, + 'The following regions are invalid: invalid-region' + ); + }); + }); + + it('should reject deletions that remove all regions', async () => { + return _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accountId: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + { + name: 'eu-central-1', + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteRegions', + }, + }).catch(err => { + assert.strictEqual( + err.message, + 'Unable to delete region(s), an account must have at least one region.' + ); + }); + }); + + it('should delete regions', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + regions: [{name: 'eu-west-2'}, {name: 'eu-central-1'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [{name: 'eu-west-1'}], + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: ['eu-west-1'], + }, + ], + }); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + ], + type: 'account', + }); + }); + + it('should delete regions in AWS organizations mode', async () => { + const mockConfig = { + putConfigurationAggregator: sinon.stub() + }; + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator', CROSS_ACCOUNT_DISCOVERY: 'AWS_ORGANIZATIONS'} + )({ + arguments: { + accountId: '111111111111', + regions: [{name: 'eu-west-2'}, {name: 'eu-central-1'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [{name: 'eu-west-1'}], + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + sinon.assert.notCalled(mockConfig.putConfigurationAggregator); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + ], + type: 'account', + }); + }); + + it('should ignore duplicate regions', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + regions: [ + {name: 'eu-west-2'}, + {name: 'eu-west-2'}, + {name: 'eu-central-1'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'deleteRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [{name: 'eu-west-1'}], + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + sinon.assert.calledWith(mockConfig.putConfigurationAggregator, { + ConfigurationAggregatorName: 'aggregator', + AccountAggregationSources: [ + { + AccountIds: ['111111111111'], + AllAwsRegions: false, + AwsRegions: ['eu-west-1'], + }, + ], + }); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + }, + ], + type: 'account', + }); + }); + + afterAll(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('getAccounts', () => { + const DB_TABLE = 'getAccountsTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeAll(async () => { + await createTable(DB_TABLE); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + accountId: '111111111111', + type: 'account', + }, + }); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '222222222222', + regions: [ + { + name: 'us-west-1', + }, + { + name: 'us-west-2', + }, + ], + accountId: '222222222222', + type: 'account', + }, + }); + }); + + it('should get accounts', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + info: { + fieldName: 'getAccounts', + }, + identity: {username: 'testUser'}, + arguments: {}, + }); + + assert.deepEqual(actual, [ + { + accountId: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '222222222222', + regions: [ + { + name: 'us-west-1', + }, + { + name: 'us-west-2', + }, + ], + }, + ]); + }); + + afterAll(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('getAccount', () => { + const DB_TABLE = 'getAccountTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeAll(async () => { + await createTable(DB_TABLE); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + accountId: '111111111111', + type: 'account', + }, + }); + }); + + it('should get account', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getAccount', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }); + }); + + afterAll(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('updateAccount', () => { + const DB_TABLE = 'updateAccountTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeAll(async () => { + await createTable(DB_TABLE); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + accountId: '111111111111', + type: 'account', + }, + }); + }); + + it('should update account', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + lastCrawled: 'new Date()', + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'updateAccount', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + lastCrawled: 'new Date()', + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + lastCrawled: 'new Date()', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + type: 'account', + }); + }); + + afterAll(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('updateRegions', () => { + const DB_TABLE = 'updateRegionsTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeAll(async () => { + await createTable(DB_TABLE); + await docClient.put({ + TableName: DB_TABLE, + Item: { + PK: 'Account', + SK: '111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + accountId: '111111111111', + type: 'account', + }, + }); + }); + + it('should update regions', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + regions: [ + {name: 'eu-west-1', lastCrawled: 'new Date()1'}, + {name: 'eu-west-2', lastCrawled: 'new Date()2'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'updateRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [ + {name: 'eu-west-1', lastCrawled: 'new Date()1'}, + {name: 'eu-west-2', lastCrawled: 'new Date()2'}, + ], + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + lastCrawled: 'new Date()1', + }, + { + name: 'eu-west-2', + lastCrawled: 'new Date()2', + }, + ], + type: 'account', + }); + }); + + it('should ignore duplicate regions', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accountId: '111111111111', + regions: [ + {name: 'eu-west-1', lastCrawled: 'new Date()1'}, + {name: 'eu-west-2', lastCrawled: 'new Date()2'}, + {name: 'eu-west-2', lastCrawled: 'new Date()2'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'updateRegions', + }, + }); + + assert.deepEqual(actual, { + accountId: '111111111111', + regions: [ + {name: 'eu-west-1', lastCrawled: 'new Date()1'}, + {name: 'eu-west-2', lastCrawled: 'new Date()2'}, + {name: 'eu-west-2', lastCrawled: 'new Date()2'}, + ], + }); + + const {Item: actualDb} = await docClient.get({ + TableName: DB_TABLE, + Key: { + PK: 'Account', + SK: '111111111111', + }, + }); + + assert.deepEqual(actualDb, { + SK: '111111111111', + accountId: '111111111111', + PK: 'Account', + regions: [ + { + name: 'eu-west-1', + lastCrawled: 'new Date()1', + }, + { + name: 'eu-west-2', + lastCrawled: 'new Date()2', + }, + ], + type: 'account', + }); + }); + + afterAll(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + }); + + describe('getResourcesMetadata', () => { + const DB_TABLE = 'getResourcesMetadataTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeEach(async () => { + await createTable(DB_TABLE); + }); + + afterEach(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + + it('should handle no accounts', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesMetadata', + }, + }); + + assert.deepEqual(actual, { + accounts: [], + count: 0, + resourceTypes: [], + }); + }); + + it('should ignore accounts with no metadata', async () => { + const {default: expected} = await import( + './fixtures/getResourcesMetadata/no-metadata-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should return meta data broken down by account and resource type', async () => { + const {default: expected} = await import( + './fixtures/getResourcesMetadata/default-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + }); + + describe('getResourcesAccountMetadata', () => { + const DB_TABLE = 'getResourcesAccountMetadataTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeEach(async () => { + await createTable(DB_TABLE); + }); + + afterEach(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + + it('should handle no accounts', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesAccountMetadata', + }, + }); + + assert.deepEqual(actual, []); + }); + + it('should ignore accounts with no metadata', async () => { + const {default: expected} = await import( + './fixtures/getResourcesAccountMetadata/no-metadata-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: null, + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesAccountMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should return per account metadata broken down by resource type', async () => { + const {default: expected} = await import( + './fixtures/getResourcesAccountMetadata/default-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: null, + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesAccountMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should return per account metadata broken down by resource type filtered by account', async () => { + const {default: expected} = await import( + './fixtures/getResourcesAccountMetadata/account-filter-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [{accountId: '111111111111'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesAccountMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should handle unprocessed keys that resolve after retry', async () => { + const {default: expected} = await import( + './fixtures/getResourcesAccountMetadata/account-filter-expected.json', + {with: {type: 'json'}} + ); + + const dynamoDB = new DynamoDBClient({ + region: 'eu-west-1', + endpoint, + credentials: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + }, + }); + + const docClient = DynamoDBDocument.from(dynamoDB); + + const ddbMock = mockClient(docClient); + + ddbMock.on(BatchGetCommand).resolvesOnce({ + Responses: { + [DB_TABLE]: [], + }, + UnprocessedKeys: { + [DB_TABLE]: { + Keys: [{PK: 'Account', SK: '111111111111'}], + ProjectionExpression: 'resourcesRegionMetadata', + }, + }, + }); + + ddbMock.send.callThrough(); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + RETRY_TIME: 10, + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [{accountId: '111111111111'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesAccountMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should return per account metadata broken down by resource type filtered by account and region', async () => { + const {default: expected} = await import( + './fixtures/getResourcesAccountMetadata/account-region-filter-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + regions: [{name: 'eu-west-1'}], + }, + {accountId: '222222222222'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesAccountMetadata', + }, + }); + + assert.deepEqual( + actual.sort((a, b) => a.accountId - b.accountId), + expected + ); + }); + }); + + describe('getResourcesRegionMetadata', () => { + const DB_TABLE = 'getResourcesRegionMetadataTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + beforeEach(async () => { + await createTable(DB_TABLE); + }); + + afterEach(async function () { + return dbClient.deleteTable({TableName: DB_TABLE}); + }); + + it('should handle no accounts', async () => { + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesRegionMetadata', + }, + }); + + assert.deepEqual(actual, []); + }); + + it('should ignore accounts with no metadata', async () => { + const {default: expected} = await import( + './fixtures/getResourcesRegionMetadata/no-metadata-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: null, + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesRegionMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should return per account metadata broken down by resource type', async () => { + const {default: expected} = await import( + './fixtures/getResourcesRegionMetadata/default-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: null, + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesRegionMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should return per account metadata broken down by resource type filtered by account', async () => { + const {default: expected} = await import( + './fixtures/getResourcesRegionMetadata/account-filter-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [{accountId: '111111111111'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesRegionMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should handle unprocessed keys that resolve after retry', async () => { + const {default: expected} = await import( + './fixtures/getResourcesRegionMetadata/account-filter-expected.json', + {with: {type: 'json'}} + ); + + const dynamoDB = new DynamoDBClient({ + region: 'eu-west-1', + endpoint, + credentials: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + }, + }); + + const docClient = DynamoDBDocument.from(dynamoDB); + + const ddbMock = mockClient(docClient); + + ddbMock.on(BatchGetCommand).resolvesOnce({ + Responses: { + [DB_TABLE]: [], + }, + UnprocessedKeys: { + [DB_TABLE]: { + Keys: [{PK: 'Account', SK: '111111111111'}], + ProjectionExpression: 'resourcesRegionMetadata', + }, + }, + }); + + ddbMock.send.callThrough(); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator', RETRY_TIME: 10} + )({ + arguments: { + accounts: [{accountId: '111111111111'}], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesRegionMetadata', + }, + }); + + assert.deepEqual(actual, expected); + }); + + it('should return per account metadata broken down by resource type filtered by account and region', async () => { + const {default: expected} = await import( + './fixtures/getResourcesRegionMetadata/account-region-filter-expected.json', + {with: {type: 'json'}} + ); + + await _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })({ + arguments: { + accounts: [ + { + accountId: '111111111111', + name: 'test111111111111', + regions: [ + { + name: 'eu-west-1', + }, + { + name: 'eu-west-2', + }, + ], + ...resourceRegionMetadataInput['111111111111'], + }, + { + accountId: '222222222222', + name: 'test222222222222', + regions: [ + { + name: 'us-east-1', + }, + ], + ...resourceRegionMetadataInput['222222222222'], + }, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'addAccounts', + }, + }); + + const actual = await _handler( + mockEc2Client, + docClient, + mockConfig, + {DB_TABLE, CONFIG_AGGREGATOR: 'aggregator'} + )({ + arguments: { + accounts: [ + { + accountId: '111111111111', + regions: [{name: 'eu-west-1'}], + }, + {accountId: '222222222222'}, + ], + }, + identity: {username: 'testUser'}, + info: { + fieldName: 'getResourcesRegionMetadata', + }, + }); + + assert.deepEqual( + actual.sort((a, b) => a.accountId - b.accountId), + expected + ); + }); + }); + + describe('unknown query', () => { + it('should reject payloads with unknown query', async () => { + const DB_TABLE = 'dbTable'; + + const mockConfig = { + putConfigurationAggregator: sinon.stub().resolves({}), + }; + + return _handler(mockEc2Client, docClient, mockConfig, { + DB_TABLE, + CONFIG_AGGREGATOR: 'aggregator', + })( + { + arguments: {}, + identity: {username: 'testUser'}, + info: { + fieldName: 'foo', + }, + }, + {} + ).catch(err => + assert.strictEqual( + err.message, + 'Unknown field, unable to resolve foo.' + ) + ); + }); + }); + + afterAll(function () { + return new Promise(resolve => { + dynamoDbLocalProcess.kill(); + resolve(); + }); + }); + }); +}); diff --git a/source/cfn/templates/application-insights.template b/source/cfn/templates/application-insights.template new file mode 100644 index 00000000..6fdd98b7 --- /dev/null +++ b/source/cfn/templates/application-insights.template @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: 2010-09-09 + +Description: Workload Discovery on AWS Application Insights Dashboard + +Parameters: + + ApplicationResourceGroupName: + Type: String + + ClusterArn: + Type: String + + DiscoveryTaskLogGroup: + Type: String + +Resources: + + ApplicationDashboard: + Type: AWS::ApplicationInsights::Application + Properties: + AutoConfigurationEnabled: true + ResourceGroupName: !Ref ApplicationResourceGroupName + LogPatternSets: + - PatternSetName: DiscoveryPatternSet + LogPatterns: + - PatternName: IamRoleNotDeployed + Pattern: 'The discovery for this account will be skipped' + Rank: 1 + ComponentMonitoringSettings: + - ComponentARN: !Ref ClusterArn + Tier: DEFAULT + ComponentConfigurationMode: DEFAULT_WITH_OVERWRITE + DefaultOverwriteComponentConfiguration: + ConfigurationDetails: + Logs: + - LogGroupName: !Ref DiscoveryTaskLogGroup + LogType: APPLICATION + PatternSet: DiscoveryPatternSet diff --git a/source/cfn/templates/myapplications-resolvers.template b/source/cfn/templates/myapplications-resolvers.template new file mode 100644 index 00000000..f3f2aded --- /dev/null +++ b/source/cfn/templates/myapplications-resolvers.template @@ -0,0 +1,147 @@ +AWSTemplateFormatVersion: 2010-09-09 + +Transform: AWS::Serverless-2016-10-31 + +Description: Workload Discovery on AWS myApplications Export API + +Parameters: + + NodeLambdaRuntime: + Type: String + + DeploymentBucket: + Type: String + + DeploymentBucketKey: + Type: String + + PerspectiveAppSyncApiId: + Type: String + + ExternalId: + Type: String + +Resources: + + MyApplicationsLambdaRole: + Type: AWS::IAM::Role + Properties: + Path: '/' + Policies: + - PolicyName: !Sub MyApplicationsAppSyncLambdaLogPolicy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: arn:aws:logs:*:*:* + - PolicyName: assumeMyApplicationsRole + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - sts:AssumeRole + Resource: !Sub arn:aws:iam::*:role/WorkloadDiscoveryMyApplicationsRole-${AWS::AccountId}-${AWS::Region} + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: sts:AssumeRole + + MyApplicationsFunction: + Type: AWS::Serverless::Function + Metadata: + cfn_nag: + rules_to_suppress: + - id: W89 + reason: This Lambda does not connect to any resources in a VPC + Properties: + Role: !GetAtt MyApplicationsLambdaRole.Arn + Description: Exports diagram to myApplications + Runtime: !Ref NodeLambdaRuntime + Handler: index.handler + CodeUri: + Bucket: !Ref DeploymentBucket + Key: !Sub ${DeploymentBucketKey}/myapplications.zip + Timeout: 10 + MemorySize: 512 + LoggingConfig: + LogGroup: !Ref MyApplicationsLambdaLogGroup + Environment: + Variables: + AWS_ACCOUNT_ID: !Ref AWS::AccountId + EXTERNAL_ID: !Ref ExternalId + + MyApplicationsLambdaLogGroup: + Type: AWS::Logs::LogGroup + Properties: + # Define a new log group as it must exist for + # MyApplicationsOperationalMetricsFunction event filter + # to be created. The implicit log group is lazily created and + # does not exist at stack deployment, causing an error + LogGroupName: !Sub + - /aws/lambda/MyApplicationsFunction-${UUID} + - UUID: !Select [2, !Split ["/", !Ref AWS::StackId]] + + RetentionInDays: 30 + + MyApplicationsInvokeRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - appsync.amazonaws.com + Action: + - sts:AssumeRole + Policies: + - PolicyName: !Sub AppSyncMyApplicationsRole + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: !GetAtt MyApplicationsFunction.Arn + + MyApplicationsExportLambdaDataSource: + Type: AWS::AppSync::DataSource + Properties: + ApiId: !Ref PerspectiveAppSyncApiId + Name: MyApplication_Lambda_DS9 + Description: myApplication Export Lambda AppSync Data Source + Type: AWS_LAMBDA + ServiceRoleArn: !GetAtt MyApplicationsInvokeRole.Arn + LambdaConfig: + LambdaFunctionArn: !GetAtt MyApplicationsFunction.Arn + + MyApplicationsExportResolver: + Type: AWS::AppSync::Resolver + Properties: + ApiId: !Ref PerspectiveAppSyncApiId + Runtime: + Name: APPSYNC_JS + RuntimeVersion: 1.0.0 + CodeS3Location: !Sub s3://${DeploymentBucket}/${DeploymentBucketKey}/default-resolver.js + TypeName: Mutation + FieldName: createApplication + DataSourceName: !GetAtt MyApplicationsExportLambdaDataSource.Name + +Outputs: + + MyApplicationsLambdaRoleArn: + Description: ARN of role used by myApplications lambda function + Value: !GetAtt MyApplicationsLambdaRole.Arn + + MyApplicationsLambdaLogGroup: + Description: The name of the myApplication lambda log group + Value: !Ref MyApplicationsLambdaLogGroup diff --git a/source/frontend/public/icons/AWS-Identity-and-Access-Management-IAM_Instance_Profile_light-bg.svg b/source/frontend/public/icons/AWS-Identity-and-Access-Management-IAM_Instance_Profile_light-bg.svg new file mode 100644 index 00000000..8cd5dc6c --- /dev/null +++ b/source/frontend/public/icons/AWS-Identity-and-Access-Management-IAM_Instance_Profile_light-bg.svg @@ -0,0 +1,15 @@ + + AWS-Identity-and-Access-Management-IAM_Role_light-bg + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/frontend/public/icons/Arch_AWS-AppSync_64-DataSource.svg b/source/frontend/public/icons/Arch_AWS-AppSync_64-DataSource.svg new file mode 100644 index 00000000..f940e545 --- /dev/null +++ b/source/frontend/public/icons/Arch_AWS-AppSync_64-DataSource.svg @@ -0,0 +1,12 @@ + + + Icon-Architecture/64/Arch_AWS-AppSync_64 + + + + + + + + + \ No newline at end of file diff --git a/source/frontend/public/icons/Arch_AWS-AppSync_64-Resolver.svg b/source/frontend/public/icons/Arch_AWS-AppSync_64-Resolver.svg new file mode 100644 index 00000000..afdbc42a --- /dev/null +++ b/source/frontend/public/icons/Arch_AWS-AppSync_64-Resolver.svg @@ -0,0 +1,12 @@ + + + Icon-Architecture/64/Arch_AWS-AppSync_64 + + + + + + + + + \ No newline at end of file diff --git a/source/frontend/public/icons/Arch_AWS-Elemental-MediaConnect_64.svg b/source/frontend/public/icons/Arch_AWS-Elemental-MediaConnect_64.svg new file mode 100644 index 00000000..4b25bf7f --- /dev/null +++ b/source/frontend/public/icons/Arch_AWS-Elemental-MediaConnect_64.svg @@ -0,0 +1,10 @@ + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaConnect_64 + + + + + + + \ No newline at end of file diff --git a/source/frontend/public/icons/Arch_AWS-Elemental-MediaTailor_64.svg b/source/frontend/public/icons/Arch_AWS-Elemental-MediaTailor_64.svg new file mode 100644 index 00000000..98c564db --- /dev/null +++ b/source/frontend/public/icons/Arch_AWS-Elemental-MediaTailor_64.svg @@ -0,0 +1,10 @@ + + + Icon-Architecture/64/Arch_AWS-Elemental-MediaTailor_64 + + + + + + + \ No newline at end of file diff --git a/source/frontend/public/icons/Arch_AWS-Service-Catalog_64.svg b/source/frontend/public/icons/Arch_AWS-Service-Catalog_64.svg new file mode 100644 index 00000000..e5564050 --- /dev/null +++ b/source/frontend/public/icons/Arch_AWS-Service-Catalog_64.svg @@ -0,0 +1,10 @@ + + + Icon-Architecture/64/Arch_AWS-Service-Catalog_64 + + + + + + + \ No newline at end of file diff --git a/source/frontend/src/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.js b/source/frontend/src/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.js new file mode 100644 index 00000000..06df538d --- /dev/null +++ b/source/frontend/src/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.js @@ -0,0 +1,378 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, {useEffect} from 'react'; +import {saveAs} from 'file-saver'; +import { + Button, + SpaceBetween, + Input, + FormField, + RadioGroup, + Modal, + Select, + Form, +} from '@cloudscape-design/components'; +import validFilename from 'valid-filename'; +import {useParams} from 'react-router-dom'; +import {exportCSVFromCanvas} from './CSV/CreateCSVExport'; +import {exportJSON} from './JSON/CreateJSONExport'; +import * as R from 'ramda'; +import {useDrawIoUrl} from '../../../../Hooks/useDrawIoUrl'; +import {useCreateApplication} from '../../../../Hooks/useMyApplications'; +import {PSEUDO_RESOURCE_TYPES} from '../../../../../config/constants'; + +const ExportDiagramModal = ({ + canvas, + elements, + visible, + onDismiss, + settings, +}) => { + const [error, setError] = React.useState(false); + const {name, visibility} = useParams(); + const [isExportButtonDisabled, setIsExportButtonDisabled] = + React.useState(true); + const [accountsObj, setAccountsObj] = React.useState({}); + const [filename, setFilename] = React.useState(name); + const [applicationName, setApplicationName] = React.useState( + createDefaultApplicationName(name) + ); + const [selectedRegion, setSelectedRegion] = React.useState(null); + const [selectedAccount, setSelectedAccount] = React.useState(null); + const [exportType, setExportType] = React.useState('drawio'); + const {isLoading: isLoadingCreateApplication, createApplicationAsync} = + useCreateApplication(); + + const {isLoading: loadingDrawIoUrl, refetch} = useDrawIoUrl( + name, + visibility, + {enabled: false} + ); + + const saveFile = (name, blob) => { + if (validFilename(name)) { + setError(false); + saveAs(blob, name); + } else { + setError(true); + } + }; + + const onChangeApplicationName = name => { + if (name.match(/^[-.\w]+$/)) { + setError(false); + } else { + setError(true); + } + setApplicationName(name); + }; + + useEffect(() => { + const missingValues = { + drawio: false, + myapplications: + applicationName == null || + selectedAccount == null || + selectedRegion == null, + csv: filename == null, + json: filename == null, + svg: filename == null, + }; + + setIsExportButtonDisabled(missingValues[exportType] || error); + }, [ + exportType, + applicationName, + selectedAccount, + selectedRegion, + error, + filename, + ]); + + useEffect(() => { + if (!R.isEmpty(elements)) { + const resources = elements.nodes + .filter( + x => + x.data.type === 'resource' && + x.data.properties.awsRegion !== 'global' + ) + .map(({data}) => { + return { + accountId: data.properties.accountId, + region: data.properties.awsRegion, + }; + }); + + const accountsObj = R.groupBy( + x => x.accountId, + R.uniqBy(x => { + return `${x.accountId}|${x.region}`; + }, resources) + ); + + setAccountsObj(accountsObj); + } + }, [elements]); + + function clearApplicationState() { + setApplicationName(createDefaultApplicationName(name)); + setSelectedAccount(null); + setSelectedRegion(null); + } + + const handleExport = async () => { + const diagramData = settings.hideEdges + ? R.pick(['nodes'], elements) + : elements; + switch (exportType) { + case 'drawio': { + const {data: url} = await refetch(); + window.open(url, '_blank', 'rel=noreferrer'); + break; + } + case 'csv': { + exportCSVFromCanvas(diagramData, name); + break; + } + case 'json': { + saveFile(name, exportJSON(diagramData)); + break; + } + case 'myapplications': { + const resources = diagramData.nodes + .filter( + x => + x.data.type === 'resource' && + !PSEUDO_RESOURCE_TYPES.has( + x.data.properties?.resourceType + ) + ) + .map(x => { + return { + id: x.data.id, + region: x.data.properties.awsRegion, + accountId: x.data.properties.accountId, + }; + }) + .filter(x => x.id.startsWith('arn:')); + + await createApplicationAsync({ + name: applicationName, + accountId: selectedAccount.value, + region: selectedRegion.value, + resources, + }).catch(_ => {}); // this noop is required to prevent unhandled promise errors due to how react-query mutation error handling works + break; + } + case 'svg': { + saveFile( + name, + new Blob([canvas.svg({full: true})], { + type: 'image/svg+xml', + }) + ); + break; + } + default: { + break; + } + } + clearApplicationState(); + onDismiss(); + }; + + return ( + + + setExportType(detail.value)} + value={exportType} + ariaLabel={ + 'Radio button group used to select which format to export to' + } + items={[ + { + value: 'json', + label: 'JSON', + description: + 'Export a JSON representation of the architecture diagram', + }, + { + value: 'csv', + label: 'CSV', + description: + 'Export a Comma-separated values representation of the architecture diagram', + }, + { + value: 'svg', + label: 'SVG', + description: + 'Export the architecture diagram as an SVG file.', + }, + { + value: 'drawio', + label: 'Diagrams.net (formerly Draw.io)', + description: + 'Export the architecture diagram as a diagrams.net URL with the diagram contents base64 encoded in the URL query string (opens in a new tab).', + }, + { + value: 'myapplications', + label: 'myApplications', + description: + 'Export the resources in this diagram to myApplications', + }, + ]} + /> +
e.preventDefault()}> + + + + + } + > + + {!['drawio', 'myapplications'].includes( + exportType + ) && ( + + + setFilename(detail.value) + } + /> + + )} + {exportType === 'myapplications' && ( + <> + + + onChangeApplicationName( + detail.value + ) + } + /> + + + + setSelectedRegion( + detail.selectedOption + ) + } + options={( + accountsObj[ + selectedAccount?.value + ] ?? [] + ).map(({region}) => { + return { + value: region, + label: region, + }; + })} + /> + + + )} + +
+ +
+
+ ); +}; + +// createDefaultApplicationName returns a string that is a valid application name +const createDefaultApplicationName = name => name.replace(/ /g, '-'); + +export default ExportDiagramModal; diff --git a/source/frontend/src/components/Diagrams/Draw/Utils/ResourceSearch.css b/source/frontend/src/components/Diagrams/Draw/Utils/ResourceSearch.css new file mode 100644 index 00000000..c9a5f7db --- /dev/null +++ b/source/frontend/src/components/Diagrams/Draw/Utils/ResourceSearch.css @@ -0,0 +1,12 @@ +.flex-container { + display: flex; +} + +.flex-no-shrink { + flex-shrink: 0; +} + +.flex-auto { + flex: auto; + margin-right: 5px; +} diff --git a/source/frontend/src/components/Hooks/useMyApplications.js b/source/frontend/src/components/Hooks/useMyApplications.js new file mode 100644 index 00000000..5428f087 --- /dev/null +++ b/source/frontend/src/components/Hooks/useMyApplications.js @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {useMutation} from 'react-query'; +import useQueryErrorHandler from './useQueryErrorHandler'; +import { + createApplication, + handleResponse, +} from '../../API/Handlers/ResourceGraphQLHandler'; +import {wrapRequest} from '../../Utils/API/HandlerUtils'; +import {processResourcesError} from '../../Utils/ErrorHandlingUtils'; +import * as R from 'ramda'; +import {useNotificationDispatch} from '../Contexts/NotificationContext'; + +export const useCreateApplication = (config = {}) => { + const {handleError} = useQueryErrorHandler(); + const {addNotification} = useNotificationDispatch(); + + const mutation = useMutation( + ({name, accountId, region, resources}) => { + return wrapRequest(processResourcesError, createApplication, { + name, + accountId, + region, + resources, + }) + .then(handleResponse) + .then(R.pathOr([], ['body', 'data', 'createApplication'])); + }, + { + onSuccess: async data => { + const hasUnprocessedResources = !R.isEmpty( + data.unprocessedResources + ); + const unprocessedResourcesMsg = hasUnprocessedResources + ? `However, the following resources were not added: ${data.unprocessedResources.join(', ')}` + : ''; + + addNotification({ + header: 'Application created', + content: `The application named ${data.name} has been created. ${unprocessedResourcesMsg}`, + type: hasUnprocessedResources ? 'warning' : 'success', + }); + }, + onError: handleError, + ...config, + } + ); + + return { + createApplication: mutation.mutate, + createApplicationAsync: mutation.mutateAsync, + isLoading: mutation.isLoading, + }; +}; diff --git a/source/frontend/src/cytoscape/plugins/svg/exportToSvg.js b/source/frontend/src/cytoscape/plugins/svg/exportToSvg.js new file mode 100644 index 00000000..1f56e288 --- /dev/null +++ b/source/frontend/src/cytoscape/plugins/svg/exportToSvg.js @@ -0,0 +1,110 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * This code is based on: + * https://github.com/iVis-at-Bilkent/cytoscape.js/blob/master/src/extensions/renderer/canvas/export-image.js + */ + +import {Context} from './svgCanvas'; + +const isNumber = obj => + obj != null && typeof obj === 'number' && !Number.isNaN(obj); + +export default function (options) { + const renderer = this.renderer(); + + //disable pathsEnabled temporarily + const pathsEnabledOld = renderer.pathsEnabled; + renderer.pathsEnabled = false; + + // flush path cache + this.elements().forEach(ele => { + ele._private.rscratch.pathCacheKey = null; + ele._private.rscratch.pathCache = null; + }); + + const eles = this.mutableElements(); + const bb = eles.boundingBox(); + const ctrRect = renderer.findContainerClientCoords(); + let width = options.full ? Math.ceil(bb.w) : ctrRect[2]; + let height = options.full ? Math.ceil(bb.h) : ctrRect[3]; + const specdMaxDims = + isNumber(options.maxWidth) || isNumber(options.maxHeight); + const pxRatio = renderer.getPixelRatio(); + let scale = 1; + + if (options.scale != null) { + width *= options.scale; + height *= options.scale; + + scale = options.scale; + } else if (specdMaxDims) { + let maxScaleW = Infinity; + let maxScaleH = Infinity; + + if (isNumber(options.maxWidth)) { + maxScaleW = (scale * options.maxWidth) / width; + } + + if (isNumber(options.maxHeight)) { + maxScaleH = (scale * options.maxHeight) / height; + } + + scale = Math.min(maxScaleW, maxScaleH); + + width *= scale; + height *= scale; + } + + if (!specdMaxDims) { + width *= pxRatio; + height *= pxRatio; + scale *= pxRatio; + } + + const buffCanvas = new Context({width, height, embedImages: true}); + + // Rasterize the layers, but only if container has nonzero size + if (width > 0 && height > 0) { + buffCanvas.clearRect(0, 0, width, height); + + buffCanvas.globalCompositeOperation = 'source-over'; + + const zsortedEles = renderer.getCachedZSortedEles(); + + if (options.full) { + // draw the full bounds of the graph + buffCanvas.translate(-bb.x1 * scale, -bb.y1 * scale); + buffCanvas.scale(scale, scale); + + renderer.drawElements(buffCanvas, zsortedEles); + + buffCanvas.scale(1 / scale, 1 / scale); + buffCanvas.translate(bb.x1 * scale, bb.y1 * scale); + } else { + // draw the current view + const pan = this.pan(); + + const translation = { + x: pan.x * scale, + y: pan.y * scale, + }; + + scale *= this.zoom(); + + buffCanvas.translate(translation.x, translation.y); + buffCanvas.scale(scale, scale); + + renderer.drawElements(buffCanvas, zsortedEles); + + buffCanvas.scale(1 / scale, 1 / scale); + buffCanvas.translate(-translation.x, -translation.y); + } + } + + // restore pathsEnabled to old value + renderer.pathsEnabled = pathsEnabledOld; + + return buffCanvas.getSerializedSvg(); +} diff --git a/source/frontend/src/cytoscape/plugins/svg/index.js b/source/frontend/src/cytoscape/plugins/svg/index.js new file mode 100644 index 00000000..f1f37944 --- /dev/null +++ b/source/frontend/src/cytoscape/plugins/svg/index.js @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import exportToSvg from './exportToSvg'; + +export default function register(cytoscape) { + cytoscape('core', 'svg', exportToSvg); +} + +// auto register +if (window.cytoscape != null) { + register(window.cytoscape); +} diff --git a/source/frontend/src/cytoscape/plugins/svg/svgCanvas.js b/source/frontend/src/cytoscape/plugins/svg/svgCanvas.js new file mode 100644 index 00000000..2a012410 --- /dev/null +++ b/source/frontend/src/cytoscape/plugins/svg/svgCanvas.js @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import {Context as ParentContextClass} from 'svgcanvas'; + +export class Context extends ParentContextClass { + constructor(options) { + super(options); + // this parameter allows us to always embed SVG images present on the canvas, without it + // resource icons do not appear in the final exported canvas + this.embedImages = options?.embedImages ?? false; + } + + drawImage() { + //convert arguments to a real array + let args = Array.prototype.slice.call(arguments), + image = args[0], + dx, + dy, + dw, + dh, + sx = 0, + sy = 0, + sw, + sh, + parent, + svg, + defs, + group, + svgImage, + canvas, + context, + id; + + if (args.length === 3) { + dx = args[1]; + dy = args[2]; + sw = image.width; + sh = image.height; + dw = sw; + dh = sh; + } else if (args.length === 5) { + dx = args[1]; + dy = args[2]; + dw = args[3]; + dh = args[4]; + sw = image.width; + sh = image.height; + } else if (args.length === 9) { + sx = args[1]; + sy = args[2]; + sw = args[3]; + sh = args[4]; + dx = args[5]; + dy = args[6]; + dw = args[7]; + dh = args[8]; + } else { + throw new Error( + 'Invalid number of arguments passed to drawImage: ' + + arguments.length + ); + } + + parent = this.__closestGroupOrSvg(); + const matrix = this.getTransform().translate(dx, dy); + if (image instanceof Context) { + svg = image.getSvg().cloneNode(true); + if (svg.childNodes && svg.childNodes.length > 1) { + defs = svg.childNodes[0]; + while (defs.childNodes.length) { + id = defs.childNodes[0].getAttribute('id'); + this.__ids[id] = id; + this.__defs.appendChild(defs.childNodes[0]); + } + group = svg.childNodes[1]; + if (group) { + this.__applyTransformation(group, matrix); + parent.appendChild(group); + } + } + } else if (image.nodeName === 'CANVAS' || image.nodeName === 'IMG') { + //canvas or image + svgImage = this.__createElement('image'); + svgImage.setAttribute('width', dw); + svgImage.setAttribute('height', dh); + svgImage.setAttribute('preserveAspectRatio', 'none'); + + if ( + this.embedImages || + sx || + sy || + sw !== image.width || + sh !== image.height + ) { + //crop the image using a temporary canvas + canvas = this.__document.createElement('canvas'); + canvas.width = dw; + canvas.height = dh; + context = canvas.getContext('2d'); + context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh); + image = canvas; + } + this.__applyTransformation(svgImage, matrix); + svgImage.setAttributeNS( + 'http://www.w3.org/1999/xlink', + 'xlink:href', + image.nodeName === 'CANVAS' + ? image.toDataURL() + : image.getAttribute('src') + ); + parent.appendChild(svgImage); + } + } +} diff --git a/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/DrawDiagramPageExport.cy.js b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/DrawDiagramPageExport.cy.js new file mode 100644 index 00000000..577aca40 --- /dev/null +++ b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/DrawDiagramPageExport.cy.js @@ -0,0 +1,659 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import App from '../../../../../../App'; +import eksNodeGroup from '../../../../../mocks/fixtures/getResourceGraph/nodegroup.json'; +import {createSearchResourceHandler} from '../../../../../mocks/handlers'; +import {createSelfManagedPerspectiveMetadata} from '../../../../../vitest/testUtils'; +import sqsLambdaResourceGraph from '../../../../../mocks/fixtures/getResourceGraph/sqs-lambda.json'; +import {HttpResponse} from 'msw'; + +describe('Diagrams Page Local', () => { + const IS_CODE_BUILD = Cypress.env('IS_CODE_BUILD'); + + it('exports diagram to csv and json', () => { + window.perspectiveMetadata = createSelfManagedPerspectiveMetadata(); + + const {worker, graphql} = window.msw; + + worker.use( + graphql.query( + 'SearchResources', + createSearchResourceHandler([ + eksNodeGroup.getResourceGraph.nodes[0], + ]) + ), + graphql.query('GetResourceGraph', ({variables}) => { + const { + pagination: {start}, + } = variables; + + const { + getResourceGraph: {nodes, edges}, + } = eksNodeGroup; + + if (nodes.length < start && edges.length < start) { + return HttpResponse.json({ + data: { + getResourceGraph: { + nodes: [], + edges: [], + }, + }, + }); + } + return HttpResponse.json({data: eksNodeGroup}); + }) + ); + + cy.mount(); + + cy.findByRole('link', {name: /Manage$/, hidden: true}).click(); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /Create Diagram/i}); + + cy.findByRole('combobox', {name: /name/i}).type('CsvExportTestDiagram'); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /CsvExportTestDiagram/}); + + cy.findByRole('button', {name: /Resource search bar/i}).click(); + + cy.findByRole('combobox').type('eks'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{enter}'); + + cy.findByRole('button', {name: 'Search'}).click(); + + cy.get('.expand-collapse-canvas').scrollIntoView({duration: 2000}); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /save/i}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /diagram/i}).click(); + + cy.findByRole('menuitem', {name: /export/i}).click({force: true}); + + cy.findByRole('radio', {name: /csv/i}).click(); + + cy.findByTestId('export-diagram-modal-button').click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /diagram/i}).click(); + + cy.findByRole('menuitem', {name: /export/i}).click({force: true}); + + cy.findByRole('radio', {name: /json/i}).click(); + + cy.findByTestId('export-diagram-modal-button').click(); + + if (!IS_CODE_BUILD) { + cy.readFile('cypress/downloads/CsvExportTestDiagram.json') + .then(async ({nodes, edges}) => { + // we can see see floating point issues with the the graphing library position values on the + // canvas (e.g., a value that on most runs is 2 can come back as 1.9999999997) that makes this + // test unreliable so we round the number to eliminate this variance + const roundedNodes = nodes.map(({position, ...props}) => { + return { + position: { + x: parseFloat(position.x.toFixed(2)), + y: parseFloat(position.y.toFixed(2)), + }, + ...props, + }; + }); + + return { + nodes: roundedNodes, + edges, + }; + }) + .then(actual => { + const expectedJsonFilePath = `src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagram${IS_CODE_BUILD ? 'Ci' : 'Local'}.json`; + return cy + .readFile(expectedJsonFilePath) + .should('deep.equal', actual); + }); + } + + cy.readFile('cypress/downloads/CsvExportTestDiagram.csv').then( + actual => { + return cy + .readFile( + 'src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/CsvExportTestDiagram.csv' + ) + .should('deep.equal', actual); + } + ); + }); + + if (!IS_CODE_BUILD) { + it('exports diagram to svg', () => { + window.perspectiveMetadata = createSelfManagedPerspectiveMetadata(); + + const {worker, graphql} = window.msw; + + worker.use( + graphql.query( + 'SearchResources', + createSearchResourceHandler([ + eksNodeGroup.getResourceGraph.nodes[0], + ]) + ), + graphql.query('GetResourceGraph', ({variables}) => { + const { + pagination: {start}, + } = variables; + + const { + getResourceGraph: {nodes, edges}, + } = eksNodeGroup; + + if (nodes.length < start && edges.length < start) { + return HttpResponse.json({ + data: { + getResourceGraph: { + nodes: [], + edges: [], + }, + }, + }); + } + return HttpResponse.json({data: eksNodeGroup}); + }) + ); + + cy.mount(); + + cy.findByRole('link', {name: /Manage$/, hidden: true}).click(); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /Create Diagram/i}); + + cy.findByRole('combobox', {name: /name/i}).type( + 'ExportToSvgTestDiagram' + ); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', { + level: 2, + name: /ExportToSvgTestDiagram/, + }); + + cy.findByRole('button', {name: /Resource search bar/i}).click(); + + cy.findByRole('combobox').type('eks'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{enter}'); + + cy.findByRole('button', {name: 'Search'}).click(); + + cy.get('.expand-collapse-canvas').scrollIntoView({duration: 2000}); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /save/i}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /diagram/i}).click(); + + cy.findByRole('menuitem', {name: /export/i}).click({force: true}); + + cy.findByRole('radio', {name: /svg/i}).click(); + + cy.findByTestId('export-diagram-modal-button').click(); + + cy.readFile('cypress/downloads/ExportToSvgTestDiagram.svg').then( + actual => { + // floating point imprecision and autogenerated ids make the generation of the SVG + // non-deterministic so we must round numbers and normalize ids to make sure + // the test doesn't fail randomly + const normalized = actual + .replaceAll(/\d{1,5}\.?\d{2,20}/g, num => { + const rounded = Number.parseFloat(num).toFixed(2); + return parseFloat(rounded); + }) + .replaceAll( + /clip-path="url\(#\w{10,13}\)/g, + 'clip-path="url(#urlId)' + ) + .replaceAll( + /clipPath id="\w{12,15}"/g, + 'clipPath id="urlId"' + ); + + const expectedSvgFilePath = `src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagram${IS_CODE_BUILD ? 'Ci' : 'Local'}.svg`; + + return cy + .readFile(expectedSvgFilePath) + .should('deep.equal', normalized); + } + ); + }); + } + + it('should export ensure diagram to drawio button is enabled', () => { + window.perspectiveMetadata = createSelfManagedPerspectiveMetadata(); + + cy.mount().then(() => { + const {worker, graphql} = window.msw; + + worker.use( + graphql.query( + 'SearchResources', + createSearchResourceHandler([ + sqsLambdaResourceGraph.getResourceGraph.nodes[0], + ]) + ), + graphql.query('GetResourceGraph', ({variables}) => { + const { + pagination: {start}, + } = variables; + + const { + getResourceGraph: {nodes, edges}, + } = sqsLambdaResourceGraph; + + if (nodes.length < start && edges.length < start) { + return HttpResponse.json({ + data: { + getResourceGraph: { + nodes: [], + edges: [], + }, + }, + }); + } + return HttpResponse.json({data: sqsLambdaResourceGraph}); + }) + ); + }); + + cy.findByRole('link', {name: /Manage$/, hidden: true}).click(); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /Create Diagram/i}); + + cy.findByRole('combobox', {name: /name/i}).type( + 'ExportToDrawIoTestDiagram' + ); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /ExportToDrawIoTestDiagram/}); + + cy.findByRole('button', {name: /Resource search bar/i}).click(); + + cy.findByRole('combobox').type('lambda'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{enter}'); + + cy.findByRole('button', {name: 'Search'}).click(); + + cy.get('.expand-collapse-canvas').scrollIntoView({duration: 2000}); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /save/i}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /diagram/i}).click(); + + cy.findByRole('menuitem', {name: /export/i}).click({force: true}); + + cy.findByRole('radio', {name: /svg/i}).click(); + + cy.findByLabelText('File name'); + + cy.findByRole('radio', {name: /Diagrams.net/i}).click(); + + cy.findByLabelText('File name').should('not.exist'); + + cy.findByTestId('export-diagram-modal-button').should( + 'not.be.disabled' + ); + }); + + it('should export to myApplications', () => { + window.perspectiveMetadata = createSelfManagedPerspectiveMetadata(); + + cy.mount().then(() => { + const {worker, graphql} = window.msw; + + worker.use( + graphql.query( + 'SearchResources', + createSearchResourceHandler([ + sqsLambdaResourceGraph.getResourceGraph.nodes[0], + ]) + ), + graphql.query('GetResourceGraph', ({variables}) => { + const { + pagination: {start}, + } = variables; + + const { + getResourceGraph: {nodes, edges}, + } = sqsLambdaResourceGraph; + + if (nodes.length < start && edges.length < start) { + return HttpResponse.json({ + data: { + getResourceGraph: { + nodes: [], + edges: [], + }, + }, + }); + } + return HttpResponse.json({data: sqsLambdaResourceGraph}); + }) + ); + + cy.findByRole('link', {name: /Manage$/, hidden: true}).click(); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /Create Diagram/i}); + + cy.findByRole('combobox', {name: /name/i}).type( + 'ExportToMyApplicationsTestDiagram' + ); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', { + level: 2, + name: /ExportToMyApplicationsTestDiagram/, + }); + + cy.findByRole('button', {name: /Resource search bar/i}).click(); + + cy.findByRole('combobox').type('lambda'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{enter}'); + + cy.findByRole('button', {name: 'Search'}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /save/i}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /diagram/i}).click(); + + cy.findByRole('menuitem', {name: /export/i}).click({force: true}); + + cy.findByRole('radio', {name: /myapplications/i}).click(); + + cy.findByRole('button', {name: /account/i}).click(); + cy.findByRole('option', {name: /xxxxxxxxxxxx/i}).click(); + + cy.findByRole('button', {name: /region/i}).click(); + cy.findByRole('option', {name: /eu-west-1/i}).click(); + + cy.findByRole('form', {name: 'export'}).within(() => { + cy.findByRole('button', {name: /export/i}).click(); + }); + + cy.findByText(/Application created/i); + + cy.findByText( + 'The application named ExportToMyApplicationsTestDiagram has been created.' + ); + + cy.findByText( + 'However, the following resources were not added:' + ).should('not.exist'); + }); + }); + + it('should export to myApplications and report any unprocessed resources', () => { + window.perspectiveMetadata = createSelfManagedPerspectiveMetadata(); + + cy.mount().then(() => { + const {worker, graphql} = window.msw; + + worker.use( + graphql.query( + 'SearchResources', + createSearchResourceHandler([ + sqsLambdaResourceGraph.getResourceGraph.nodes[0], + ]) + ), + graphql.query('GetResourceGraph', ({variables}) => { + const { + pagination: {start}, + } = variables; + + const { + getResourceGraph: {nodes, edges}, + } = sqsLambdaResourceGraph; + + if (nodes.length < start && edges.length < start) { + return HttpResponse.json({ + data: { + getResourceGraph: { + nodes: [], + edges: [], + }, + }, + }); + } + return HttpResponse.json({data: sqsLambdaResourceGraph}); + }), + graphql.mutation('CreateApplication', ({variables}) => { + const {resources} = variables; + + return HttpResponse.json({ + data: { + createApplication: { + applicationTag: 'myApplicationTag', + name: variables.name, + unprocessedResources: [resources[0].id], + }, + }, + }); + }) + ); + + cy.findByRole('link', {name: /Manage$/, hidden: true}).click(); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /Create Diagram/i}); + + cy.findByRole('combobox', {name: /name/i}).type( + 'ExportToMyApplicationsUnprocessedTestDiagram' + ); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', { + level: 2, + name: /ExportToMyApplicationsUnprocessedTestDiagram/, + }); + + cy.findByRole('button', {name: /Resource search bar/i}).click(); + + cy.findByRole('combobox').type('lambda'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{enter}'); + + cy.findByRole('button', {name: 'Search'}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /save/i}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /diagram/i}).click(); + + cy.findByRole('menuitem', {name: /export/i}).click({force: true}); + + cy.findByRole('radio', {name: /myapplications/i}).click(); + + cy.findByRole('button', {name: /account/i}).click(); + cy.findByRole('option', {name: /xxxxxxxxxxxx/i}).click(); + + cy.findByRole('button', {name: /region/i}).click(); + cy.findByRole('option', {name: /eu-west-1/i}).click(); + + cy.findByRole('form', {name: 'export'}).within(() => { + cy.findByRole('button', {name: /export/i}).click(); + }); + + cy.findByText(/Application created/i); + + cy.findByText( + 'The application named ExportToMyApplicationsUnprocessedTestDiagram has been created. However, the following resources were not added: arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:sqs' + ); + }); + }); + + it('should return error if application with same name exists', () => { + window.perspectiveMetadata = createSelfManagedPerspectiveMetadata(); + + cy.mount().then(() => { + const {worker, graphql} = window.msw; + + worker.use( + graphql.query( + 'SearchResources', + createSearchResourceHandler([ + sqsLambdaResourceGraph.getResourceGraph.nodes[0], + ]) + ), + graphql.query('GetResourceGraph', ({variables}) => { + const { + pagination: {start}, + } = variables; + + const { + getResourceGraph: {nodes, edges}, + } = sqsLambdaResourceGraph; + + if (nodes.length < start && edges.length < start) { + return HttpResponse.json({ + data: { + getResourceGraph: { + nodes: [], + edges: [], + }, + }, + }); + } + return HttpResponse.json({data: sqsLambdaResourceGraph}); + }), + graphql.mutation('CreateApplication', ({variables}) => { + const {name} = variables; + + return HttpResponse.json({ + errors: [ + { + path: ['createApplication'], + data: null, + errorType: 'Lambda:Unhandled', + errorInfo: null, + locations: [ + {line: 2, column: 3, sourceName: null}, + ], + message: `An application with the name ${name} already exists.`, + }, + ], + }); + }) + ); + + cy.findByRole('link', {name: /Manage$/, hidden: true}).click(); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', {level: 2, name: /Create Diagram/i}); + + cy.findByRole('combobox', {name: /name/i}).type( + 'ExportToMyApplicationsExistsTestDiagram' + ); + + cy.findByRole('button', {name: /create/i}).click(); + + cy.findByRole('heading', { + level: 2, + name: /ExportToMyApplicationsExistsTestDiagram/, + }); + + cy.findByRole('button', {name: /Resource search bar/i}).click(); + + cy.findByRole('combobox').type('lambda'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{downArrow}'); + + cy.findByRole('combobox').type('{enter}'); + + cy.findByRole('button', {name: 'Search'}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /save/i}).click(); + + cy.findByRole('button', {name: /action/i}).click(); + + cy.findByRole('menuitem', {name: /diagram/i}).click(); + + cy.findByRole('menuitem', {name: /export/i}).click({force: true}); + + cy.findByRole('radio', {name: /myapplications/i}).click(); + + cy.findByRole('button', {name: /account/i}).click(); + cy.findByRole('option', {name: /xxxxxxxxxxxx/i}).click(); + + cy.findByRole('button', {name: /region/i}).click(); + cy.findByRole('option', {name: /eu-west-1/i}).click(); + + cy.findByRole('form', {name: 'export'}).within(() => { + cy.findByRole('button', {name: /export/i}).click(); + }); + + cy.findByText( + 'An application with the name ExportToMyApplicationsExistsTestDiagram already exists.' + ); + }); + }); +}); diff --git a/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramCi.svg b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramCi.svg new file mode 100644 index 00000000..422993d3 --- /dev/null +++ b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramCi.svg @@ -0,0 +1 @@ +xxxxxxxxxxxxeu-west-1globaltest-cluster-vpcRoleLaunchTemplateeu-west-1a,eu-west-1b,eu-west-1cNodegroupAutoScalingGroupClusterng-11111111test-cluster...test-cluster...eks-5ababb6a...test-cluster \ No newline at end of file diff --git a/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramLocal.svg b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramLocal.svg new file mode 100644 index 00000000..a86cbae2 --- /dev/null +++ b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/ExportToSvgTestDiagramLocal.svg @@ -0,0 +1 @@ +xxxxxxxxxxxxeu-west-1globaltest-cluster-vpcRoleLaunchTemplateeu-west-1a,eu-west-1b,eu-west-1cNodegroupAutoScalingGroupClusterng-11111111test-cluster...test-cluster...eks-5ababb6a...test-cluster \ No newline at end of file diff --git a/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramCi.json b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramCi.json new file mode 100644 index 00000000..4b8cecac --- /dev/null +++ b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramCi.json @@ -0,0 +1,864 @@ +{ + "nodes": [ + { + "data": { + "id": "xxxxxxxxxxxx", + "title": "xxxxxxxxxxxx", + "label": "xxxxxxxxxxxx", + "plainLabel": "xxxxxxxxxxxx", + "type": "account", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/AWS-Cloud-alt_light-bg.svg", + "clickedId": "xxxxxxxxxxxx", + "cost": 0, + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": -43, + "y": 33.5 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "account removeAll _gridParentPadding" + }, + { + "data": { + "id": "xxxxxxxxxxxx-eu-west-1", + "parent": "xxxxxxxxxxxx", + "title": "eu-west-1", + "label": "eu-west-1", + "plainLabel": "eu-west-1", + "type": "region", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/Region_light-bg.svg", + "clickedId": "xxxxxxxxxxxx-eu-west-1", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": -109.5, + "y": 33.5 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "region removeAll _gridParentPadding" + }, + { + "data": { + "id": "xxxxxxxxxxxx-global", + "parent": "xxxxxxxxxxxx", + "title": "global", + "label": "global", + "plainLabel": "global", + "type": "region", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/Region_light-bg.svg", + "clickedId": "xxxxxxxxxxxx-global", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(175, 17, 83, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": 160, + "y": -90.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "region removeAll _gridParentPadding" + }, + { + "data": { + "id": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111", + "title": "eu-west-1a,eu-west-1b,eu-west-1c", + "label": "eu-west-1a,eu-west-1b,eu-west-1c", + "plainLabel": "eu-west-1a,eu-west-1b,eu-west-1c", + "type": "availabilityZone", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/availabilityZone.svg", + "clickedId": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": -154.5, + "y": 55 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "availabilityZone removeAll _gridParentPadding" + }, + { + "data": { + "id": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111", + "parent": "xxxxxxxxxxxx-eu-west-1", + "title": "test-cluster-vpc", + "label": "test-cluster-vpc", + "plainLabel": "test-cluster-vpc", + "type": "vpc", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/VPC-collapsed.svg", + "clickedId": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814", + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:vpc/vpc-11111111111111111", + "availabilityZone": "Multiple Availability Zones", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2023-05-30T22:16:37.179Z", + "configurationItemStatus": "OK", + "configurationStateId": null, + "resourceCreationTime": null, + "resourceId": "vpc-11111111111111111", + "resourceName": null, + "resourceType": "AWS::EC2::VPC", + "supplementaryConfiguration": null, + "tags": "[]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/vpc/v2/home?region=eu-west-1#vpcs:sort=VpcId", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/vpc?region=eu-west-1#vpcs:sort=VpcId", + "title": "test-cluster-vpc", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + }, + "resource": { + "id": "vpc-11111111111111111", + "name": null, + "value": null, + "type": "AWS::EC2::VPC", + "tags": "[]", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:vpc/vpc-11111111111111111", + "region": "eu-west-1", + "state": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/vpc/v2/home?region=eu-west-1#vpcs:sort=VpcId", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/vpc?region=eu-west-1#vpcs:sort=VpcId", + "accountId": "xxxxxxxxxxxx" + } + }, + "position": { + "x": -154.5, + "y": 55 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "vpc removeAll _gridParentPadding" + }, + { + "data": { + "id": "Nodegroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "title": "Nodegroup", + "label": "Nodegroup", + "plainLabel": "Nodegroup", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "Nodegroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": -106, + "y": -4.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "data": { + "id": "Role-xxxxxxxxxxxx-global", + "parent": "xxxxxxxxxxxx-global", + "title": "Role", + "label": "Role", + "plainLabel": "Role", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "Role-xxxxxxxxxxxx-global", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": 160, + "y": -90.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "data": { + "id": "LaunchTemplate-xxxxxxxxxxxx-eu-west-1", + "parent": "xxxxxxxxxxxx-eu-west-1", + "title": "LaunchTemplate", + "label": "LaunchTemplate", + "plainLabel": "LaunchTemplate", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "LaunchTemplate-xxxxxxxxxxxx-eu-west-1", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": 27, + "y": -90.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "data": { + "id": "AutoScalingGroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "title": "AutoScalingGroup", + "label": "AutoScalingGroup", + "plainLabel": "AutoScalingGroup", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "AutoScalingGroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": -201, + "y": -4.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "data": { + "id": "Cluster-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "title": "Cluster", + "label": "Cluster", + "plainLabel": "Cluster", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "Cluster-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "position": { + "x": -203, + "y": 114.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "data": { + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceId": [ + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3" + ], + "parent": "Nodegroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "title": "ng-11111111", + "label": "ng-11111111", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "image": "/icons/Arch_Amazon-EKS-Distro_64.svg", + "cost": 0, + "private": null, + "resource": { + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "name": "ng-11111111", + "value": null, + "type": "AWS::EKS::Nodegroup", + "tags": "[]", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "region": "eu-west-1", + "state": null, + "loggedInURL": null, + "loginURL": null, + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "availabilityZone": "eu-west-1a,eu-west-1b,eu-west-1c", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": null, + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": null, + "resourceCreationTime": null, + "resourceId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceName": "ng-11111111", + "resourceType": "AWS::EKS::Nodegroup", + "supplementaryConfiguration": null, + "tags": "[]", + "version": null, + "vpcId": "vpc-11111111111111111", + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "ng-11111111", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + }, + "selected": true + }, + "position": { + "x": -106, + "y": -4.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "data": { + "arn": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "resourceId": [ + "BBBBBBBBBBBBBB", + "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole" + ], + "parent": "Role-xxxxxxxxxxxx-global", + "id": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "title": "test-cluster-NodeInstanceRole", + "label": "test-cluster...", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(175, 17, 83, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "image": "/icons/AWS-Identity-and-Access-Management-IAM_Role_light-bg.svg", + "cost": 0, + "private": null, + "resource": { + "id": "BBBBBBBBBBBBBB", + "name": "test-cluster-NodeInstanceRole", + "value": null, + "type": "AWS::IAM::Role", + "tags": "[]", + "arn": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "region": "global", + "state": null, + "loggedInURL": "https://console.aws.amazon.com/iam/home?#/roles", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/roles", + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "availabilityZone": "Not Applicable", + "awsRegion": "global", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2022-02-14T13:50:59.294Z", + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": null, + "resourceCreationTime": "2020-10-30T00:07:24.000Z", + "resourceId": "BBBBBBBBBBBBBB", + "resourceName": "test-cluster-NodeInstanceRole", + "resourceType": "AWS::IAM::Role", + "supplementaryConfiguration": null, + "tags": "[]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": "https://console.aws.amazon.com/iam/home?#/roles", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/roles", + "title": "test-cluster-NodeInstanceRole", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "position": { + "x": 160, + "y": -90.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "data": { + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "resourceId": [ + "lt-11111111111111111", + "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111" + ], + "parent": "LaunchTemplate-xxxxxxxxxxxx-eu-west-1", + "id": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "title": "test-cluster--nodegroup-ng-11111111", + "label": "test-cluster...", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "image": "/icons/Res_AWS-EC2_Launch_Template_48_Light.svg", + "cost": 0, + "private": null, + "resource": { + "id": "lt-11111111111111111", + "name": "test-cluster--nodegroup-ng-11111111", + "value": null, + "type": "AWS::EC2::LaunchTemplate", + "tags": "[]", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "region": "eu-west-1", + "state": null, + "loggedInURL": null, + "loginURL": null, + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "availabilityZone": "Regional", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2022-02-22T10:12:29.829Z", + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": "1645524749829", + "resourceCreationTime": null, + "resourceId": "lt-11111111111111111", + "resourceName": "test-cluster--nodegroup-ng-11111111", + "resourceType": "AWS::EC2::LaunchTemplate", + "supplementaryConfiguration": "\"{}\"", + "tags": "[]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "test-cluster--nodegroup-ng-11111111", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "position": { + "x": 27, + "y": -90.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "data": { + "arn": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceId": [ + "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3" + ], + "parent": "AutoScalingGroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "id": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "title": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "label": "eks-5ababb6a...", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "image": "/icons/Amazon-EC2-Auto-Scaling.svg", + "cost": 0, + "private": null, + "resource": { + "id": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "name": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "value": null, + "type": "AWS::AutoScaling::AutoScalingGroup", + "tags": "[]", + "arn": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "region": "eu-west-1", + "state": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/ec2/home/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/ec2/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "availabilityZone": "eu-west-1a,eu-west-1b,eu-west-1c", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2023-01-03T23:54:04.184Z", + "configurationItemStatus": "OK", + "configurationStateId": null, + "resourceCreationTime": "2020-10-30T00:09:06.968Z", + "resourceId": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceName": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceType": "AWS::AutoScaling::AutoScalingGroup", + "supplementaryConfiguration": null, + "tags": "[]", + "version": "1.3", + "vpcId": "vpc-11111111111111111", + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/ec2/home/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/ec2/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "title": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "position": { + "x": -201, + "y": -4.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "data": { + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "resourceId": [ + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster" + ], + "parent": "Cluster-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "title": "test-cluster", + "label": "test-cluster", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "image": "/icons/Amazon-Elastic-Kubernetes-Service-menu.svg", + "cost": 0, + "private": null, + "resource": { + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "name": "test-cluster", + "value": null, + "type": "AWS::EKS::Cluster", + "tags": "[]", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "region": "eu-west-1", + "state": null, + "loggedInURL": null, + "loginURL": null, + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "availabilityZone": "eu-west-1a,eu-west-1b,eu-west-1c", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2023-03-11T00:54:04.337Z", + "configurationItemStatus": "OK", + "configurationStateId": "1666054444349", + "resourceCreationTime": null, + "resourceId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "resourceName": "test-cluster", + "resourceType": "AWS::EKS::Cluster", + "supplementaryConfiguration": "\"{}\"", + "tags": "[]", + "version": "1.3", + "vpcId": "vpc-11111111111111111", + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "test-cluster", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "position": { + "x": -203, + "y": 114.75 + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + } + ], + "edges": [ + { + "data": { + "id": "c6c2d776-016e-d989-5de4-4233bb148cc5", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + }, + { + "data": { + "id": "26c2d776-016e-965a-74db-03e8b99f2dd7", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + }, + { + "data": { + "id": "04c2d776-016e-6eaf-6e94-28d3c6ec465f", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + }, + { + "data": { + "id": "e0c2d776-016d-e0ac-9ba1-767fc246ec8e", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + } + ] +} \ No newline at end of file diff --git a/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramLocal.json b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramLocal.json new file mode 100644 index 00000000..31df552e --- /dev/null +++ b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__file_snapshots__/JsonExportTestDiagramLocal.json @@ -0,0 +1,864 @@ +{ + "nodes": [ + { + "position": { + "x": -43, + "y": 33.5 + }, + "data": { + "id": "xxxxxxxxxxxx", + "title": "xxxxxxxxxxxx", + "label": "xxxxxxxxxxxx", + "plainLabel": "xxxxxxxxxxxx", + "type": "account", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/AWS-Cloud-alt_light-bg.svg", + "clickedId": "xxxxxxxxxxxx", + "cost": 0, + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "account removeAll _gridParentPadding" + }, + { + "position": { + "x": -109.5, + "y": 33.5 + }, + "data": { + "id": "xxxxxxxxxxxx-eu-west-1", + "parent": "xxxxxxxxxxxx", + "title": "eu-west-1", + "label": "eu-west-1", + "plainLabel": "eu-west-1", + "type": "region", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/Region_light-bg.svg", + "clickedId": "xxxxxxxxxxxx-eu-west-1", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "region removeAll _gridParentPadding" + }, + { + "position": { + "x": 158, + "y": -90.75 + }, + "data": { + "id": "xxxxxxxxxxxx-global", + "parent": "xxxxxxxxxxxx", + "title": "global", + "label": "global", + "plainLabel": "global", + "type": "region", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/Region_light-bg.svg", + "clickedId": "xxxxxxxxxxxx-global", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(175, 17, 83, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "region removeAll _gridParentPadding" + }, + { + "position": { + "x": -154.5, + "y": 55 + }, + "data": { + "id": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111", + "title": "eu-west-1a,eu-west-1b,eu-west-1c", + "label": "eu-west-1a,eu-west-1b,eu-west-1c", + "plainLabel": "eu-west-1a,eu-west-1b,eu-west-1c", + "type": "availabilityZone", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/availabilityZone.svg", + "clickedId": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "availabilityZone removeAll _gridParentPadding" + }, + { + "position": { + "x": -154.5, + "y": 55 + }, + "data": { + "id": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111", + "parent": "xxxxxxxxxxxx-eu-west-1", + "title": "test-cluster-vpc", + "label": "test-cluster-vpc", + "plainLabel": "test-cluster-vpc", + "type": "vpc", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "image": "/icons/VPC-collapsed.svg", + "clickedId": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814", + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:vpc/vpc-11111111111111111", + "availabilityZone": "Multiple Availability Zones", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2023-05-30T22:16:37.179Z", + "configurationItemStatus": "OK", + "configurationStateId": null, + "resourceCreationTime": null, + "resourceId": "vpc-11111111111111111", + "resourceName": null, + "resourceType": "AWS::EC2::VPC", + "supplementaryConfiguration": null, + "tags": "[]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/vpc/v2/home?region=eu-west-1#vpcs:sort=VpcId", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/vpc?region=eu-west-1#vpcs:sort=VpcId", + "title": "test-cluster-vpc", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + }, + "resource": { + "id": "vpc-11111111111111111", + "name": null, + "value": null, + "type": "AWS::EC2::VPC", + "tags": "[]", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:vpc/vpc-11111111111111111", + "region": "eu-west-1", + "state": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/vpc/v2/home?region=eu-west-1#vpcs:sort=VpcId", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/vpc?region=eu-west-1#vpcs:sort=VpcId", + "accountId": "xxxxxxxxxxxx" + } + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "vpc removeAll _gridParentPadding" + }, + { + "position": { + "x": -201, + "y": -4.75 + }, + "data": { + "id": "Nodegroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "title": "Nodegroup", + "label": "Nodegroup", + "plainLabel": "Nodegroup", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "Nodegroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "position": { + "x": 158, + "y": -90.75 + }, + "data": { + "id": "Role-xxxxxxxxxxxx-global", + "parent": "xxxxxxxxxxxx-global", + "title": "Role", + "label": "Role", + "plainLabel": "Role", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "Role-xxxxxxxxxxxx-global", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "position": { + "x": 25, + "y": -90.75 + }, + "data": { + "id": "LaunchTemplate-xxxxxxxxxxxx-eu-west-1", + "parent": "xxxxxxxxxxxx-eu-west-1", + "title": "LaunchTemplate", + "label": "LaunchTemplate", + "plainLabel": "LaunchTemplate", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "LaunchTemplate-xxxxxxxxxxxx-eu-west-1", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "position": { + "x": -108, + "y": -4.75 + }, + "data": { + "id": "AutoScalingGroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "title": "AutoScalingGroup", + "label": "AutoScalingGroup", + "plainLabel": "AutoScalingGroup", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "AutoScalingGroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "position": { + "x": -201, + "y": 114.75 + }, + "data": { + "id": "Cluster-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "parent": "arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "title": "Cluster", + "label": "Cluster", + "plainLabel": "Cluster", + "type": "type", + "borderStyle": "solid", + "color": "#fff", + "borderColour": "#AAB7B8", + "opacity": "0", + "clickedId": "Cluster-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "cost": 0, + "accountColour": "rgba(175, 17, 83, 0.5)", + "regionColour": "rgba(9, 86, 99, 0.5)", + "aZColour": "#00A1C9", + "subnetColour": "#248814" + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "type removeAll _gridParentPadding" + }, + { + "position": { + "x": -201, + "y": -4.75 + }, + "data": { + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceId": [ + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3" + ], + "parent": "Nodegroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "title": "ng-11111111", + "label": "ng-11111111", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "image": "/icons/Arch_Amazon-EKS-Distro_64.svg", + "cost": 0, + "private": null, + "resource": { + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "name": "ng-11111111", + "value": null, + "type": "AWS::EKS::Nodegroup", + "tags": "[]", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "region": "eu-west-1", + "state": null, + "loggedInURL": null, + "loginURL": null, + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "availabilityZone": "eu-west-1a,eu-west-1b,eu-west-1c", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": null, + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": null, + "resourceCreationTime": null, + "resourceId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceName": "ng-11111111", + "resourceType": "AWS::EKS::Nodegroup", + "supplementaryConfiguration": null, + "tags": "[]", + "version": null, + "vpcId": "vpc-11111111111111111", + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "ng-11111111", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + }, + "selected": true + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "position": { + "x": 158, + "y": -90.75 + }, + "data": { + "arn": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "resourceId": [ + "BBBBBBBBBBBBBB", + "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole" + ], + "parent": "Role-xxxxxxxxxxxx-global", + "id": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "title": "test-cluster-NodeInstanceRole", + "label": "test-cluster...", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(175, 17, 83, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "image": "/icons/AWS-Identity-and-Access-Management-IAM_Role_light-bg.svg", + "cost": 0, + "private": null, + "resource": { + "id": "BBBBBBBBBBBBBB", + "name": "test-cluster-NodeInstanceRole", + "value": null, + "type": "AWS::IAM::Role", + "tags": "[]", + "arn": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "region": "global", + "state": null, + "loggedInURL": "https://console.aws.amazon.com/iam/home?#/roles", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/roles", + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole", + "availabilityZone": "Not Applicable", + "awsRegion": "global", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2022-02-14T13:50:59.294Z", + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": null, + "resourceCreationTime": "2020-10-30T00:07:24.000Z", + "resourceId": "BBBBBBBBBBBBBB", + "resourceName": "test-cluster-NodeInstanceRole", + "resourceType": "AWS::IAM::Role", + "supplementaryConfiguration": null, + "tags": "[]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": "https://console.aws.amazon.com/iam/home?#/roles", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/iam?home?#/roles", + "title": "test-cluster-NodeInstanceRole", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "position": { + "x": 25, + "y": -90.75 + }, + "data": { + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "resourceId": [ + "lt-11111111111111111", + "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111" + ], + "parent": "LaunchTemplate-xxxxxxxxxxxx-eu-west-1", + "id": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "title": "test-cluster--nodegroup-ng-11111111", + "label": "test-cluster...", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "image": "/icons/Res_AWS-EC2_Launch_Template_48_Light.svg", + "cost": 0, + "private": null, + "resource": { + "id": "lt-11111111111111111", + "name": "test-cluster--nodegroup-ng-11111111", + "value": null, + "type": "AWS::EC2::LaunchTemplate", + "tags": "[]", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "region": "eu-west-1", + "state": null, + "loggedInURL": null, + "loginURL": null, + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111", + "availabilityZone": "Regional", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2022-02-22T10:12:29.829Z", + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": "1645524749829", + "resourceCreationTime": null, + "resourceId": "lt-11111111111111111", + "resourceName": "test-cluster--nodegroup-ng-11111111", + "resourceType": "AWS::EC2::LaunchTemplate", + "supplementaryConfiguration": "\"{}\"", + "tags": "[]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "test-cluster--nodegroup-ng-11111111", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "position": { + "x": -108, + "y": -4.75 + }, + "data": { + "arn": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceId": [ + "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3" + ], + "parent": "AutoScalingGroup-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "id": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "title": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "label": "eks-5ababb6a...", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "image": "/icons/Amazon-EC2-Auto-Scaling.svg", + "cost": 0, + "private": null, + "resource": { + "id": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "name": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "value": null, + "type": "AWS::AutoScaling::AutoScalingGroup", + "tags": "[]", + "arn": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "region": "eu-west-1", + "state": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/ec2/home/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/ec2/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "availabilityZone": "eu-west-1a,eu-west-1b,eu-west-1c", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2023-01-03T23:54:04.184Z", + "configurationItemStatus": "OK", + "configurationStateId": null, + "resourceCreationTime": "2020-10-30T00:09:06.968Z", + "resourceId": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceName": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "resourceType": "AWS::AutoScaling::AutoScalingGroup", + "supplementaryConfiguration": null, + "tags": "[]", + "version": "1.3", + "vpcId": "vpc-11111111111111111", + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/ec2/home/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/ec2/autoscaling/home?region=eu-west-1#AutoScalingGroups:id=eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3;view=details", + "title": "eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + }, + { + "position": { + "x": -201, + "y": 114.75 + }, + "data": { + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "resourceId": [ + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster" + ], + "parent": "Cluster-arn-aws-ec2-eu-west-1-xxxxxxxxxxxx-vpc/vpc-11111111111111111-eu-west-1a,eu-west-1b,eu-west-1c", + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "title": "test-cluster", + "label": "test-cluster", + "shape": "image", + "type": "resource", + "accountColour": "rgba(15, 153, 10, 0.5)", + "regionColour": "rgba(79, 16, 163, 0.5)", + "color": "#fff", + "borderStyle": "solid", + "borderColour": "#545B64", + "borderOpacity": 0.25, + "borderSize": 1, + "opacity": "0", + "clickedId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "image": "/icons/Amazon-Elastic-Kubernetes-Service-menu.svg", + "cost": 0, + "private": null, + "resource": { + "id": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "name": "test-cluster", + "value": null, + "type": "AWS::EKS::Cluster", + "tags": "[]", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "region": "eu-west-1", + "state": null, + "loggedInURL": null, + "loginURL": null, + "accountId": "xxxxxxxxxxxx" + }, + "highlight": true, + "existing": false, + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "availabilityZone": "eu-west-1a,eu-west-1b,eu-west-1c", + "awsRegion": "eu-west-1", + "configuration": "\"{}\"", + "configurationItemCaptureTime": "2023-03-11T00:54:04.337Z", + "configurationItemStatus": "OK", + "configurationStateId": "1666054444349", + "resourceCreationTime": null, + "resourceId": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster", + "resourceName": "test-cluster", + "resourceType": "AWS::EKS::Cluster", + "supplementaryConfiguration": "\"{}\"", + "tags": "[]", + "version": "1.3", + "vpcId": "vpc-11111111111111111", + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "test-cluster", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + "group": "nodes", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": false, + "classes": "resource image selectable hoverover" + } + ], + "edges": [ + { + "data": { + "id": "c6c2d776-016e-d989-5de4-4233bb148cc5", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:iam::xxxxxxxxxxxx:role/test-cluster-NodeInstanceRole" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + }, + { + "data": { + "id": "26c2d776-016e-965a-74db-03e8b99f2dd7", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:ec2:eu-west-1:xxxxxxxxxxxx:launch-template/lt-11111111111111111" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + }, + { + "data": { + "id": "04c2d776-016e-6eaf-6e94-28d3c6ec465f", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:autoscaling:eu-west-1:xxxxxxxxxxxx:autoScalingGroup:cc4a478a-ce27-43ad-b468-3b5f3a4013e6:autoScalingGroupName/eks-5ababb6a-75d5-8af7-0351-02c55b0db9f3" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + }, + { + "data": { + "id": "e0c2d776-016d-e0ac-9ba1-767fc246ec8e", + "source": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:nodegroup/test-cluster/ng-11111111/5ababb6a-75d5-8af7-0351-02c55b0db9f3", + "target": "arn:aws:eks:eu-west-1:xxxxxxxxxxxx:cluster/test-cluster" + }, + "position": { + "x": 0, + "y": 0 + }, + "group": "edges", + "removed": false, + "selected": false, + "selectable": true, + "locked": false, + "grabbable": true, + "pannable": true, + "classes": "" + } + ] +} \ No newline at end of file diff --git a/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__image_snapshots__/Diagrams Page should allow resources to be added to an existing diagram #0.png b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__image_snapshots__/Diagrams Page should allow resources to be added to an existing diagram #0.png new file mode 100644 index 0000000000000000000000000000000000000000..0fafdddb7bd75a9898de1d0fae1a7445250357fb GIT binary patch literal 122240 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

&B9pXER30zfbf@ix>J)+_$QopROh#%_~a{3oc6o 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
XsB7XKhNX;oS&j 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%0DKWtl3{fnspcZttH16IrX=NH0!hhu~1^(#q6yPlP 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 { + function renderModal(component, diagramTitle = 'myDiagram') { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchInterval: 60000, + refetchOnWindowFocus: false, + retry: 1, + }, + }, + }); + + const history = createMemoryHistory(); + history.location = { + pathname: `/diagrams/private/${diagramTitle}`, + }; + + const container = render( + + + + + {component} + + + + + ); + + return {user: userEvent.setup(), container, history}; + } + + const elements = { + nodes: [ + { + data: { + id: 'arn:s3BucketArn', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_1, + awsRegion: EU_WEST_1, + resourceType: 'AWS::S3::Bucket', + }, + }, + }, + { + data: { + id: 'arn:s3BucketArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_1, + awsRegion: EU_WEST_1, + resourceType: 'AWS::S3::Bucket', + }, + }, + }, + { + data: { + id: 'arn:s3BucketArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_2, + awsRegion: EU_WEST_1, + resourceType: 'AWS::S3::Bucket', + }, + }, + }, + { + data: { + id: 'arn:ec2InstanceArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_2, + awsRegion: EU_WEST_2, + resourceType: 'AWS::EC2::Instance', + }, + }, + }, + { + data: { + id: 'arn:lambdaArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_3, + awsRegion: EU_WEST_1, + resourceType: 'AWS::Lambda::Function', + }, + }, + }, + { + data: { + id: 'arn:lambdaArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_3, + awsRegion: EU_WEST_2, + resourceType: 'AWS::Lambda::Function', + }, + }, + }, + { + data: { + id: 'arn:snsTopicArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_3, + awsRegion: US_EAST_1, + resourceType: 'AWS::SNS::Topic', + }, + }, + }, + ], + edges: [], + }; + + describe('Export to myApplication', () => { + it('should not be possible to create an application without providing a name, account ID and region', async () => { + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + const applicationNameTextBox = screen.getByRole('textbox', { + name: /application name/i, + }); + expect(applicationNameTextBox).toHaveValue('myDiagram'); + + const exportForm = screen.getByRole('form', {name: 'export'}); + + const exportButton = within(exportForm).getByRole('button', { + name: /export/i, + }); + await waitFor(() => expect(exportButton).toBeDisabled()); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /111111111111/i}) + ); + + await waitFor(() => expect(exportButton).toBeDisabled()); + + await user.click(screen.getByRole('button', {name: /region/i})); + await user.click(screen.getByRole('option', {name: /eu-west-1/i})); + + await waitFor(() => expect(exportButton).toBeEnabled()); + }); + + it('should use the diagram name as the default but allow it to be changed', async () => { + const applicationName = 'newDiagramName'; + + let name = null; + + server.use( + graphql.mutation('CreateApplication', ({variables}) => { + name = variables.name; + + return HttpResponse.json({ + data: { + createApplication: { + applicationTag: 'myApplicationTag', + name, + unprocessedResources: [], + }, + }, + }); + }) + ); + + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + const nameTextBox = screen.getByRole('textbox', { + name: /application name/i, + }); + expect(nameTextBox).toHaveValue('myDiagram'); + + await user.clear(nameTextBox); + await user.type(nameTextBox, applicationName); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /333333333333/i}) + ); + + await user.click(screen.getByRole('button', {name: /region/i})); + await user.click(screen.getByRole('option', {name: /us-east-1/i})); + + const exportForm = screen.getByRole('form', {name: 'export'}); + const exportButton = within(exportForm).getByRole('button', { + name: /export/i, + }); + await user.click(exportButton); + + await waitFor(() => expect(name).toEqual(applicationName)); + }); + + it('should use transform the diagram name to a suitable default application name', async () => { + const applicationName = 'name-with-spaces'; + + let name = null; + + server.use( + graphql.mutation('CreateApplication', ({variables}) => { + name = variables.name; + + return HttpResponse.json({ + data: { + createApplication: { + applicationTag: 'myApplicationTag', + name, + unprocessedResources: [], + }, + }, + }); + }) + ); + + const {user} = renderModal( + , + 'name with spaces' + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + const nameTextBox = screen.getByRole('textbox', { + name: /application name/i, + }); + expect(nameTextBox).toHaveValue('name-with-spaces'); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /333333333333/i}) + ); + + await user.click(screen.getByRole('button', {name: /region/i})); + await user.click(screen.getByRole('option', {name: /us-east-1/i})); + + const exportForm = screen.getByRole('form', {name: 'export'}); + const exportButton = within(exportForm).getByRole('button', { + name: /export/i, + }); + await user.click(exportButton); + + await waitFor(() => expect(name).toEqual(applicationName)); + }); + it('should use prevent the user exporting a diagram with an invalid name', async () => { + const invalidApplicationName = 'Name with spaces'; + + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + const nameTextBox = screen.getByRole('textbox', { + name: /application name/i, + }); + + await user.clear(nameTextBox); + await user.type(nameTextBox, invalidApplicationName); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /333333333333/i}) + ); + + await user.click(screen.getByRole('button', {name: /region/i})); + await user.click(screen.getByRole('option', {name: /us-east-1/i})); + + const exportForm = screen.getByRole('form', {name: 'export'}); + const exportButton = within(exportForm).getByRole('button', { + name: /export/i, + }); + expect(exportButton).toBeDisabled(); + }); + + it('should not show global region in region dropdown list', async () => { + const iamRoleResource = { + data: { + id: 'arn:roleArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_1, + awsRegion: 'global', + resourceType: 'AWS::IAM::Role', + }, + }, + }; + + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /111111111111/i}) + ); + + await user.click(screen.getByRole('button', {name: /region/i})); + expect(screen.getAllByRole('option')).toHaveLength(1); + screen.getByRole('option', {name: /eu-west-1/i}); + expect(screen.queryByRole('option', {name: /global/i})).toBeNull(); + }); + + it('should only show regions associated with their account', async () => { + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /111111111111/i}) + ); + await user.click(screen.getByRole('button', {name: /region/i})); + expect(screen.getAllByRole('option')).toHaveLength(1); + screen.getByRole('option', {name: /eu-west-1/i}); + + await user.click( + screen.getByRole('button', {name: /account 111111111111/i}) + ); + await user.click( + screen.getByRole('option', {name: /222222222222/i}) + ); + await user.click(screen.getByRole('button', {name: /region/i})); + expect(screen.getAllByRole('option')).toHaveLength(2); + screen.getByRole('option', {name: /eu-west-1/i}); + screen.getByRole('option', {name: /eu-west-2/i}); + + await user.click( + screen.getByRole('button', {name: /account 222222222222/i}) + ); + await user.click( + screen.getByRole('option', {name: /333333333333/i}) + ); + await user.click(screen.getByRole('button', {name: /region/i})); + expect(screen.getAllByRole('option')).toHaveLength(3); + screen.getByRole('option', {name: /eu-west-1/i}); + screen.getByRole('option', {name: /eu-west-2/i}); + screen.getByRole('option', {name: /us-east-1/i}); + }); + + it('should should filter out pseudo-resource types', async () => { + const elements = { + nodes: [ + { + data: { + id: 'arn:TagcArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_3, + awsRegion: 'global', + resourceType: 'AWS::Tags::Tag', + }, + }, + }, + { + data: { + id: 'arn:TagcArn1', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_3, + awsRegion: 'global', + resourceType: 'AWS::IAM::InlinePolicy', + }, + }, + }, + { + data: { + id: 'arn:s3BucketArn', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_1, + awsRegion: EU_WEST_1, + resourceType: 'AWS::S3::Bucket', + }, + }, + }, + ], + }; + + const resources = []; + + server.use( + graphql.mutation('CreateApplication', ({variables}) => { + resources.push(...variables.resources); + + return HttpResponse.json({ + data: { + createApplication: { + applicationTag: 'myApplicationTag', + name, + unprocessedResources: [], + }, + }, + }); + }) + ); + + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + await user.click(screen.getByRole('button', {name: /account/i})); + expect( + screen.queryByRole('option', {name: /333333333333/i}) + ).toBeNull(); + await user.click( + screen.getByRole('option', {name: /111111111111/i}) + ); + + await user.click(screen.getByRole('button', {name: /region/i})); + await user.click(screen.getByRole('option', {name: EU_WEST_1})); + + const exportForm = screen.getByRole('form', {name: 'export'}); + const exportButton = within(exportForm).getByRole('button', { + name: /export/i, + }); + await user.click(exportButton); + + await waitFor(() => { + expect(resources).toEqual([ + { + id: 'arn:s3BucketArn', + accountId: ACCOUNT_ID_1, + region: EU_WEST_1, + }, + ]); + }); + }); + + it('should should filter out non-resource types and resources with invalid ARNs', async () => { + const elements = { + nodes: [ + { + data: { + type: 'account', + }, + }, + { + data: { + type: 'region', + }, + }, + { + data: { + id: 's3BucketInvalidArn', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_1, + awsRegion: EU_WEST_1, + resourceType: 'AWS::S3::Bucket', + }, + }, + }, + { + data: { + id: 'arn:s3BucketArn', + type: 'resource', + properties: { + accountId: ACCOUNT_ID_1, + awsRegion: EU_WEST_1, + resourceType: 'AWS::S3::Bucket', + }, + }, + }, + ], + }; + + const resources = []; + + server.use( + graphql.mutation('CreateApplication', ({variables}) => { + resources.push(...variables.resources); + + return HttpResponse.json({ + data: { + createApplication: { + applicationTag: 'myApplicationTag', + name, + unprocessedResources: [], + }, + }, + }); + }) + ); + + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /111111111111/i}) + ); + + await user.click(screen.getByRole('button', {name: /region/i})); + await user.click(screen.getByRole('option', {name: /eu-west-1/i})); + + const exportForm = screen.getByRole('form', {name: 'export'}); + const exportButton = within(exportForm).getByRole('button', { + name: /export/i, + }); + await user.click(exportButton); + + await waitFor(() => { + expect(resources).toEqual([ + { + id: 'arn:s3BucketArn', + accountId: ACCOUNT_ID_1, + region: EU_WEST_1, + }, + ]); + }); + }); + + it('should export application to myApplications', async () => { + const {user} = renderModal( + + ); + + const myApplicationsRadioButton = await screen.findByRole('radio', { + name: /myapplications/i, + }); + + await user.click(myApplicationsRadioButton); + + await user.click(screen.getByRole('button', {name: /account/i})); + await user.click( + screen.getByRole('option', {name: /333333333333/i}) + ); + + await user.click(screen.getByRole('button', {name: /region/i})); + await user.click(screen.getByRole('option', {name: /us-east-1/i})); + + const exportForm = screen.getByRole('form', {name: 'export'}); + const exportButton = within(exportForm).getByRole('button', { + name: /export/i, + }); + await user.click(exportButton); + }); + }); +});