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 00000000..0fafdddb Binary files /dev/null and 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 differ diff --git a/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__image_snapshots__/Diagrams Page shows preview of diagram before creation #0.png b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__image_snapshots__/Diagrams Page shows preview of diagram before creation #0.png new file mode 100644 index 00000000..4fc8c998 Binary files /dev/null and b/source/frontend/src/tests/cypress/components/Diagrams/Draw/DrawDiagram/__image_snapshots__/Diagrams Page shows preview of diagram before creation #0.png differ diff --git a/source/frontend/src/tests/mocks/fixtures/getResourceGraph/sqs-lambda.json b/source/frontend/src/tests/mocks/fixtures/getResourceGraph/sqs-lambda.json new file mode 100644 index 00000000..64a811a6 --- /dev/null +++ b/source/frontend/src/tests/mocks/fixtures/getResourceGraph/sqs-lambda.json @@ -0,0 +1,180 @@ +{ + "getResourceGraph": { + "nodes": [ + { + "id": "arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:sqs", + "label": "AWS_Lambda_Function", + "md5Hash": "", + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:sqs", + "availabilityZone": "Not Applicable", + "awsRegion": "eu-west-1", + "configuration": "\"{\\\"architectures\\\":[\\\"x86_64\\\"],\\\"ephemeralStorage\\\":{\\\"size\\\":512},\\\"handler\\\":\\\"index.handler\\\",\\\"role\\\":\\\"arn:aws:iam::xxxxxxxxxxxx:role/service-role/sqslambda\\\",\\\"functionName\\\":\\\"sqs\\\",\\\"tracingConfig\\\":{\\\"mode\\\":\\\"PassThrough\\\"},\\\"runtime\\\":\\\"nodejs12.x\\\",\\\"description\\\":\\\"An Amazon SQS trigger that logs messages in a queue.\\\",\\\"codeSize\\\":383,\\\"version\\\":\\\"$LATEST\\\",\\\"packageType\\\":\\\"Zip\\\",\\\"timeout\\\":3,\\\"revisionId\\\":\\\"52a51ae1-ed13-4919-8a3d-c48b689966a8\\\",\\\"codeSha256\\\":\\\"xJUh5EPexgAe4RwmBK7VTazOpu57cdQE22Iso9jwSng=\\\",\\\"memorySize\\\":128,\\\"fileSystemConfigs\\\":[],\\\"lastUpdateStatus\\\":\\\"Successful\\\",\\\"loggingConfig\\\":{\\\"logFormat\\\":\\\"Text\\\",\\\"logGroup\\\":\\\"/aws/lambda/sqs\\\"},\\\"layers\\\":[],\\\"snapStart\\\":{\\\"applyOn\\\":\\\"None\\\",\\\"optimizationStatus\\\":\\\"Off\\\"},\\\"lastModified\\\":\\\"2020-04-10T12:36:27.981+0000\\\",\\\"state\\\":{\\\"value\\\":\\\"Active\\\"},\\\"functionArn\\\":\\\"arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:sqs\\\"}\"", + "configurationItemCaptureTime": "2024-01-09T06:54:06.285Z", + "configurationItemStatus": "OK", + "configurationStateId": null, + "resourceCreationTime": null, + "resourceId": "sqs", + "resourceName": "sqs", + "resourceType": "AWS::Lambda::Function", + "supplementaryConfiguration": null, + "tags": "[{\"tag\":\"lambda-console:blueprint=sqs-poller\",\"value\":\"sqs-poller\",\"key\":\"lambda-console:blueprint\"}]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": "https://eu-west-1.console.aws.amazon.com/lambda/home?region=eu-west-1#/functions/sqs?tab=graph", + "loginURL": "https://xxxxxxxxxxxx.signin.aws.amazon.com/console/lambda?region=eu-west-1#/functions/sqs?tab=graph", + "title": "sqs", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + { + "id": "arn:aws:iam::xxxxxxxxxxxx:role/service-role/sqslambda", + "label": "AWS_IAM_Role", + "md5Hash": "", + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:iam::xxxxxxxxxxxx:role/service-role/sqslambda", + "availabilityZone": "Not Applicable", + "awsRegion": "global", + "configuration": "\"{\\\"path\\\":\\\"/service-role/\\\",\\\"assumeRolePolicyDocument\\\":\\\"%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22lambda.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D\\\",\\\"instanceProfileList\\\":[],\\\"roleId\\\":\\\"AROAQU6I6AO4TGMKBRV2F\\\",\\\"attachedManagedPolicies\\\":[{\\\"policyArn\\\":\\\"arn:aws:iam::xxxxxxxxxxxx:policy/service-role/AWSLambdaSQSPollerExecutionRole-4dc6ea34-4c9d-4508-8f49-adf50e74c431\\\",\\\"policyName\\\":\\\"AWSLambdaSQSPollerExecutionRole-4dc6ea34-4c9d-4508-8f49-adf50e74c431\\\"},{\\\"policyArn\\\":\\\"arn:aws:iam::xxxxxxxxxxxx:policy/service-role/AWSLambdaBasicExecutionRole-2688086c-525a-478f-9584-cdbf63e8583c\\\",\\\"policyName\\\":\\\"AWSLambdaBasicExecutionRole-2688086c-525a-478f-9584-cdbf63e8583c\\\"}],\\\"roleName\\\":\\\"sqslambda\\\",\\\"arn\\\":\\\"arn:aws:iam::xxxxxxxxxxxx:role/service-role/sqslambda\\\",\\\"createDate\\\":\\\"2020-04-10T12:36:15.000Z\\\",\\\"rolePolicyList\\\":[],\\\"tags\\\":[]}\"", + "configurationItemCaptureTime": "2022-02-14T13:50:59.294Z", + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": null, + "resourceCreationTime": "2020-04-10T12:36:15.000Z", + "resourceId": "AROAQU6I6AO4TGMKBRV2F", + "resourceName": "sqslambda", + "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": "sqslambda", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + { + "id": "arn:aws:sqs:eu-west-1:xxxxxxxxxxxx:test-queue", + "label": "AWS_SQS_Queue", + "md5Hash": "", + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:sqs:eu-west-1:xxxxxxxxxxxx:test-queue", + "availabilityZone": "Not Applicable", + "awsRegion": "eu-west-1", + "configuration": "\"{\\\"ReceiveMessageWaitTimeSeconds\\\":0,\\\"SqsManagedSseEnabled\\\":\\\"false\\\",\\\"CreatedTimestamp\\\":\\\"1586428013\\\",\\\"DelaySeconds\\\":0,\\\"MessageRetentionPeriod\\\":345600,\\\"MaximumMessageSize\\\":262144,\\\"VisibilityTimeout\\\":30,\\\"LastModifiedTimestamp\\\":\\\"1586428013\\\",\\\"QueueArn\\\":\\\"arn:aws:sqs:eu-west-1:xxxxxxxxxxxx:test-queue\\\"}\"", + "configurationItemCaptureTime": "2021-11-18T15:12:30.432Z", + "configurationItemStatus": "OK", + "configurationStateId": null, + "resourceCreationTime": "2020-04-09T10:26:53.000Z", + "resourceId": "https://sqs.eu-west-1.amazonaws.com/xxxxxxxxxxxx/test-queue", + "resourceName": "test-queue", + "resourceType": "AWS::SQS::Queue", + "supplementaryConfiguration": null, + "tags": "[]", + "version": "1.3", + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "test-queue", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + }, + { + "id": "arn:aws:tags::xxxxxxxxxxxx:tag/lambda-console:blueprint=sqs-poller", + "label": "AWS_Tags_Tag", + "md5Hash": "", + "properties": { + "accountId": "xxxxxxxxxxxx", + "arn": "arn:aws:tags::xxxxxxxxxxxx:tag/lambda-console:blueprint=sqs-poller", + "availabilityZone": "Not Applicable", + "awsRegion": "global", + "configuration": "\"{}\"", + "configurationItemCaptureTime": null, + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": null, + "resourceCreationTime": null, + "resourceId": "arn:aws:tags::xxxxxxxxxxxx:tag/lambda-console:blueprint=sqs-poller", + "resourceName": "lambda-console:blueprint=sqs-poller", + "resourceType": "AWS::Tags::Tag", + "supplementaryConfiguration": null, + "tags": "[]", + "version": null, + "vpcId": null, + "subnetId": null, + "subnetIds": null, + "resourceValue": null, + "state": null, + "private": null, + "loggedInURL": null, + "loginURL": null, + "title": "lambda-console:blueprint=sqs-poller", + "dBInstanceStatus": null, + "statement": null, + "instanceType": null + } + } + ], + "edges": [ + { + "id": "04c61a6d-f60b-4877-76cc-28744ae06d7e", + "label": "IS_ASSOCIATED_WITH_ROLE", + "source": { + "id": "arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:sqs", + "label": "AWS_Lambda_Function" + }, + "target": { + "id": "arn:aws:iam::xxxxxxxxxxxx:role/service-role/sqslambda", + "label": "AWS_IAM_Role" + } + }, + { + "id": "cec61a6d-f60c-1aca-e359-6d2a3cbadec9", + "label": "IS_ASSOCIATED_WITH", + "source": { + "id": "arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:sqs", + "label": "AWS_Lambda_Function" + }, + "target": { + "id": "arn:aws:sqs:eu-west-1:xxxxxxxxxxxx:test-queue", + "label": "AWS_SQS_Queue" + } + }, + { + "id": "c8c61a6d-ffe7-9524-a55c-a9473154e444", + "label": "IS_ASSOCIATED_WITH", + "source": { + "id": "arn:aws:tags::xxxxxxxxxxxx:tag/lambda-console:blueprint=sqs-poller", + "label": "AWS_Tags_Tag" + }, + "target": { + "id": "arn:aws:lambda:eu-west-1:xxxxxxxxxxxx:function:sqs", + "label": "AWS_Lambda_Function" + } + } + ] + } +} diff --git a/source/frontend/src/tests/vitest/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.test.js b/source/frontend/src/tests/vitest/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.test.js new file mode 100644 index 00000000..84af4b72 --- /dev/null +++ b/source/frontend/src/tests/vitest/components/Diagrams/Draw/Canvas/Export/ExportDiagramModal.test.js @@ -0,0 +1,668 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import {describe, expect, it, vi} from 'vitest'; +import {render, screen, waitFor, within} from '@testing-library/react'; +import ExportDiagramModal from '../../../../../../../components/Diagrams/Draw/Canvas/Export/ExportDiagramModal'; +import {QueryClient, QueryClientProvider} from 'react-query'; +import {createMemoryHistory} from 'history'; +import {Route, Router} from 'react-router-dom'; +import userEvent from '@testing-library/user-event'; +import {server} from '../../../../../../mocks/server'; +import {graphql, HttpResponse} from 'msw'; +import {NotificationProvider} from '../../../../../../../components/Contexts/NotificationContext'; + +const EU_WEST_1 = 'eu-west-1'; +const EU_WEST_2 = 'eu-west-2'; +const US_EAST_1 = 'us-east-1'; + +const ACCOUNT_ID_1 = '111111111111'; +const ACCOUNT_ID_2 = '222222222222'; +const ACCOUNT_ID_3 = '333333333333'; + +describe('Export', () => { + 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); + }); + }); +});