From 1c674113e06cbaf478dcecba7e598dbcaaf10180 Mon Sep 17 00:00:00 2001 From: jrgaray Date: Wed, 12 Nov 2025 11:48:35 -0500 Subject: [PATCH] release v3.0.0 --- .gitignore | 23 +- CHANGELOG.md | 44 + NOTICE.txt | 2064 +-- README.md | 76 +- deployment/build-open-source-dist.sh | 8 +- deployment/build-s3-dist.sh | 93 +- deployment/manifest-generator/app.js | 53 + .../manifest-generator/package-lock.json | 27 + deployment/manifest-generator/package.json | 13 + deployment/poetry.lock | 2106 ++-- deployment/pyproject.toml | 39 +- deployment/run-unit-tests.sh | 133 +- deployment/utils/generate-controls-list.js | 51 + .../utils/generate-controls-list.test.js | 96 + deployment/utils/package-lock.json | 3641 ++++++ deployment/utils/package.json | 17 + docs/architecture_diagram.png | Bin 275168 -> 0 bytes ...y-response-on-aws-architecture-diagram.png | Bin 0 -> 500072 bytes solution-manifest.yaml | 3 +- sonar-project.properties | 46 +- source/.eslintrc.js | 64 +- source/.prettierignore | 2 +- source/Orchestrator/check_ssm_doc_state.py | 4 + source/Orchestrator/check_ssm_execution.py | 2 +- source/Orchestrator/schedule_remediation.py | 5 +- source/Orchestrator/send_notifications.py | 1107 +- .../test/test_check_ssm_doc_state.py | 84 + .../test/test_check_ssm_execution.py | 34 +- .../test/test_schedule_remediation.py | 169 + .../test/test_send_notifications.py | 1111 +- ...test_stepfunctions_event_transformation.py | 135 + source/blueprints/cdk/blueprint-stack.ts | 2 +- .../jira/cdk/jira-blueprint-stack.ts | 4 + .../jira-blueprint-stack.test.ts.snap | 9 +- .../cdk/test/jira-blueprint-stack.test.ts | 2 +- .../ticket_generator/jira_ticket_generator.py | 19 +- source/blueprints/poetry.lock | 20 +- source/blueprints/pyproject.toml | 4 +- .../cdk/servicenow-blueprint-stack.ts | 4 + .../servicenow-blueprint-stack.test.ts.snap | 9 +- .../test/servicenow-blueprint-stack.test.ts | 2 +- .../servicenow_ticket_generator.py | 19 +- source/data-models/apiActions.ts | 14 + source/data-models/finding.ts | 51 + source/data-models/index.ts | 9 + source/data-models/package-lock.json | 41 + source/data-models/package.json | 26 + source/data-models/remediation.ts | 27 + source/data-models/schemaTypes.ts | 491 + source/data-models/searchCriteria.ts | 30 + source/data-models/tsconfig.cjs.json | 9 + source/data-models/tsconfig.esm.json | 11 + source/data-models/user.ts | 72 + source/lambdas/api/README.md | 16 + .../lambdas/api/__tests__/clients/s3.test.ts | 252 + .../api/__tests__/handlers/apiHandler.test.ts | 1235 ++ .../__tests__/handlers/baseHandler.test.ts | 126 + .../__tests__/handlers/deployWebui.test.ts | 384 + .../api/__tests__/handlers/findings.test.ts | 1861 +++ .../api/__tests__/handlers/preSignUp.test.ts | 332 + .../__tests__/handlers/remediations.test.ts | 346 + .../api/__tests__/handlers/users.test.ts | 1544 +++ .../__tests__/services/authorization.test.ts | 182 + .../api/__tests__/services/cognito.test.ts | 1149 ++ .../services/findingsService.test.ts | 169 + .../services/remediationService.test.ts | 568 + source/lambdas/api/__tests__/utils.ts | 134 + source/lambdas/api/clients/ASRS3Client.ts | 140 + source/lambdas/api/handlers/apiHandler.ts | 190 + source/lambdas/api/handlers/baseHandler.ts | 146 + source/lambdas/api/handlers/cfnResponse.ts | 66 + source/lambdas/api/handlers/deployWebui.ts | 105 + source/lambdas/api/handlers/findings.ts | 115 + source/lambdas/api/handlers/preSignUp.ts | 105 + source/lambdas/api/handlers/remediations.ts | 99 + source/lambdas/api/handlers/users.ts | 231 + source/lambdas/api/services/authorization.ts | 79 + .../lambdas/api/services/baseSearchService.ts | 223 + source/lambdas/api/services/cognito.ts | 363 + .../lambdas/api/services/findingsService.ts | 257 + .../api/services/remediationService.ts | 269 + .../lambdas/common/__tests__/dynamodbSetup.ts | 261 + source/lambdas/common/__tests__/envSetup.ts | 35 + .../common/__tests__/filterPattern.test.ts | 212 + .../common/__tests__/filterUtils.test.ts | 795 ++ .../__tests__/findingRepository.test.ts | 1084 ++ .../common/__tests__/jestAfterEnvSetup.ts | 14 + .../common/__tests__/metricsMockSetup.ts | 33 + .../common/__tests__/metricsUtils.test.ts | 313 + .../userAccountMappingRepository.test.ts | 449 + .../__tests__/securityStandardFilters.test.ts | 201 + .../lambdas/common/constants/apiConstant.ts | 6 + .../constants/securityStandardFilters.ts | 186 + .../remediationHistoryRepository.test.ts | 145 + .../common/repositories/abstractRepository.ts | 524 + .../common/repositories/findingRepository.ts | 687 + .../remediationHistoryRepository.ts | 478 + .../userAccountMappingRepository.ts | 83 + .../common/services/findingDataService.ts | 206 + .../utils/__tests__/findingUtils.test.ts | 117 + .../__tests__/remediationStatusMapper.test.ts | 52 + .../utils/__tests__/securityHub.test.ts | 167 + .../common/utils/__tests__/ttlUtils.test.ts | 98 + source/lambdas/common/utils/clock.ts | 14 + source/lambdas/common/utils/constants.ts | 54 + source/lambdas/common/utils/dynamodb.ts | 31 + source/lambdas/common/utils/errorUtils.ts | 32 + source/lambdas/common/utils/filterUtils.ts | 378 + source/lambdas/common/utils/findingUtils.ts | 96 + source/lambdas/common/utils/httpErrors.ts | 45 + source/lambdas/common/utils/logger.ts | 7 + source/lambdas/common/utils/metricsUtils.ts | 193 + source/lambdas/common/utils/orchestrator.ts | 49 + .../common/utils/remediationStatusMapper.ts | 40 + source/lambdas/common/utils/securityHub.ts | 65 + source/lambdas/common/utils/ssmCache.ts | 72 + source/lambdas/common/utils/tracer.ts | 7 + source/lambdas/common/utils/ttlUtils.ts | 44 + source/lambdas/jest.config.js | 81 + source/lambdas/package-lock.json | 10441 ++++++++++++++++ source/lambdas/package.json | 74 + .../Normalizer/findingEventNormalizer.ts | 309 + .../pre-processor/RemediationConfigChecker.ts | 65 + .../__tests__/findingEventNormalizer.test.ts | 325 + .../pre-processor/__tests__/preProcessor.ts | 2180 ++++ .../remediationConfigChecker.test.ts | 112 + source/lambdas/pre-processor/preProcessor.ts | 454 + .../__tests__/customResourceHandler.test.ts | 241 + .../__tests__/synchronizationHandler.test.ts | 1491 +++ .../synchronization/customResourceHandler.ts | 172 + .../synchronization/synchronizationHandler.ts | 389 + source/lambdas/tsconfig.json | 15 + source/layer/metrics.py | 195 +- source/layer/sechub_findings.py | 46 +- source/layer/test/test_cloudwatch_metrics.py | 10 +- ...rvation.py => test_document_validation.py} | 7 +- source/layer/test/test_metrics.py | 611 +- source/layer/test/test_sechub_findings.py | 131 + source/layer/test/test_utils.py | 2 +- source/layer/utils.py | 21 +- .../__snapshots__/member-stack.test.ts.snap | 18 +- source/lib/action-log.ts | 9 +- source/lib/admin-playbook.ts | 9 +- source/lib/administrator-stack.ts | 632 +- source/lib/cdk-helper/eventeattern-helper.ts | 103 - source/lib/cdk-helper/metric-resources.ts | 15 +- source/lib/cloudwatch_metrics.ts | 139 +- source/lib/common-orchestrator-construct.ts | 132 +- source/lib/constants/parameters.ts | 92 + source/lib/member-stack.test.ts | 11 +- source/lib/member-stack.ts | 2 +- .../event-processor.js | 1 + .../package-lock.json | 622 +- .../cloud-trail-event-processor/package.json | 11 +- source/lib/member/cloud-trail.ts | 53 +- source/lib/member/log-group.ts | 3 +- source/lib/orchestrator_roles-construct.ts | 2 +- source/lib/parameters/account-target-param.ts | 49 - source/lib/playbook-construct.ts | 56 +- source/lib/pre-processor-construct.ts | 152 + source/lib/remediation-runbook-stack.ts | 187 +- source/lib/ssmplaybook.ts | 144 +- .../lib/synchronization-findings-construct.ts | 307 + source/lib/tags/applyTag.ts | 18 - source/lib/webui-nested-stack.ts | 130 + source/lib/webui/api-construct.ts | 307 + source/lib/webui/cognito-construct.ts | 292 + source/lib/webui/webUIDeploymentConstruct.ts | 107 + source/lib/webui/webUIHostingConstruct.ts | 98 + source/package-lock.json | 8183 +++++++----- source/package.json | 56 +- .../__snapshots__/afsbp_stack.test.ts.snap | 634 +- .../test/__snapshots__/cis_stack.test.ts.snap | 667 +- .../test/__snapshots__/cis_stack.test.ts.snap | 634 +- .../CIS300/lib/cis300_remediations.ts | 2 +- .../CIS300/lib/control_runbooks-construct.ts | 4 +- .../{CIS300_2.1.4.ts => CIS300_2.1.4.1.ts} | 2 +- .../__snapshots__/cis300_stack.test.ts.snap | 660 +- .../CIS300/test/cis300_stack.test.ts | 29 +- .../newplaybook_stack.test.ts.snap | 630 +- .../__snapshots__/nist_stack.test.ts.snap | 634 +- .../__snapshots__/pci321_stack.test.ts.snap | 634 +- .../security_controls_playbook-construct.ts | 35 +- .../playbooks/SC/ssmdocs/SC_CloudTrail.2.ts | 5 +- .../playbooks/SC/ssmdocs/SC_CloudTrail.5.ts | 8 +- .../playbooks/SC/ssmdocs/SC_CloudTrail.6.ts | 2 +- .../playbooks/SC/ssmdocs/SC_CloudTrail.7.ts | 2 +- source/playbooks/SC/ssmdocs/SC_S3.4.ts | 2 +- source/playbooks/SC/ssmdocs/SC_SNS.1.ts | 2 +- source/playbooks/SC/ssmdocs/SC_SQS.1.ts | 2 +- .../security_controls_stack.test.ts.snap | 646 +- source/playbooks/common/parse_input.py | 2 +- .../CreateLogMetricFilterAndAlarm.yaml | 26 +- .../EnableCloudTrailToCloudWatchLogging.yaml | 87 +- .../scripts/AttachServiceVPCEndpoint.py | 19 +- ...AccessLoggingBucket_createloggingbucket.py | 9 +- ...dTrailMultiRegionTrail_enablecloudtrail.py | 65 +- .../scripts/CreateLogMetricFilterAndAlarm.py | 220 +- ...railToCloudWatchLogging_fixbucketpolicy.py | 136 + ...oudTrailToCloudWatchLogging_updatetrail.py | 77 + ...TrailToCloudWatchLogging_validatepolicy.py | 147 + ...railToCloudWatchLogging_waitforloggroup.py | 92 +- .../scripts/SetS3LifecyclePolicy.py | 5 +- .../test/test_AttachServiceVPCEndpoint.py | 24 +- ...TrailToCloudWatchLogging_validatepolicy.py | 190 + .../test_createcloudtrailmultiregiontrail.py | 236 +- .../test_createlogmetricfilterandalarm.py | 170 +- ...est_enablecloudtrailtocloudwatchlogging.py | 97 +- source/solution_deploy/bin/solution_deploy.ts | 9 +- .../deployment_metrics_custom_resource.py | 27 +- .../source/remediation_config_provider.py | 190 + .../test/test_deployment_custom_resource.py | 83 +- .../test/test_remediation_config_provider.py | 295 + .../member_runbook_stack.test.ts.snap | 894 +- .../__snapshots__/orchestrator.test.ts.snap | 56 +- .../solution_deploy.test.ts.snap | 6261 ++++++--- source/test/accountTargetParam.test.ts | 111 - .../administrator-targetAccountIds.test.ts | 45 - source/test/applyTag.test.ts | 116 - source/test/eventPatternHelper.test.ts | 85 - source/test/member_runbook_stack.test.ts | 10 +- source/test/orchestrator.test.ts | 275 +- source/test/solution_deploy.test.ts | 115 +- source/test/test_data/tstest-runbook.yaml | 22 +- source/test/webui-nested-stack.test.ts | 128 + source/tsconfig.json | 8 +- source/webui/index.html | 19 + source/webui/package-lock.json | 9998 +++++++++++++++ source/webui/package.json | 51 + source/webui/public/aws-exports.template.json | 27 + source/webui/public/aws-logo.svg | 7 + source/webui/public/cognito-login-banner.png | Bin 0 -> 6639 bytes .../cognito-managed-login-branding.json | 473 + source/webui/public/favicon.ico | Bin 0 -> 1150 bytes source/webui/public/logo.png | Bin 0 -> 1150 bytes source/webui/public/manifest.json | 15 + source/webui/public/mockServiceWorker.js | 344 + source/webui/src/App.tsx | 33 + source/webui/src/AppRoutes.tsx | 49 + source/webui/src/Layout.tsx | 44 + source/webui/src/__tests__/App.test.tsx | 350 + .../navigation/create-breadcrumbs.test.ts | 38 + .../__tests__/contexts/UserContext.test.tsx | 422 + .../src/__tests__/pages/CallbackPage.test.tsx | 138 + .../src/__tests__/pages/FindingsPage.test.tsx | 711 ++ .../__tests__/pages/InviteUsersPage.test.tsx | 616 + .../pages/RemediationHistoryPage.test.tsx | 451 + .../src/__tests__/pages/UsersPage.test.tsx | 273 + .../src/__tests__/pages/UsersTable.test.tsx | 1091 ++ source/webui/src/__tests__/server.ts | 9 + .../webui/src/__tests__/test-data-factory.ts | 119 + .../src/__tests__/test-data-random-utils.ts | 59 + source/webui/src/__tests__/test-utils.tsx | 53 + .../webui/src/components/ActionsDropdown.tsx | 83 + .../webui/src/components/EmptyTableState.tsx | 27 + .../webui/src/components/ProtectedRoute.tsx | 22 + .../src/components/navigation/Breadcrumbs.tsx | 24 + .../navigation/SideNavigationBar.tsx | 72 + .../navigation/TopNavigationBar.tsx | 48 + .../navigation/create-breadcrumbs.ts | 40 + source/webui/src/contexts/ConfigContext.tsx | 31 + .../src/contexts/NotificationContext.tsx | 50 + source/webui/src/contexts/UserContext.tsx | 102 + source/webui/src/main.tsx | 100 + source/webui/src/mocks/browser.ts | 34 + source/webui/src/mocks/handlers.ts | 302 + .../webui/src/pages/callback/CallbackPage.tsx | 101 + .../pages/findings/FindingsOverviewPage.tsx | 7 + .../findings/findings-table/FindingsTable.tsx | 937 ++ .../createColumnDefinitions.tsx | 173 + .../RemediationHistoryOverviewPage.tsx | 7 + .../history-table/RemediationHistoryTable.tsx | 652 + .../createHistoryColumnDefinitions.tsx | 115 + .../src/pages/users/UsersOverviewPage.tsx | 42 + .../pages/users/invite/InviteUsersPage.tsx | 177 + .../pages/users/users-table/UsersTable.tsx | 429 + .../users-table/createColumnDefinitions.tsx | 51 + source/webui/src/setupTests.ts | 79 + source/webui/src/store/findingsApiSlice.ts | 75 + source/webui/src/store/notificationsSlice.ts | 40 + source/webui/src/store/remediationsSlice.ts | 69 + source/webui/src/store/solutionApi.ts | 64 + source/webui/src/store/store.ts | 29 + source/webui/src/store/types.ts | 55 + source/webui/src/store/usersApiSlice.ts | 48 + source/webui/src/styles.css | 27 + source/webui/src/utils/API.adapter.ts | 99 + source/webui/src/utils/error.ts | 26 + source/webui/src/utils/userPermissions.ts | 11 + source/webui/src/utils/validation.ts | 30 + source/webui/tsconfig.json | 41 + source/webui/tsconfig.node.json | 10 + source/webui/vite.config.ts | 52 + .../lambda/reset_remediation_resources.py | 6 +- tox.ini | 2 + 295 files changed, 80599 insertions(+), 14142 deletions(-) create mode 100644 deployment/manifest-generator/app.js create mode 100644 deployment/manifest-generator/package-lock.json create mode 100644 deployment/manifest-generator/package.json create mode 100755 deployment/utils/generate-controls-list.js create mode 100644 deployment/utils/generate-controls-list.test.js create mode 100644 deployment/utils/package-lock.json create mode 100644 deployment/utils/package.json delete mode 100644 docs/architecture_diagram.png create mode 100644 docs/automated-security-response-on-aws-architecture-diagram.png create mode 100644 source/Orchestrator/test/test_stepfunctions_event_transformation.py create mode 100644 source/data-models/apiActions.ts create mode 100644 source/data-models/finding.ts create mode 100644 source/data-models/index.ts create mode 100644 source/data-models/package-lock.json create mode 100644 source/data-models/package.json create mode 100644 source/data-models/remediation.ts create mode 100644 source/data-models/schemaTypes.ts create mode 100644 source/data-models/searchCriteria.ts create mode 100644 source/data-models/tsconfig.cjs.json create mode 100644 source/data-models/tsconfig.esm.json create mode 100644 source/data-models/user.ts create mode 100644 source/lambdas/api/README.md create mode 100644 source/lambdas/api/__tests__/clients/s3.test.ts create mode 100644 source/lambdas/api/__tests__/handlers/apiHandler.test.ts create mode 100644 source/lambdas/api/__tests__/handlers/baseHandler.test.ts create mode 100644 source/lambdas/api/__tests__/handlers/deployWebui.test.ts create mode 100644 source/lambdas/api/__tests__/handlers/findings.test.ts create mode 100644 source/lambdas/api/__tests__/handlers/preSignUp.test.ts create mode 100644 source/lambdas/api/__tests__/handlers/remediations.test.ts create mode 100644 source/lambdas/api/__tests__/handlers/users.test.ts create mode 100644 source/lambdas/api/__tests__/services/authorization.test.ts create mode 100644 source/lambdas/api/__tests__/services/cognito.test.ts create mode 100644 source/lambdas/api/__tests__/services/findingsService.test.ts create mode 100644 source/lambdas/api/__tests__/services/remediationService.test.ts create mode 100644 source/lambdas/api/__tests__/utils.ts create mode 100644 source/lambdas/api/clients/ASRS3Client.ts create mode 100644 source/lambdas/api/handlers/apiHandler.ts create mode 100644 source/lambdas/api/handlers/baseHandler.ts create mode 100644 source/lambdas/api/handlers/cfnResponse.ts create mode 100644 source/lambdas/api/handlers/deployWebui.ts create mode 100644 source/lambdas/api/handlers/findings.ts create mode 100644 source/lambdas/api/handlers/preSignUp.ts create mode 100644 source/lambdas/api/handlers/remediations.ts create mode 100644 source/lambdas/api/handlers/users.ts create mode 100644 source/lambdas/api/services/authorization.ts create mode 100644 source/lambdas/api/services/baseSearchService.ts create mode 100644 source/lambdas/api/services/cognito.ts create mode 100644 source/lambdas/api/services/findingsService.ts create mode 100644 source/lambdas/api/services/remediationService.ts create mode 100644 source/lambdas/common/__tests__/dynamodbSetup.ts create mode 100644 source/lambdas/common/__tests__/envSetup.ts create mode 100644 source/lambdas/common/__tests__/filterPattern.test.ts create mode 100644 source/lambdas/common/__tests__/filterUtils.test.ts create mode 100644 source/lambdas/common/__tests__/findingRepository.test.ts create mode 100644 source/lambdas/common/__tests__/jestAfterEnvSetup.ts create mode 100644 source/lambdas/common/__tests__/metricsMockSetup.ts create mode 100644 source/lambdas/common/__tests__/metricsUtils.test.ts create mode 100644 source/lambdas/common/__tests__/userAccountMappingRepository.test.ts create mode 100644 source/lambdas/common/constants/__tests__/securityStandardFilters.test.ts create mode 100644 source/lambdas/common/constants/apiConstant.ts create mode 100644 source/lambdas/common/constants/securityStandardFilters.ts create mode 100644 source/lambdas/common/repositories/__tests__/remediationHistoryRepository.test.ts create mode 100644 source/lambdas/common/repositories/abstractRepository.ts create mode 100644 source/lambdas/common/repositories/findingRepository.ts create mode 100644 source/lambdas/common/repositories/remediationHistoryRepository.ts create mode 100644 source/lambdas/common/repositories/userAccountMappingRepository.ts create mode 100644 source/lambdas/common/services/findingDataService.ts create mode 100644 source/lambdas/common/utils/__tests__/findingUtils.test.ts create mode 100644 source/lambdas/common/utils/__tests__/remediationStatusMapper.test.ts create mode 100644 source/lambdas/common/utils/__tests__/securityHub.test.ts create mode 100644 source/lambdas/common/utils/__tests__/ttlUtils.test.ts create mode 100644 source/lambdas/common/utils/clock.ts create mode 100644 source/lambdas/common/utils/constants.ts create mode 100644 source/lambdas/common/utils/dynamodb.ts create mode 100644 source/lambdas/common/utils/errorUtils.ts create mode 100644 source/lambdas/common/utils/filterUtils.ts create mode 100644 source/lambdas/common/utils/findingUtils.ts create mode 100644 source/lambdas/common/utils/httpErrors.ts create mode 100644 source/lambdas/common/utils/logger.ts create mode 100644 source/lambdas/common/utils/metricsUtils.ts create mode 100644 source/lambdas/common/utils/orchestrator.ts create mode 100644 source/lambdas/common/utils/remediationStatusMapper.ts create mode 100644 source/lambdas/common/utils/securityHub.ts create mode 100644 source/lambdas/common/utils/ssmCache.ts create mode 100644 source/lambdas/common/utils/tracer.ts create mode 100644 source/lambdas/common/utils/ttlUtils.ts create mode 100644 source/lambdas/jest.config.js create mode 100644 source/lambdas/package-lock.json create mode 100644 source/lambdas/package.json create mode 100644 source/lambdas/pre-processor/Normalizer/findingEventNormalizer.ts create mode 100644 source/lambdas/pre-processor/RemediationConfigChecker.ts create mode 100644 source/lambdas/pre-processor/__tests__/findingEventNormalizer.test.ts create mode 100644 source/lambdas/pre-processor/__tests__/preProcessor.ts create mode 100644 source/lambdas/pre-processor/__tests__/remediationConfigChecker.test.ts create mode 100644 source/lambdas/pre-processor/preProcessor.ts create mode 100644 source/lambdas/synchronization/__tests__/customResourceHandler.test.ts create mode 100644 source/lambdas/synchronization/__tests__/synchronizationHandler.test.ts create mode 100644 source/lambdas/synchronization/customResourceHandler.ts create mode 100644 source/lambdas/synchronization/synchronizationHandler.ts create mode 100644 source/lambdas/tsconfig.json rename source/layer/test/{test_orchestrator_logic_preservation.py => test_document_validation.py} (97%) delete mode 100644 source/lib/cdk-helper/eventeattern-helper.ts create mode 100644 source/lib/constants/parameters.ts delete mode 100644 source/lib/parameters/account-target-param.ts create mode 100644 source/lib/pre-processor-construct.ts create mode 100644 source/lib/synchronization-findings-construct.ts delete mode 100644 source/lib/tags/applyTag.ts create mode 100644 source/lib/webui-nested-stack.ts create mode 100644 source/lib/webui/api-construct.ts create mode 100644 source/lib/webui/cognito-construct.ts create mode 100644 source/lib/webui/webUIDeploymentConstruct.ts create mode 100644 source/lib/webui/webUIHostingConstruct.ts rename source/playbooks/CIS300/ssmdocs/{CIS300_2.1.4.ts => CIS300_2.1.4.1.ts} (88%) create mode 100644 source/remediation_runbooks/scripts/EnableCloudTrailToCloudWatchLogging_fixbucketpolicy.py create mode 100644 source/remediation_runbooks/scripts/EnableCloudTrailToCloudWatchLogging_updatetrail.py create mode 100644 source/remediation_runbooks/scripts/EnableCloudTrailToCloudWatchLogging_validatepolicy.py create mode 100644 source/remediation_runbooks/scripts/test/test_EnableCloudTrailToCloudWatchLogging_validatepolicy.py create mode 100644 source/solution_deploy/source/remediation_config_provider.py create mode 100644 source/solution_deploy/source/test/test_remediation_config_provider.py delete mode 100644 source/test/accountTargetParam.test.ts delete mode 100644 source/test/administrator-targetAccountIds.test.ts delete mode 100644 source/test/applyTag.test.ts delete mode 100644 source/test/eventPatternHelper.test.ts create mode 100644 source/test/webui-nested-stack.test.ts create mode 100644 source/webui/index.html create mode 100644 source/webui/package-lock.json create mode 100644 source/webui/package.json create mode 100644 source/webui/public/aws-exports.template.json create mode 100644 source/webui/public/aws-logo.svg create mode 100644 source/webui/public/cognito-login-banner.png create mode 100644 source/webui/public/cognito-managed-login-branding.json create mode 100644 source/webui/public/favicon.ico create mode 100644 source/webui/public/logo.png create mode 100644 source/webui/public/manifest.json create mode 100644 source/webui/public/mockServiceWorker.js create mode 100644 source/webui/src/App.tsx create mode 100644 source/webui/src/AppRoutes.tsx create mode 100644 source/webui/src/Layout.tsx create mode 100644 source/webui/src/__tests__/App.test.tsx create mode 100644 source/webui/src/__tests__/components/navigation/create-breadcrumbs.test.ts create mode 100644 source/webui/src/__tests__/contexts/UserContext.test.tsx create mode 100644 source/webui/src/__tests__/pages/CallbackPage.test.tsx create mode 100644 source/webui/src/__tests__/pages/FindingsPage.test.tsx create mode 100644 source/webui/src/__tests__/pages/InviteUsersPage.test.tsx create mode 100644 source/webui/src/__tests__/pages/RemediationHistoryPage.test.tsx create mode 100644 source/webui/src/__tests__/pages/UsersPage.test.tsx create mode 100644 source/webui/src/__tests__/pages/UsersTable.test.tsx create mode 100644 source/webui/src/__tests__/server.ts create mode 100644 source/webui/src/__tests__/test-data-factory.ts create mode 100644 source/webui/src/__tests__/test-data-random-utils.ts create mode 100644 source/webui/src/__tests__/test-utils.tsx create mode 100644 source/webui/src/components/ActionsDropdown.tsx create mode 100644 source/webui/src/components/EmptyTableState.tsx create mode 100644 source/webui/src/components/ProtectedRoute.tsx create mode 100644 source/webui/src/components/navigation/Breadcrumbs.tsx create mode 100644 source/webui/src/components/navigation/SideNavigationBar.tsx create mode 100644 source/webui/src/components/navigation/TopNavigationBar.tsx create mode 100644 source/webui/src/components/navigation/create-breadcrumbs.ts create mode 100644 source/webui/src/contexts/ConfigContext.tsx create mode 100644 source/webui/src/contexts/NotificationContext.tsx create mode 100644 source/webui/src/contexts/UserContext.tsx create mode 100644 source/webui/src/main.tsx create mode 100644 source/webui/src/mocks/browser.ts create mode 100644 source/webui/src/mocks/handlers.ts create mode 100644 source/webui/src/pages/callback/CallbackPage.tsx create mode 100644 source/webui/src/pages/findings/FindingsOverviewPage.tsx create mode 100644 source/webui/src/pages/findings/findings-table/FindingsTable.tsx create mode 100644 source/webui/src/pages/findings/findings-table/createColumnDefinitions.tsx create mode 100644 source/webui/src/pages/history/RemediationHistoryOverviewPage.tsx create mode 100644 source/webui/src/pages/history/history-table/RemediationHistoryTable.tsx create mode 100644 source/webui/src/pages/history/history-table/createHistoryColumnDefinitions.tsx create mode 100644 source/webui/src/pages/users/UsersOverviewPage.tsx create mode 100644 source/webui/src/pages/users/invite/InviteUsersPage.tsx create mode 100644 source/webui/src/pages/users/users-table/UsersTable.tsx create mode 100644 source/webui/src/pages/users/users-table/createColumnDefinitions.tsx create mode 100644 source/webui/src/setupTests.ts create mode 100644 source/webui/src/store/findingsApiSlice.ts create mode 100644 source/webui/src/store/notificationsSlice.ts create mode 100644 source/webui/src/store/remediationsSlice.ts create mode 100644 source/webui/src/store/solutionApi.ts create mode 100644 source/webui/src/store/store.ts create mode 100644 source/webui/src/store/types.ts create mode 100644 source/webui/src/store/usersApiSlice.ts create mode 100644 source/webui/src/styles.css create mode 100644 source/webui/src/utils/API.adapter.ts create mode 100644 source/webui/src/utils/error.ts create mode 100644 source/webui/src/utils/userPermissions.ts create mode 100644 source/webui/src/utils/validation.ts create mode 100644 source/webui/tsconfig.json create mode 100644 source/webui/tsconfig.node.json create mode 100644 source/webui/vite.config.ts diff --git a/.gitignore b/.gitignore index 53d32c32..c63a54d8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,18 +7,24 @@ /build # test /deployment/test/coverage-reports/ +/deployment/utils/coverage requirements_dev.txt # Typescript /source/dist/ -*.d.ts +/source/webui/dist/ +/source/data-models/cjs/ +/source/data-models/esm/ +*.d.ts* *.js - -# CloudTrail event processor is JavaScript +*.cjs +!source/webui/public/mockServiceWorker.js +!deployment/manifest-generator/app.js +!/**/jest.config.js +!source/.eslintrc.js !**/cloud-trail-event-processor/*.js - -# config -!.eslintrc.js +!deployment/utils/generate-controls-list.js +!deployment/utils/generate-controls-list.test.js # Node node_modules/ @@ -44,4 +50,7 @@ requirements.txt .idea/ # system -.DS_Store \ No newline at end of file +.DS_Store +/.temp_redpencil/ +/bom.json +aws-exports.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f894d8..b2fc3591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2025-11-13 + +### Added + +- Optional Web User Interface to run remediations, view past remediations, and delegate access to the solution + - When the `ShouldDeployWebUI` parameter is *"yes"*, you must enter a value for `AdminUserEmail` which will be granted administrator access to the Web UI. You will receive temporary credential and a login link via email. + - Deploying the Web UI provisions additional resources such as a CloudFront distribution, Cognito User Pool, S3 bucket for hosting, and more. +- Support for Security Control findings in Security Hub v2 + - The solution continues to support Security Hub CSPM in addition to Security Hub v2 +- API Gateway REST API to support the new Web User Interface +- Automated remediation filtering capabilities based on Account ID, Organizational Unit ID, and resource tags + - Controlled via SSM parameters under `ASR/Filters/` +- Pre-Processor Lambda function to centralize processing of Security Hub finding events +- DynamoDB tables to store Security Hub finding data, remediation history data, and automated remediation settings +- Complete list of supported control IDs in `solutions-reference/automated-security-response-on-aws/latest/supported-controls.json` +- EventBridge rule to run a weekly refresh of the Findings DynamoDB table +- EventBridge rule to capture and handle Step Function failures in the Orchestrator + +### Changed + +- Security Hub events are now consumed by a single EventBridge rule and forwarded to the Pre-processor +- Enabling / Disabling automated remediations is now controlled by the Remediation Configuration DynamoDB table, which can be modified post-deployment. See the [Implementation Guide](https://docs.aws.amazon.com/solutions/latest/automated-security-response-on-aws/getting-stated-with-asr.html) for details. + - You can find the DynamoDB table name in the Stack Outputs after deploying the Admin stack + - Automated remediations are still toggled per Control ID, and are disabled by default +- Updated several dependencies to address security vulnerabilities +- Migrated to Node's built-in randomUUID() instead of importing uuid +- This solution sends operational metrics to AWS (the "Data") about the use of this solution. We use this Data to better understand how customers use this solution and related services and products. AWS’s collection of this Data is subject to the [AWS Privacy Notice](https://aws.amazon.com/privacy/). + +### Removed + +- EventBridge rules per Control ID +- Filtering configuration in Admin stack parameters + - Filtering settings are now configurable in Systems Manager Parameter Store, e.g. `ASR/Filters/AccountFilters` + +### Fixed + +- S3.1 control ID in the CIS v3 playbook (2.1.4 -> 2.1.4.1) +- Improved logic in EnableCloudTrailToCloudWatchLogging_waitforloggroup remediation script +- Finding link in SNS notifications now links to the finding directly, instead of the control view in the Security Hub console +- Fixed bugs in CloudTrail.5 and CloudWatch.1 remediations +- Fixed resource ID parameter in CloudTrail.4 and CloudTrail.7 control runbooks +- Improved error handling in the Orchestrator Step Function +- Included CreateServiceLinkedRole permissions in GuardDuty.1 remediation role + ## [2.3.2] - 2025-08-14 ### Fixed diff --git a/NOTICE.txt b/NOTICE.txt index 21e48cc0..7a363ee6 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -12,784 +12,998 @@ THIRD PARTY COMPONENTS ********************** This software includes third party software subject to the following copyrights: -@aws-cdk/aws-servicecatalogappregistry-alpha under the Apache License 2.0 -@cdklabs/cdk-ssm-documents under the Apache License 2.0 -@types/jest under the MIT License -@types/js-yaml under the MIT License -@types/node under the MIT License -@types/prettier under the MIT License -@typescript-eslint/eslint-plugin under the MIT License -aws-cdk under the Apache License 2.0 -aws-cdk-lib under the Apache License 2.0 -constructs under the Apache License 2.0 -eslint under the MIT License -eslint-config-prettier under the MIT License -eslint-plugin-header under the MIT License -eslint-plugin-import under the MIT License -eslint-plugin-prettier under the MIT License -jest under the MIT License -js-yaml under the MIT License -source-map-support under the MIT License -ts-jest under the MIT License -ts-node under the MIT License -typescript under the Apache License 2.0 - -attrs under the MIT License -aws-lambda-powertools under the MIT License -awscli under the Apache License 2.0 -boto3 under the Apache License 2.0 -boto3-stubs-lite under the MIT License -botocore under the Apache License 2.0 -botocore-stubs under the MIT License -cffi under the MIT License -colorama under the BSD 3-Clause "New" or "Revised" License -coverage under the Apache License 2.0 -cryptography under the Apache License 2.0 -docutils under the Creative Commons Public Domain Dedication -exceptiongroup under the MIT License -iniconfig under the MIT License -Jinja2 under the BSD 3-Clause "New" or "Revised" License -jmespath under the MIT License -MarkupSafe under the BSD 3-Clause "New" or "Revised" License -moto under the Apache License 2.0 -mypy-boto3-s3 under the MIT License -pip under the MIT License -pluggy under the MIT License -py-partiql-parser under the MIT License -pyasn1 under the BSD 2-Clause "Simplified" License -pycparser under the BSD 3-Clause "New" or "Revised" License -pytest under the MIT License -pytest-cov under the MIT License -pytest-env under the MIT License -pytest-mock under the MIT License -python-dateutil under the Apache License 2.0 and the BSD 3-Clause "New" or "Revised" License -responses under the Apache License 2.0 -rsa under the Apache License 2.0 -s3transfer under the Apache License 2.0 -setuptools under the MIT License -six under the MIT License -tomli under the MIT License -types-PyYAML under the Apache License 2.0 -types-awscrt under the MIT License -types-s3transfer under the MIT License -typing_extensions under the Python Software Foundation License 2.0 -urllib3 under the MIT License -Werkzeug under the BSD 3-Clause "New" or "Revised" License -virtualenv under the MIT License -Jinja2 under the BSD 3-Clause -MarkupSafe under the BSD 3-Clause -Werkzeug under the BSD 3-Clause -boolean.py under the BSD-2-Clause -botocore-stubs under the MIT License -cffi under the MIT License -coverage under the Apache License 2.0 -cryptography under the Apache License 2.0 and the BSD 3-Clause -exceptiongroup under the MIT License -iniconfig under the MIT License -license-expression under the Apache License 2.0 -mypy-boto3-s3 under the MIT License -pluggy under the MIT License -py-partiql-parser under the MIT License -pycparser under the BSD 3-Clause -responses under the Apache License 2.0 -tomli under the MIT License -types-PyYAML under the Apache License 2.0 -types-awscrt under the MIT License -types-s3transfer under the MIT License -typing_extensions under the Python Software Foundation License -xmltodict under the MIT License -aiohttp under the Apache License 2.0 -aiosignal under the Apache License 2.0 -async-timeout under the Apache License 2.0 -black under the MIT License -cachetools under the MIT License -click under the BSD 3-Clause -distlib under the Python Software Foundation License -docker under the Apache License 2.0 -flake8 under the MIT License -frozenlist under the Apache License 2.0 -isort under the MIT License -mccabe under the MIT License -multidict under the Apache License 2.0 -mypy-boto3-cloudformation under the MIT License -mypy-boto3-cloudfront under the MIT License -mypy-boto3-cloudwatch under the MIT License -mypy-boto3-ec2 under the MIT License -mypy-boto3-iam under the MIT License -mypy-boto3-sns under the MIT License -mypy-boto3-ssm under the MIT License -mypy-boto3-sts under the MIT License -mypy-extensions under the MIT License -platformdirs under the MIT License -pycodestyle under the MIT License -pyflakes under the MIT License -pyproject-api under the MIT License -tox under the MIT License -types-urllib3 under the Apache License 2.0 -yarl under the Apache License 2.0 -compare-versions under the MIT license. -@aws-cdk/aws-servicecatalogappregistry-alpha under the Apache-2.0 license. -aws-cdk-lib under the Apache-2.0 license. -@balena/dockerignore under the Apache-2.0 license. -ajv under the MIT license. -ansi-regex under the MIT license. -ansi-styles under the MIT license. -astral-regex under the MIT license. -balanced-match under the MIT license. -brace-expansion under the MIT license. -case under the MIT license. -color-convert under the MIT license. -color-name under the MIT license. -concat-map under the MIT license. -emoji-regex under the MIT license. -fast-deep-equal under the MIT license. -fast-uri under the BSD-3-Clause license. -fs-extra under the MIT license. -graceful-fs under the ISC license. -ignore under the MIT license. -is-fullwidth-code-point under the MIT license. -json-schema-traverse under the MIT license. -jsonfile under the MIT license. -jsonschema under the MIT license. -lodash.truncate under the MIT license. -mime-db under the MIT license. -mime-types under the MIT license. -minimatch under the ISC license. -punycode under the MIT license. -require-from-string under the MIT license. -semver under the ISC license. -slice-ansi under the MIT license. -string-width under the MIT license. -strip-ansi under the MIT license. -table under the BSD-3-Clause license. -universalify under the MIT license. -uri-js under the BSD-2-Clause license. -yaml under the ISC license. -constructs under the Apache-2.0 license. -@aws-cdk/asset-awscli-v1 under the Apache-2.0 license. -@aws-cdk/asset-kubectl-v20 under the Apache-2.0 license. -@aws-cdk/asset-node-proxy-agent-v6 under the Apache-2.0 license. -@aws-cdk/cloud-assembly-schema under the Apache-2.0 license. -@cdklabs/cdk-ssm-documents under the Apache-2.0 license. -argparse under the MIT license. -available-typed-arrays under the MIT license. -aws-sdk under the Apache-2.0 license. -base64-js under the MIT license. -bindings under the MIT license. -buffer under the MIT license. -ieee754 under the BSD-3-Clause license. -call-bind under the MIT license. -deasync under the MIT license. -deep-is under the MIT license. -define-data-property under the MIT license. -es-define-property under the MIT license. -es-errors under the MIT license. -escodegen under the BSD-2-Clause license. -estraverse under the BSD-2-Clause license. -levn under the MIT license. -optionator under the MIT license. -esprima under the BSD-2-Clause license. -esutils under the BSD-2-Clause license. -events under the MIT license. -fast-levenshtein under the MIT license. -file-uri-to-path under the MIT license. -for-each under the MIT license. -function-bind under the MIT license. -get-intrinsic under the MIT license. -gopd under the MIT license. -has-property-descriptors under the MIT license. -has-proto under the MIT license. -has-symbols under the MIT license. -has-tostringtag under the MIT license. -hasown under the MIT license. -immutable under the MIT license. -inherits under the ISC license. -is-arguments under the MIT license. -is-callable under the MIT license. -is-generator-function under the MIT license. -is-typed-array under the MIT license. -isarray under the MIT license. -jmespath under the MIT license. -js-yaml under the MIT license. -jsonpath under the MIT license. -node-addon-api under the MIT license. -possible-typed-array-names under the MIT license. -prelude-ls under the MIT license. -python-shell under the MIT license. -querystring under the MIT license. -sax under the ISC license. -set-function-length under the MIT license. -source-map under the BSD-3-Clause license. -static-eval under the MIT license. -synchronized-promise under the MIT license. -type-check under the MIT license. -underscore under the MIT license. -url under the MIT license. -util under the MIT license. -uuid under the MIT license. -which-typed-array under the MIT license. -word-wrap under the MIT license. -xml2js under the MIT license. -xmlbuilder under the MIT license. -@types/jest under the MIT license. -expect under the MIT license. -@jest/expect-utils under the MIT license. -jest-get-type under the MIT license. -jest-matcher-utils under the MIT license. -chalk under the MIT license. -supports-color under the MIT license. -has-flag under the MIT license. -jest-diff under the MIT license. -diff-sequences under the MIT license. -pretty-format under the MIT license. -@jest/schemas under the MIT license. -@sinclair/typebox under the MIT license. -react-is under the MIT license. -jest-message-util under the MIT license. -@babel/code-frame under the MIT license. -escape-string-regexp under the MIT license. -@babel/highlight under the MIT license. -@babel/helper-validator-identifier under the MIT license. -js-tokens under the MIT license. -@jest/types under the MIT license. -@types/istanbul-lib-coverage under the MIT license. -@types/istanbul-reports under the MIT license. -@types/istanbul-lib-report under the MIT license. -@types/node under the MIT license. -undici-types under the MIT license. -@types/yargs under the MIT license. -@types/yargs-parser under the MIT license. -@types/stack-utils under the MIT license. -micromatch under the MIT license. -braces under the MIT license. -fill-range under the MIT license. -to-regex-range under the MIT license. -is-number under the MIT license. -picomatch under the MIT license. -slash under the MIT license. -stack-utils under the MIT license. -jest-util under the MIT license. -ci-info under the MIT license. -@types/js-yaml under the MIT license. -@types/prettier under the MIT license. -@typescript-eslint/eslint-plugin under the MIT license. -@typescript-eslint/parser under the BSD-2-Clause license. -eslint under the MIT license. -eslint-scope under the BSD-2-Clause license. -esrecurse under the BSD-2-Clause license. -@eslint-community/eslint-utils under the MIT license. -eslint-visitor-keys under the Apache-2.0 license. -@eslint-community/regexpp under the MIT license. -@eslint/eslintrc under the MIT license. -fast-json-stable-stringify under the MIT license. -debug under the MIT license. -ms under the MIT license. -espree under the BSD-2-Clause license. -acorn under the MIT license. -acorn-jsx under the MIT license. -globals under the MIT license. -type-fest under the MIT license. -import-fresh under the MIT license. -parent-module under the MIT license. -callsites under the MIT license. -resolve-from under the MIT license. -strip-json-comments under the MIT license. -@eslint/js under the MIT license. -@humanwhocodes/config-array under the Apache-2.0 license. -@humanwhocodes/object-schema under the BSD-3-Clause license. -@humanwhocodes/module-importer under the Apache-2.0 license. -@nodelib/fs.walk under the MIT license. -@nodelib/fs.scandir under the MIT license. -@nodelib/fs.stat under the MIT license. -run-parallel under the MIT license. -queue-microtask under the MIT license. -fastq under the ISC license. -reusify under the MIT license. -cross-spawn under the MIT license. -path-key under the MIT license. -shebang-command under the MIT license. -shebang-regex under the MIT license. -which under the ISC license. -isexe under the ISC license. -doctrine under the Apache-2.0 license. -esquery under the BSD-3-Clause license. -file-entry-cache under the MIT license. -flat-cache under the MIT license. -flatted under the ISC license. -keyv under the MIT license. -json-buffer under the MIT license. -rimraf under the ISC license. -glob under the ISC license. -fs.realpath under the ISC license. -inflight under the ISC license. -once under the ISC license. -wrappy under the ISC license. -path-is-absolute under the MIT license. -find-up under the MIT license. -locate-path under the MIT license. -p-locate under the MIT license. -p-limit under the MIT license. -yocto-queue under the MIT license. -path-exists under the MIT license. -glob-parent under the ISC license. -is-glob under the MIT license. -is-extglob under the MIT license. -graphemer under the MIT license. -imurmurhash under the MIT license. -is-path-inside under the MIT license. -json-stable-stringify-without-jsonify under the MIT license. -lodash.merge under the MIT license. -natural-compare under the MIT license. -@aashutoshrathi/word-wrap under the MIT license. -text-table under the MIT license. -@typescript-eslint/scope-manager under the MIT license. -@typescript-eslint/types under the MIT license. -@typescript-eslint/visitor-keys under the MIT license. -@typescript-eslint/typescript-estree under the BSD-2-Clause license. -globby under the MIT license. -array-union under the MIT license. -dir-glob under the MIT license. -path-type under the MIT license. -fast-glob under the MIT license. -merge2 under the MIT license. -lru-cache under the ISC license. -yallist under the ISC license. -tsutils under the MIT license. -typescript under the Apache-2.0 license. -tslib under the 0BSD license. -@typescript-eslint/type-utils under the MIT license. -@typescript-eslint/utils under the MIT license. -@types/json-schema under the MIT license. -@types/semver under the MIT license. -natural-compare-lite under the MIT license. -aws-cdk under the Apache-2.0 license. -fsevents under the MIT license. -eslint-config-prettier under the MIT license. -eslint-plugin-header under the MIT license. -eslint-plugin-import under the MIT license. -array-includes under the MIT license. -has under the MIT license. -define-properties under the MIT license. -object-keys under the MIT license. -es-abstract under the MIT license. -array-buffer-byte-length under the MIT license. -is-array-buffer under the MIT license. -arraybuffer.prototype.slice under the MIT license. -is-shared-array-buffer under the MIT license. -es-set-tostringtag under the MIT license. -es-to-primitive under the MIT license. -is-date-object under the MIT license. -is-symbol under the MIT license. -function.prototype.name under the MIT license. -functions-have-names under the MIT license. -get-symbol-description under the MIT license. -globalthis under the MIT license. -internal-slot under the MIT license. -side-channel under the MIT license. -object-inspect under the MIT license. -is-negative-zero under the MIT license. -is-regex under the MIT license. -is-string under the MIT license. -is-weakref under the MIT license. -object.assign under the MIT license. -regexp.prototype.flags under the MIT license. -set-function-name under the MIT license. -safe-array-concat under the MIT license. -safe-regex-test under the MIT license. -string.prototype.trim under the MIT license. -string.prototype.trimend under the MIT license. -string.prototype.trimstart under the MIT license. -typed-array-buffer under the MIT license. -typed-array-byte-length under the MIT license. -typed-array-byte-offset under the MIT license. -typed-array-length under the MIT license. -unbox-primitive under the MIT license. -has-bigints under the MIT license. -which-boxed-primitive under the MIT license. -is-bigint under the MIT license. -is-boolean-object under the MIT license. -is-number-object under the MIT license. -array.prototype.findlastindex under the MIT license. -es-shim-unscopables under the MIT license. -array.prototype.flat under the MIT license. -array.prototype.flatmap under the MIT license. -eslint-import-resolver-node under the MIT license. -is-core-module under the MIT license. -resolve under the MIT license. -path-parse under the MIT license. -supports-preserve-symlinks-flag under the MIT license. -eslint-module-utils under the MIT license. -object.fromentries under the MIT license. -object.groupby under the MIT license. -object.values under the MIT license. -tsconfig-paths under the MIT license. -json5 under the MIT license. -minimist under the MIT license. -strip-bom under the MIT license. -@types/json5 under the MIT license. -eslint-plugin-prettier under the MIT license. -prettier under the MIT license. -prettier-linter-helpers under the MIT license. -fast-diff under the Apache-2.0 license. -synckit under the MIT license. -@pkgr/utils under the MIT license. -open under the MIT license. -default-browser under the MIT license. -execa under the MIT license. -get-stream under the MIT license. -merge-stream under the MIT license. -signal-exit under the ISC license. -human-signals under the Apache-2.0 license. -is-stream under the MIT license. -mimic-fn under the MIT license. -npm-run-path under the MIT license. -onetime under the MIT license. -strip-final-newline under the MIT license. -bundle-name under the MIT license. -run-applescript under the MIT license. -default-browser-id under the MIT license. -bplist-parser under the MIT license. -big-integer under the Unlicense license. -untildify under the MIT license. -titleize under the MIT license. -define-lazy-prop under the MIT license. -is-inside-container under the MIT license. -is-docker under the MIT license. -is-wsl under the MIT license. -picocolors under the ISC license. -jest under the MIT license. -@jest/core under the MIT license. -@jest/console under the MIT license. -@jest/reporters under the MIT license. -@bcoe/v8-coverage under the MIT license. -@jest/test-result under the MIT license. -collect-v8-coverage under the MIT license. -@jest/transform under the MIT license. -@babel/core under the MIT license. -@ampproject/remapping under the Apache-2.0 license. -@jridgewell/gen-mapping under the MIT license. -@jridgewell/set-array under the MIT license. -@jridgewell/sourcemap-codec under the MIT license. -@jridgewell/trace-mapping under the MIT license. -@jridgewell/resolve-uri under the MIT license. -@babel/generator under the MIT license. -@babel/types under the MIT license. -@babel/helper-string-parser under the MIT license. -to-fast-properties under the MIT license. -jsesc under the MIT license. -@babel/helper-compilation-targets under the MIT license. -@babel/compat-data under the MIT license. -@babel/helper-validator-option under the MIT license. -browserslist under the MIT license. -caniuse-lite under the CC-BY-4.0 license. -electron-to-chromium under the ISC license. -node-releases under the MIT license. -update-browserslist-db under the MIT license. -escalade under the MIT license. -@babel/helper-module-transforms under the MIT license. -@babel/helper-environment-visitor under the MIT license. -@babel/helper-module-imports under the MIT license. -@babel/helper-simple-access under the MIT license. -@babel/helper-split-export-declaration under the MIT license. -@babel/helpers under the MIT license. -@babel/template under the MIT license. -@babel/parser under the MIT license. -@babel/traverse under the MIT license. -@babel/helper-function-name under the MIT license. -@babel/helper-hoist-variables under the MIT license. -convert-source-map under the MIT license. -gensync under the MIT license. -babel-plugin-istanbul under the BSD-3-Clause license. -istanbul-lib-instrument under the BSD-3-Clause license. -@istanbuljs/schema under the MIT license. -istanbul-lib-coverage under the BSD-3-Clause license. -@babel/helper-plugin-utils under the MIT license. -@istanbuljs/load-nyc-config under the ISC license. -sprintf-js under the BSD-3-Clause license. -p-try under the MIT license. -camelcase under the MIT license. -get-package-type under the MIT license. -test-exclude under the ISC license. -jest-haste-map under the MIT license. -@types/graceful-fs under the MIT license. -anymatch under the ISC license. -normalize-path under the MIT license. -fb-watchman under the Apache-2.0 license. -bser under the Apache-2.0 license. -node-int64 under the MIT license. -jest-regex-util under the MIT license. -jest-worker under the MIT license. -walker under the Apache-2.0 license. -makeerror under the BSD-3-Clause license. -tmpl under the BSD-3-Clause license. -pirates under the MIT license. -write-file-atomic under the ISC license. -exit under the MIT license. -istanbul-lib-report under the BSD-3-Clause license. -make-dir under the MIT license. -istanbul-lib-source-maps under the BSD-3-Clause license. -istanbul-reports under the BSD-3-Clause license. -html-escaper under the MIT license. -string-length under the MIT license. -char-regex under the MIT license. -v8-to-istanbul under the ISC license. -ansi-escapes under the MIT license. -jest-changed-files under the MIT license. -jest-config under the MIT license. -ts-node under the MIT license. -@cspotcode/source-map-support under the MIT license. -@tsconfig/node10 under the MIT license. -@tsconfig/node12 under the MIT license. -@tsconfig/node14 under the MIT license. -@tsconfig/node16 under the MIT license. -acorn-walk under the MIT license. -arg under the MIT license. -create-require under the MIT license. -diff under the BSD-3-Clause license. -make-error under the ISC license. -v8-compile-cache-lib under the MIT license. -yn under the MIT license. -@jest/test-sequencer under the MIT license. -babel-jest under the MIT license. -@types/babel__core under the MIT license. -@types/babel__generator under the MIT license. -@types/babel__template under the MIT license. -@types/babel__traverse under the MIT license. -babel-preset-jest under the MIT license. -babel-plugin-jest-hoist under the MIT license. -babel-preset-current-node-syntax under the MIT license. -@babel/plugin-syntax-async-generators under the MIT license. -@babel/plugin-syntax-bigint under the MIT license. -@babel/plugin-syntax-class-properties under the MIT license. -@babel/plugin-syntax-import-meta under the MIT license. -@babel/plugin-syntax-json-strings under the MIT license. -@babel/plugin-syntax-logical-assignment-operators under the MIT license. -@babel/plugin-syntax-nullish-coalescing-operator under the MIT license. -@babel/plugin-syntax-numeric-separator under the MIT license. -@babel/plugin-syntax-object-rest-spread under the MIT license. -@babel/plugin-syntax-optional-catch-binding under the MIT license. -@babel/plugin-syntax-optional-chaining under the MIT license. -@babel/plugin-syntax-top-level-await under the MIT license. -deepmerge under the MIT license. -jest-circus under the MIT license. -@jest/environment under the MIT license. -@jest/fake-timers under the MIT license. -@sinonjs/fake-timers under the BSD-3-Clause license. -@sinonjs/commons under the BSD-3-Clause license. -type-detect under the MIT license. -jest-mock under the MIT license. -@jest/expect under the MIT license. -jest-snapshot under the MIT license. -@babel/plugin-syntax-jsx under the MIT license. -@babel/plugin-syntax-typescript under the MIT license. -co under the MIT license. -dedent under the MIT license. -is-generator-fn under the MIT license. -jest-each under the MIT license. -jest-runtime under the MIT license. -@jest/globals under the MIT license. -@jest/source-map under the MIT license. -cjs-module-lexer under the MIT license. -jest-resolve under the MIT license. -jest-pnp-resolver under the MIT license. -jest-validate under the MIT license. -leven under the MIT license. -resolve.exports under the MIT license. -pure-rand under the MIT license. -jest-environment-node under the MIT license. -jest-runner under the MIT license. -source-map-support under the MIT license. -buffer-from under the MIT license. -emittery under the MIT license. -jest-docblock under the MIT license. -detect-newline under the MIT license. -jest-leak-detector under the MIT license. -jest-watcher under the MIT license. -parse-json under the MIT license. -error-ex under the MIT license. -is-arrayish under the MIT license. -json-parse-even-better-errors under the MIT license. -lines-and-columns under the MIT license. -jest-resolve-dependencies under the MIT license. -import-local under the MIT license. -pkg-dir under the MIT license. -resolve-cwd under the MIT license. -jest-cli under the MIT license. -create-jest under the MIT license. -prompts under the MIT license. -kleur under the MIT license. -sisteransi under the MIT license. -yargs under the MIT license. -cliui under the ISC license. -wrap-ansi under the MIT license. -get-caller-file under the ISC license. -require-directory under the MIT license. -y18n under the ISC license. -yargs-parser under the ISC license. -ts-jest under the MIT license. -bs-logger under the MIT license. -lodash.memoize under the MIT license. -@aws-sdk/client-cloudwatch-logs under the Apache-2.0 license. +@aashutoshrathi/word-wrap under the MIT license. +@adobe/css-tools under the MIT license. +@ampproject/remapping under the Apache-2.0 license. +@asamuzakjp/css-color under the MIT license. +@aws-amplify/analytics under the Apache-2.0 license. +@aws-amplify/api under the Apache-2.0 license. +@aws-amplify/api-graphql under the Apache-2.0 license. +@aws-amplify/api-rest under the Apache-2.0 license. +@aws-amplify/auth under the Apache-2.0 license. +@aws-amplify/core under the Apache-2.0 license. +@aws-amplify/data-schema under the Apache-2.0 license. +@aws-amplify/data-schema-types under the Apache-2.0 license. +@aws-amplify/datastore under the Apache-2.0 license. +@aws-amplify/notifications under the Apache-2.0 license. +@aws-amplify/storage under the Apache-2.0 license. +@aws-amplify/ui under the Apache-2.0 license. +@aws-amplify/ui-react under the Apache-2.0 license. +@aws-amplify/ui-react-core under the Apache-2.0 license. +@aws-cdk/asset-awscli-v1 under the Apache-2.0 license. +@aws-cdk/asset-kubectl-v20 under the Apache-2.0 license. +@aws-cdk/asset-node-proxy-agent-v6 under the Apache-2.0 license. +@aws-cdk/aws-servicecatalogappregistry-alpha under the Apache-2.0 license. +@aws-cdk/cloud-assembly-schema under the Apache-2.0 license. +@aws-crypto/crc32 under the Apache-2.0 license. +@aws-crypto/crc32c under the Apache-2.0 license. +@aws-crypto/sha1-browser under the Apache-2.0 license. @aws-crypto/sha256-browser under the Apache-2.0 license. -@smithy/is-array-buffer under the Apache-2.0 license. -@smithy/util-buffer-from under the Apache-2.0 license. -@smithy/util-utf8 under the Apache-2.0 license. @aws-crypto/sha256-js under the Apache-2.0 license. -@aws-crypto/util under the Apache-2.0 license. -@aws-sdk/types under the Apache-2.0 license. -@smithy/types under the Apache-2.0 license. @aws-crypto/supports-web-crypto under the Apache-2.0 license. -@aws-sdk/util-locate-window under the Apache-2.0 license. +@aws-crypto/util under the Apache-2.0 license. +@aws-lambda-powertools/batch under the MIT license. +@aws-lambda-powertools/commons under the MIT license. +@aws-lambda-powertools/logger under the MIT license. +@aws-lambda-powertools/metrics under the MIT license. +@aws-lambda-powertools/tracer under the MIT license. +@aws-sdk/client-cloudformation under the Apache-2.0 license. +@aws-sdk/client-cloudfront under the Apache-2.0 license. +@aws-sdk/client-cloudwatch under the Apache-2.0 license. +@aws-sdk/client-cloudwatch-logs under the Apache-2.0 license. +@aws-sdk/client-cognito-identity-provider under the Apache-2.0 license. +@aws-sdk/client-dynamodb under the Apache-2.0 license. +@aws-sdk/client-ec2 under the Apache-2.0 license. +@aws-sdk/client-firehose under the Apache-2.0 license. +@aws-sdk/client-iam under the Apache-2.0 license. +@aws-sdk/client-kinesis under the Apache-2.0 license. +@aws-sdk/client-lambda under the Apache-2.0 license. +@aws-sdk/client-organizations under the Apache-2.0 license. +@aws-sdk/client-personalize-events under the Apache-2.0 license. +@aws-sdk/client-resource-groups-tagging-api under the Apache-2.0 license. +@aws-sdk/client-s3 under the Apache-2.0 license. +@aws-sdk/client-securityhub under the Apache-2.0 license. +@aws-sdk/client-sfn under the Apache-2.0 license. +@aws-sdk/client-sns under the Apache-2.0 license. +@aws-sdk/client-sqs under the Apache-2.0 license. +@aws-sdk/client-sso under the Apache-2.0 license. @aws-sdk/client-sso-oidc under the Apache-2.0 license. +@aws-sdk/client-ssm under the Apache-2.0 license. @aws-sdk/client-sts under the Apache-2.0 license. @aws-sdk/core under the Apache-2.0 license. -@smithy/core under the Apache-2.0 license. -@smithy/middleware-serde under the Apache-2.0 license. -@smithy/protocol-http under the Apache-2.0 license. -@smithy/util-body-length-browser under the Apache-2.0 license. -@smithy/util-middleware under the Apache-2.0 license. -@smithy/util-stream under the Apache-2.0 license. -@smithy/fetch-http-handler under the Apache-2.0 license. -@smithy/querystring-builder under the Apache-2.0 license. -@smithy/util-uri-escape under the Apache-2.0 license. -@smithy/util-base64 under the Apache-2.0 license. -@smithy/node-http-handler under the Apache-2.0 license. -@smithy/abort-controller under the Apache-2.0 license. -@smithy/util-hex-encoding under the Apache-2.0 license. -@smithy/node-config-provider under the Apache-2.0 license. -@smithy/property-provider under the Apache-2.0 license. -@smithy/shared-ini-file-loader under the Apache-2.0 license. -@smithy/signature-v4 under the Apache-2.0 license. -@smithy/smithy-client under the Apache-2.0 license. -@smithy/middleware-endpoint under the Apache-2.0 license. -@smithy/url-parser under the Apache-2.0 license. -@smithy/querystring-parser under the Apache-2.0 license. -@smithy/middleware-stack under the Apache-2.0 license. -fast-xml-parser under the MIT license. -strnum under the MIT license. -@aws-sdk/credential-provider-node under the Apache-2.0 license. @aws-sdk/credential-provider-env under the Apache-2.0 license. @aws-sdk/credential-provider-http under the Apache-2.0 license. @aws-sdk/credential-provider-ini under the Apache-2.0 license. +@aws-sdk/credential-provider-node under the Apache-2.0 license. @aws-sdk/credential-provider-process under the Apache-2.0 license. @aws-sdk/credential-provider-sso under the Apache-2.0 license. -@aws-sdk/client-sso under the Apache-2.0 license. +@aws-sdk/credential-provider-web-identity under the Apache-2.0 license. +@aws-sdk/endpoint-cache under the Apache-2.0 license. +@aws-sdk/lib-dynamodb under the Apache-2.0 license. +@aws-sdk/middleware-bucket-endpoint under the Apache-2.0 license. +@aws-sdk/middleware-endpoint-discovery under the Apache-2.0 license. +@aws-sdk/middleware-expect-continue under the Apache-2.0 license. +@aws-sdk/middleware-flexible-checksums under the Apache-2.0 license. @aws-sdk/middleware-host-header under the Apache-2.0 license. +@aws-sdk/middleware-location-constraint under the Apache-2.0 license. @aws-sdk/middleware-logger under the Apache-2.0 license. @aws-sdk/middleware-recursion-detection under the Apache-2.0 license. +@aws-sdk/middleware-sdk-ec2 under the Apache-2.0 license. +@aws-sdk/middleware-sdk-s3 under the Apache-2.0 license. +@aws-sdk/middleware-sdk-sqs under the Apache-2.0 license. +@aws-sdk/middleware-ssec under the Apache-2.0 license. @aws-sdk/middleware-user-agent under the Apache-2.0 license. -@aws-sdk/util-endpoints under the Apache-2.0 license. -@smithy/util-endpoints under the Apache-2.0 license. +@aws-sdk/nested-clients under the Apache-2.0 license. @aws-sdk/region-config-resolver under the Apache-2.0 license. -@smithy/util-config-provider under the Apache-2.0 license. +@aws-sdk/s3-request-presigner under the Apache-2.0 license. +@aws-sdk/signature-v4-multi-region under the Apache-2.0 license. +@aws-sdk/token-providers under the Apache-2.0 license. +@aws-sdk/types under the Apache-2.0 license. +@aws-sdk/util-arn-parser under the Apache-2.0 license. +@aws-sdk/util-dynamodb under the Apache-2.0 license. +@aws-sdk/util-endpoints under the Apache-2.0 license. +@aws-sdk/util-format-url under the Apache-2.0 license. +@aws-sdk/util-locate-window under the Apache-2.0 license. @aws-sdk/util-user-agent-browser under the Apache-2.0 license. -bowser under the MIT license. @aws-sdk/util-user-agent-node under the Apache-2.0 license. +@aws-sdk/xml-builder under the Apache-2.0 license. +@aws-solutions-constructs/aws-cloudfront-s3 under the Apache-2.0 license. +@aws-solutions-constructs/aws-eventbridge-sqs under the Apache-2.0 license. +@aws-solutions-constructs/core under the Apache-2.0 license. +@aws-solutions-constructs/resources under the Apache-2.0 license. +@aws/lambda-invoke-store under the Apache-2.0 license. +@babel/code-frame under the MIT license. +@babel/compat-data under the MIT license. +@babel/core under the MIT license. +@babel/generator under the MIT license. +@babel/helper-compilation-targets under the MIT license. +@babel/helper-environment-visitor under the MIT license. +@babel/helper-function-name under the MIT license. +@babel/helper-globals under the MIT license. +@babel/helper-hoist-variables under the MIT license. +@babel/helper-module-imports under the MIT license. +@babel/helper-module-transforms under the MIT license. +@babel/helper-plugin-utils under the MIT license. +@babel/helper-simple-access under the MIT license. +@babel/helper-split-export-declaration under the MIT license. +@babel/helper-string-parser under the MIT license. +@babel/helper-validator-identifier under the MIT license. +@babel/helper-validator-option under the MIT license. +@babel/helpers under the MIT license. +@babel/highlight under the MIT license. +@babel/parser under the MIT license. +@babel/plugin-syntax-async-generators under the MIT license. +@babel/plugin-syntax-bigint under the MIT license. +@babel/plugin-syntax-class-properties under the MIT license. +@babel/plugin-syntax-class-static-block under the MIT license. +@babel/plugin-syntax-import-attributes under the MIT license. +@babel/plugin-syntax-import-meta under the MIT license. +@babel/plugin-syntax-json-strings under the MIT license. +@babel/plugin-syntax-jsx under the MIT license. +@babel/plugin-syntax-logical-assignment-operators under the MIT license. +@babel/plugin-syntax-nullish-coalescing-operator under the MIT license. +@babel/plugin-syntax-numeric-separator under the MIT license. +@babel/plugin-syntax-object-rest-spread under the MIT license. +@babel/plugin-syntax-optional-catch-binding under the MIT license. +@babel/plugin-syntax-optional-chaining under the MIT license. +@babel/plugin-syntax-private-property-in-object under the MIT license. +@babel/plugin-syntax-top-level-await under the MIT license. +@babel/plugin-syntax-typescript under the MIT license. +@babel/runtime under the MIT license. +@babel/template under the MIT license. +@babel/traverse under the MIT license. +@babel/types under the MIT license. +@balena/dockerignore under the Apache-2.0 license. +@bcoe/v8-coverage under the MIT license. +@bundled-es-modules/cookie under the ISC license +@bundled-es-modules/statuses under the ISC license +@bundled-es-modules/tough-cookie under the ISC license +@cdklabs/cdk-ssm-documents under the Apache-2.0 license. +@cloudscape-design/collection-hooks under the Apache-2.0 license. +@cloudscape-design/component-toolkit under the Apache-2.0 license. +@cloudscape-design/components under the Apache-2.0 license. +@cloudscape-design/design-tokens under the Apache-2.0 license. +@cloudscape-design/global-styles under the Apache-2.0 license. +@cloudscape-design/test-utils-core under the Apache-2.0 license. +@cloudscape-design/theming-runtime under the Apache-2.0 license. +@cspotcode/source-map-support under the MIT license. +@csstools/color-helpers under the MIT license. +@csstools/css-calc under the MIT license. +@csstools/css-color-parser under the MIT license. +@csstools/css-parser-algorithms under the MIT license. +@csstools/css-tokenizer under the MIT license. +@dnd-kit/accessibility under the MIT license. +@dnd-kit/core under the MIT license. +@dnd-kit/sortable under the MIT license. +@dnd-kit/utilities under the MIT license. +@emnapi/core under the MIT license. +@emnapi/runtime under the MIT license. +@emnapi/wasi-threads under the MIT license. +@esbuild/aix-ppc64 under the MIT license. +@esbuild/android-arm under the MIT license. +@esbuild/android-arm64 under the MIT license. +@esbuild/android-x64 under the MIT license. +@esbuild/darwin-arm64 under the MIT license. +@esbuild/darwin-x64 under the MIT license. +@esbuild/freebsd-arm64 under the MIT license. +@esbuild/freebsd-x64 under the MIT license. +@esbuild/linux-arm under the MIT license. +@esbuild/linux-arm64 under the MIT license. +@esbuild/linux-ia32 under the MIT license. +@esbuild/linux-loong64 under the MIT license. +@esbuild/linux-mips64el under the MIT license. +@esbuild/linux-ppc64 under the MIT license. +@esbuild/linux-riscv64 under the MIT license. +@esbuild/linux-s390x under the MIT license. +@esbuild/linux-x64 under the MIT license. +@esbuild/netbsd-arm64 under the MIT license. +@esbuild/netbsd-x64 under the MIT license. +@esbuild/openbsd-arm64 under the MIT license. +@esbuild/openbsd-x64 under the MIT license. +@esbuild/openharmony-arm64 under the MIT license. +@esbuild/sunos-x64 under the MIT license. +@esbuild/win32-arm64 under the MIT license. +@esbuild/win32-ia32 under the MIT license. +@esbuild/win32-x64 under the MIT license. +@es-joy/jsdoccomment under the MIT license. +@eslint-community/eslint-utils under the MIT license. +@eslint-community/regexpp under the MIT license. +@eslint/config-array under the Apache-2.0 license. +@eslint/config-helpers under the Apache-2.0 license. +@eslint/core under the Apache-2.0 license. +@eslint/eslintrc under the MIT license. +@eslint/js under the MIT license. +@eslint/object-schema under the Apache-2.0 license. +@eslint/plugin-kit under the Apache-2.0 license. +@floating-ui/core under the MIT license. +@floating-ui/dom under the MIT license. +@floating-ui/react-dom under the MIT license. +@floating-ui/utils under the MIT license. +@formatjs/ecma402-abstract under the MIT license. +@formatjs/fast-memoize under the MIT license. +@formatjs/icu-messageformat-parser under the MIT license. +@formatjs/icu-skeleton-parser under the MIT license. +@formatjs/intl-localematcher under the MIT license. +@humanfs/core under the Apache-2.0 license. +@humanfs/node under the Apache-2.0 license. +@humanwhocodes/config-array under the Apache-2.0 license. +@humanwhocodes/module-importer under the Apache-2.0 license. +@humanwhocodes/object-schema under the BSD 3-Clause license +@humanwhocodes/retry under the Apache-2.0 license. +@inquirer/confirm under the MIT license. +@inquirer/core under the MIT license. +@inquirer/figures under the MIT license. +@inquirer/type under the MIT license. +@isaacs/balanced-match under the MIT license. +@isaacs/brace-expansion under the MIT license. +@isaacs/cliui under the ISC license +@isaacs/fs-minipass under the ISC license +@isaacs/string-locale-compare under the ISC license +@istanbuljs/load-nyc-config under the ISC license +@istanbuljs/schema under the MIT license. +@jest/console under the MIT license. +@jest/core under the MIT license. +@jest/diff-sequences under the MIT license. +@jest/environment under the MIT license. +@jest/expect under the MIT license. +@jest/expect-utils under the MIT license. +@jest/fake-timers under the MIT license. +@jest/get-type under the MIT license. +@jest/globals under the MIT license. +@jest/pattern under the MIT license. +@jest/reporters under the MIT license. +@jest/schemas under the MIT license. +@jest/snapshot-utils under the MIT license. +@jest/source-map under the MIT license. +@jest/test-result under the MIT license. +@jest/test-sequencer under the MIT license. +@jest/transform under the MIT license. +@jest/types under the MIT license. +@jridgewell/gen-mapping under the MIT license. +@jridgewell/remapping under the Apache-2.0 license. +@jridgewell/resolve-uri under the MIT license. +@jridgewell/set-array under the MIT license. +@jridgewell/sourcemap-codec under the MIT license. +@jridgewell/trace-mapping under the MIT license. +@juggle/resize-observer under the Apache-2.0 license. +@middy/core under the MIT license. +@middy/http-cors under the MIT license. +@middy/http-error-handler under the MIT license. +@middy/http-header-normalizer under the MIT license. +@middy/http-json-body-parser under the MIT license. +@middy/http-router under the MIT license. +@middy/http-urlencode-body-parser under the MIT license. +@middy/http-urlencode-path-parser under the MIT license. +@middy/util under the MIT license. +@mswjs/interceptors under the MIT license. +@napi-rs/wasm-runtime under the MIT license. +@nodelib/fs.scandir under the MIT license. +@nodelib/fs.stat under the MIT license. +@nodelib/fs.walk under the MIT license. +@npmcli/agent under the ISC license +@npmcli/arborist under the ISC license +@npmcli/config under the ISC license +@npmcli/fs under the ISC license +@npmcli/git under the ISC license +@npmcli/installed-package-contents under the ISC license +@npmcli/map-workspaces under the ISC license +@npmcli/metavuln-calculator under the ISC license +@npmcli/name-from-folder under the ISC license +@npmcli/node-gyp under the ISC license +@npmcli/package-json under the ISC license +@npmcli/promise-spawn under the ISC license +@npmcli/query under the ISC license +@npmcli/redact under the ISC license +@npmcli/run-script under the ISC license +@open-draft/deferred-promise under the MIT license. +@open-draft/logger under the MIT license. +@open-draft/until under the MIT license. +@pkgjs/parseargs under the MIT license. +@pkgr/core under the MIT license. +@pkgr/utils under the MIT license. +@radix-ui/number under the MIT license. +@radix-ui/primitive under the MIT license. +@radix-ui/react-arrow under the MIT license. +@radix-ui/react-collection under the MIT license. +@radix-ui/react-compose-refs under the MIT license. +@radix-ui/react-context under the MIT license. +@radix-ui/react-direction under the MIT license. +@radix-ui/react-dismissable-layer under the MIT license. +@radix-ui/react-dropdown-menu under the MIT license. +@radix-ui/react-focus-guards under the MIT license. +@radix-ui/react-focus-scope under the MIT license. +@radix-ui/react-id under the MIT license. +@radix-ui/react-menu under the MIT license. +@radix-ui/react-popper under the MIT license. +@radix-ui/react-portal under the MIT license. +@radix-ui/react-presence under the MIT license. +@radix-ui/react-primitive under the MIT license. +@radix-ui/react-roving-focus under the MIT license. +@radix-ui/react-slider under the MIT license. +@radix-ui/react-slot under the MIT license. +@radix-ui/react-use-callback-ref under the MIT license. +@radix-ui/react-use-controllable-state under the MIT license. +@radix-ui/react-use-effect-event under the MIT license. +@radix-ui/react-use-escape-keydown under the MIT license. +@radix-ui/react-use-layout-effect under the MIT license. +@radix-ui/react-use-previous under the MIT license. +@radix-ui/react-use-rect under the MIT license. +@radix-ui/react-use-size under the MIT license. +@radix-ui/rect under the MIT license. +@reduxjs/toolkit under the MIT license. +@rolldown/pluginutils under the MIT license. +@rollup/rollup-android-arm-eabi under the MIT license. +@rollup/rollup-android-arm64 under the MIT license. +@rollup/rollup-darwin-arm64 under the MIT license. +@rollup/rollup-darwin-x64 under the MIT license. +@rollup/rollup-freebsd-arm64 under the MIT license. +@rollup/rollup-freebsd-x64 under the MIT license. +@rollup/rollup-linux-arm-gnueabihf under the MIT license. +@rollup/rollup-linux-arm-musleabihf under the MIT license. +@rollup/rollup-linux-arm64-gnu under the MIT license. +@rollup/rollup-linux-arm64-musl under the MIT license. +@rollup/rollup-linux-loongarch64-gnu under the MIT license. +@rollup/rollup-linux-ppc64-gnu under the MIT license. +@rollup/rollup-linux-riscv64-gnu under the MIT license. +@rollup/rollup-linux-riscv64-musl under the MIT license. +@rollup/rollup-linux-s390x-gnu under the MIT license. +@rollup/rollup-linux-x64-gnu under the MIT license. +@rollup/rollup-linux-x64-musl under the MIT license. +@rollup/rollup-win32-arm64-msvc under the MIT license. +@rollup/rollup-win32-ia32-msvc under the MIT license. +@rollup/rollup-win32-x64-msvc under the MIT license. +@rtsao/scc under the MIT license. +@sigstore/bundle under the Apache-2.0 license. +@sigstore/core under the Apache-2.0 license. +@sigstore/protobuf-specs under the Apache-2.0 license. +@sigstore/sign under the Apache-2.0 license. +@sigstore/tuf under the Apache-2.0 license. +@sigstore/verify under the Apache-2.0 license. +@sinclair/typebox under the MIT license. +@sinonjs/commons under the BSD 3-Clause license +@sinonjs/fake-timers under the BSD 3-Clause license +@sinonjs/samsam under the BSD 3-Clause license +@sinonjs/text-encoding under the Apache-2.0 license. +@smithy/abort-controller under the Apache-2.0 license. +@smithy/chunked-blob-reader under the Apache-2.0 license. +@smithy/chunked-blob-reader-native under the Apache-2.0 license. @smithy/config-resolver under the Apache-2.0 license. +@smithy/core under the Apache-2.0 license. +@smithy/credential-provider-imds under the Apache-2.0 license. +@smithy/eventstream-codec under the Apache-2.0 license. +@smithy/eventstream-serde-browser under the Apache-2.0 license. +@smithy/eventstream-serde-config-resolver under the Apache-2.0 license. +@smithy/eventstream-serde-node under the Apache-2.0 license. +@smithy/eventstream-serde-universal under the Apache-2.0 license. +@smithy/fetch-http-handler under the Apache-2.0 license. +@smithy/hash-blob-browser under the Apache-2.0 license. @smithy/hash-node under the Apache-2.0 license. +@smithy/hash-stream-node under the Apache-2.0 license. @smithy/invalid-dependency under the Apache-2.0 license. +@smithy/is-array-buffer under the Apache-2.0 license. +@smithy/md5-js under the Apache-2.0 license. +@smithy/middleware-compression under the Apache-2.0 license. @smithy/middleware-content-length under the Apache-2.0 license. +@smithy/middleware-endpoint under the Apache-2.0 license. @smithy/middleware-retry under the Apache-2.0 license. +@smithy/middleware-serde under the Apache-2.0 license. +@smithy/middleware-stack under the Apache-2.0 license. +@smithy/node-config-provider under the Apache-2.0 license. +@smithy/node-http-handler under the Apache-2.0 license. +@smithy/property-provider under the Apache-2.0 license. +@smithy/protocol-http under the Apache-2.0 license. +@smithy/querystring-builder under the Apache-2.0 license. +@smithy/querystring-parser under the Apache-2.0 license. @smithy/service-error-classification under the Apache-2.0 license. -@smithy/util-retry under the Apache-2.0 license. +@smithy/shared-ini-file-loader under the Apache-2.0 license. +@smithy/signature-v4 under the Apache-2.0 license. +@smithy/smithy-client under the Apache-2.0 license. +@smithy/types under the Apache-2.0 license. +@smithy/url-parser under the Apache-2.0 license. +@smithy/util-base64 under the Apache-2.0 license. +@smithy/util-body-length-browser under the Apache-2.0 license. @smithy/util-body-length-node under the Apache-2.0 license. +@smithy/util-buffer-from under the Apache-2.0 license. +@smithy/util-config-provider under the Apache-2.0 license. @smithy/util-defaults-mode-browser under the Apache-2.0 license. @smithy/util-defaults-mode-node under the Apache-2.0 license. -@smithy/credential-provider-imds under the Apache-2.0 license. -@aws-sdk/token-providers under the Apache-2.0 license. -@aws-sdk/credential-provider-web-identity under the Apache-2.0 license. -@smithy/eventstream-serde-browser under the Apache-2.0 license. -@smithy/eventstream-serde-universal under the Apache-2.0 license. -@smithy/eventstream-codec under the Apache-2.0 license. -@aws-crypto/crc32 under the Apache-2.0 license. -@smithy/eventstream-serde-config-resolver under the Apache-2.0 license. -@smithy/eventstream-serde-node under the Apache-2.0 license. -@types/uuid under the MIT license. -@aws-sdk/client-s3 under the Apache-2.0 license. -@aws-crypto/sha1-browser under the Apache-2.0 license. -@aws-sdk/middleware-bucket-endpoint under the Apache-2.0 license. -@aws-sdk/util-arn-parser under the Apache-2.0 license. -@aws-sdk/middleware-expect-continue under the Apache-2.0 license. -@aws-sdk/middleware-flexible-checksums under the Apache-2.0 license. -@aws-crypto/crc32c under the Apache-2.0 license. -@aws-sdk/middleware-location-constraint under the Apache-2.0 license. -@aws-sdk/middleware-sdk-s3 under the Apache-2.0 license. -@aws-sdk/middleware-ssec under the Apache-2.0 license. -@aws-sdk/signature-v4-multi-region under the Apache-2.0 license. -@aws-sdk/xml-builder under the Apache-2.0 license. -@smithy/hash-blob-browser under the Apache-2.0 license. -@smithy/chunked-blob-reader under the Apache-2.0 license. -@smithy/chunked-blob-reader-native under the Apache-2.0 license. -@smithy/hash-stream-node under the Apache-2.0 license. -@smithy/md5-js under the Apache-2.0 license. +@smithy/util-endpoints under the Apache-2.0 license. +@smithy/util-hex-encoding under the Apache-2.0 license. +@smithy/util-middleware under the Apache-2.0 license. +@smithy/util-retry under the Apache-2.0 license. +@smithy/util-stream under the Apache-2.0 license. +@smithy/util-uri-escape under the Apache-2.0 license. +@smithy/util-utf8 under the Apache-2.0 license. @smithy/util-waiter under the Apache-2.0 license. -aws-sdk-client-mock under the MIT license. +@smithy/uuid under the Apache-2.0 license. +@standard-schema/spec under the MIT license. +@standard-schema/utils under the MIT license. +@swc/core under the Apache-2.0 license. +@swc/core-darwin-arm64 under the Apache-2.0 license. +@swc/core-darwin-x64 under the Apache-2.0 license. +@swc/core-linux-arm-gnueabihf under the Apache-2.0 license. +@swc/core-linux-arm64-gnu under the Apache-2.0 license. +@swc/core-linux-arm64-musl under the Apache-2.0 license. +@swc/core-linux-x64-gnu under the Apache-2.0 license. +@swc/core-linux-x64-musl under the Apache-2.0 license. +@swc/core-win32-arm64-msvc under the Apache-2.0 license. +@swc/core-win32-ia32-msvc under the Apache-2.0 license. +@swc/core-win32-x64-msvc under the Apache-2.0 license. +@swc/counter under the Apache-2.0 license. +@swc/types under the Apache-2.0 license. +@testing-library/dom under the MIT license. +@testing-library/jest-dom under the MIT license. +@testing-library/react under the MIT license. +@testing-library/user-event under the MIT license. +@tsconfig/node10 under the MIT license. +@tsconfig/node12 under the MIT license. +@tsconfig/node14 under the MIT license. +@tsconfig/node16 under the MIT license. +@tufjs/canonical-json under the MIT license. +@tufjs/models under the MIT license. +@tybys/wasm-util under the MIT license. +@types/aria-query under the MIT license. +@types/aws-lambda under the MIT license. +@types/babel__core under the MIT license. +@types/babel__generator under the MIT license. +@types/babel__template under the MIT license. +@types/babel__traverse under the MIT license. +@types/cfn-response under the MIT license. +@types/chai under the MIT license. +@types/cls-hooked under the MIT license. +@types/cookie under the MIT license. +@types/deep-eql under the MIT license. +@types/estree under the MIT license. +@types/graceful-fs under the MIT license. +@types/istanbul-lib-coverage under the MIT license. +@types/istanbul-lib-report under the MIT license. +@types/istanbul-reports under the MIT license. +@types/jest under the MIT license. +@types/js-yaml under the MIT license. +@types/json-schema under the MIT license. +@types/json5 under the MIT license. +@types/luxon under the MIT license. +@types/mute-stream under the MIT license. +@types/node under the MIT license. +@types/pako under the MIT license. +@types/prettier under the MIT license. +@types/prop-types under the MIT license. +@types/react under the MIT license. +@types/react-dom under the MIT license. +@types/semver under the MIT license. @types/sinon under the MIT license. @types/sinonjs__fake-timers under the MIT license. -sinon under the BSD-3-Clause license. -@sinonjs/samsam under the BSD-3-Clause license. -lodash.get under the MIT license. -nise under the BSD-3-Clause license. -@sinonjs/text-encoding under the Apache-2.0 license. -just-extend under the MIT license. -path-to-regexp under the MIT license. -aws-sdk-client-mock-jest under the MIT license. +@types/stack-utils under the MIT license. +@types/statuses under the MIT license. +@types/tough-cookie under the MIT license. +@types/use-sync-external-store under the MIT license. +@types/uuid under the MIT license. +@types/wrap-ansi under the MIT license. +@types/yargs under the MIT license. +@types/yargs-parser under the MIT license. +@typescript-eslint/eslint-plugin under the MIT license. +@typescript-eslint/parser under the BSD 2-Clause license +@typescript-eslint/project-service under the MIT license. +@typescript-eslint/scope-manager under the MIT license. +@typescript-eslint/tsconfig-utils under the MIT license. +@typescript-eslint/type-utils under the MIT license. +@typescript-eslint/typescript-estree under the BSD 2-Clause license +@typescript-eslint/types under the MIT license. +@typescript-eslint/utils under the MIT license. +@typescript-eslint/visitor-keys under the MIT license. +@ungap/structured-clone under the ISC license +@unrs/resolver-binding-android-arm-eabi under the MIT license. +@unrs/resolver-binding-android-arm64 under the MIT license. +@unrs/resolver-binding-darwin-arm64 under the MIT license. +@unrs/resolver-binding-darwin-x64 under the MIT license. +@unrs/resolver-binding-freebsd-x64 under the MIT license. +@unrs/resolver-binding-linux-arm-gnueabihf under the MIT license. +@unrs/resolver-binding-linux-arm-musleabihf under the MIT license. +@unrs/resolver-binding-linux-arm64-gnu under the MIT license. +@unrs/resolver-binding-linux-arm64-musl under the MIT license. +@unrs/resolver-binding-linux-ppc64-gnu under the MIT license. +@unrs/resolver-binding-linux-riscv64-gnu under the MIT license. +@unrs/resolver-binding-linux-riscv64-musl under the MIT license. +@unrs/resolver-binding-linux-s390x-gnu under the MIT license. +@unrs/resolver-binding-linux-x64-gnu under the MIT license. +@unrs/resolver-binding-linux-x64-musl under the MIT license. +@unrs/resolver-binding-wasm32-wasi under the MIT license. +@unrs/resolver-binding-win32-arm64-msvc under the MIT license. +@unrs/resolver-binding-win32-ia32-msvc under the MIT license. +@unrs/resolver-binding-win32-x64-msvc under the MIT license. +@vitejs/plugin-react-swc under the MIT license. +@vitest/coverage-v8 under the MIT license. @vitest/expect under the MIT license. +@vitest/mocker under the MIT license. +@vitest/pretty-format under the MIT license. +@vitest/runner under the MIT license. +@vitest/snapshot under the MIT license. @vitest/spy under the MIT license. -tinyspy under the MIT license. @vitest/utils under the MIT license. -@vitest/pretty-format under the MIT license. -tinyrainbow under the MIT license. -loupe under the MIT license. -chai under the MIT license. -assertion-error under the MIT license. -check-error under the MIT license. -deep-eql under the MIT license. -pathval under the MIT license. -@babel/plugin-syntax-class-static-block under the MIT license. -@babel/plugin-syntax-import-attributes under the MIT license. -@babel/plugin-syntax-private-property-in-object under the MIT license. -aws-lambda-powertools under the MIT license. -aws-xray-sdk under the Apache-2.0 license. -botocore under the Apache-2.0 license. -python-dateutil under the Apache-2.0 license. -six under the MIT license. -typing-extensions under the PSF-2.0 license. -urllib3 under the MIT license. -wrapt under the 0BSD license. +@xstate/react under the MIT license. +abbrev under the ISC license +ace-builds under the MIT license. +acorn under the MIT license. +acorn-jsx under the MIT license. +acorn-walk under the MIT license. +agent-base under the MIT license. +aiohttp under the Apache-2.0 license. +aiosignal under the Apache-2.0 license. +ajv under the MIT license. +ansi-escapes under the MIT license. +ansi-regex under the MIT license. +ansi-styles under the MIT license. annotated-types under the MIT license. +anymatch under the ISC license +aproba under the ISC license +archy under the ISC license +are-docs-informative under the MIT license. +are-we-there-yet under the ISC license +arg under the MIT license. +argparse under the MIT license. +aria-hidden under the MIT license. +aria-query under the Apache-2.0 license. +array-buffer-byte-length under the MIT license. +array-includes under the MIT license. +array-union under the MIT license. +array.prototype.findlastindex under the MIT license. +array.prototype.flat under the MIT license. +array.prototype.flatmap under the MIT license. +arraybuffer.prototype.slice under the MIT license. +as-needed under the MIT license. +assertion-error under the MIT license. +ast-v8-to-istanbul under the ISC license +astral-regex under the MIT license. +async under the MIT license. +async-function under the MIT license. +async-hook-jl under the MIT license. +async-timeout under the Apache-2.0 license. +atomic-batcher under the MIT license. attrs under the MIT license. +available-typed-arrays under the MIT license. +aws-amplify under the Apache-2.0 license. +aws-cdk under the Apache-2.0 license. +aws-cdk-lib under the Apache-2.0 license. aws-encryption-sdk under the Apache-2.0 license. +aws-lambda under the MIT license. aws-lambda-context under the MIT license. +aws-lambda-powertools under the MIT license. +aws-sdk under the Apache-2.0 license. +aws-sdk-client-mock under the MIT license. +aws-sdk-client-mock-jest under the MIT license. +aws-xray-sdk under the Apache-2.0 license. +aws-xray-sdk-core under the Apache-2.0 license. +awscli under the Apache-2.0 license. +babel-jest under the MIT license. +babel-plugin-istanbul under the BSD 3-Clause license +babel-plugin-jest-hoist under the MIT license. +babel-preset-current-node-syntax under the MIT license. +babel-preset-jest under the MIT license. +balanced-match under the MIT license. +base64-js under the MIT license. +baseline-browser-mapping under the MIT license. +big-integer under the Unlicense license. +bin-links under the ISC license +binary-extensions under the MIT license. +bindings under the MIT license. black under the MIT license. +boolean.py under the BSD 2-Clause license boto3 under the Apache-2.0 license. boto3-stubs-lite under the MIT license. +botocore under the Apache-2.0 license. botocore-stubs under the MIT license. +bowser under the MIT license. +bplist-parser under the MIT license. +brace-expansion under the MIT license. +braces under the MIT license. +browserslist under the MIT license. +bs-logger under the MIT license. +bser under the Apache-2.0 license. +buffer under the MIT license. +buffer-from under the MIT license. +bundle-name under the MIT license. +cac under the MIT license. +cacache under the ISC license cachetools under the MIT license. +call-bind under the MIT license. +call-bind-apply-helpers under the MIT license. +call-bound under the MIT license. +callsites under the MIT license. +camelcase under the MIT license. +caniuse-lite under the CC-BY-4.0 license. +case under the MIT license. certifi under the MPL-2.0 license. cffi under the MIT license. -chardet under the LGPL license(s). +chai under the MIT license. +chalk under the MIT license. +char-regex under the MIT license. +chardet under the LGPL license. charset-normalizer under the MIT license. -click under the 0BSD license. -colorama under the 0BSD license. +check-error under the MIT license. +chownr under the ISC license +ci-info under the MIT license. +cidr-regex under the BSD 2-Clause license +cjs-module-lexer under the MIT license. +cli-columns under the MIT license. +cli-width under the ISC license +click under the BSD 3-Clause license +cliui under the ISC license +clsx under the MIT license. +cls-hooked under the BSD 2-Clause license +cmd-shim under the ISC license +co under the MIT license. +collect-v8-coverage under the MIT license. +color-convert under the MIT license. +color-name under the MIT license. +color-support under the ISC license +colorama under the BSD 3-Clause license +commander under the MIT license. +common-ancestor-path under the ISC license +compare-versions under the MIT license. +concat-map under the MIT license. +console-control-strings under the ISC license +constructs under the Apache-2.0 license. +convert-source-map under the MIT license. +cookie under the MIT license. coverage under the Apache-2.0 license. +crc-32 under the Apache-2.0 license. +create-jest under the MIT license. +create-require under the MIT license. +cross-spawn under the MIT license. cryptography under the Apache-2.0 license. +css-selector-tokenizer under the MIT license. +css.escape under the MIT license. +cssesc under the MIT license. +cssstyle under the MIT license. +csstype under the MIT license. +d3-path under the BSD 3-Clause license +d3-shape under the BSD 3-Clause license +data-uri-to-buffer under the MIT license. +data-urls under the MIT license. +data-view-buffer under the MIT license. +data-view-byte-length under the MIT license. +data-view-byte-offset under the MIT license. +date-fns under the MIT license. +debug under the MIT license. +deasync under the MIT license. +decamelize under the MIT license. +decimal.js under the MIT license. +dedent under the MIT license. +deep-diff under the MIT license. +deep-eql under the MIT license. +deep-is under the MIT license. +deepmerge under the MIT license. +default-browser under the MIT license. +default-browser-id under the MIT license. +define-data-property under the MIT license. +define-lazy-prop under the MIT license. +define-properties under the MIT license. +dequal under the MIT license. +detect-newline under the MIT license. +detect-node-es under the MIT license. +diff under the BSD 3-Clause license +diff-sequences under the MIT license. +dijkstrajs under the MIT license. +dir-glob under the MIT license. distlib under the PSF-2.0 license. docker under the Apache-2.0 license. +doctrine under the Apache-2.0 license. +docutils under the BSD license. +dom-accessibility-api under the MIT license. +dom-helpers under the MIT license. +dunder-proto under the MIT license. +eastasianwidth under the MIT license. +ejs under the Apache-2.0 license. +electron-to-chromium under the ISC license +emittery under the MIT license. +emitter-listener under the BSD 2-Clause license +emoji-regex under the MIT license. +encode-utf8 under the MIT license. +encoding under the MIT license. +enhanced-resolve under the MIT license. +entities under the BSD 2-Clause license +env-paths under the MIT license. +err-code under the MIT license. +error-ex under the MIT license. +es-abstract under the MIT license. +es-define-property under the MIT license. +es-errors under the MIT license. +es-module-lexer under the MIT license. +es-object-atoms under the MIT license. +es-set-tostringtag under the MIT license. +es-shim-unscopables under the MIT license. +es-to-primitive under the MIT license. +escalade under the MIT license. +escape-string-regexp under the MIT license. +esbuild under the MIT license. +escodegen under the BSD 2-Clause license +eslint under the MIT license. +eslint-compat-utils under the MIT license. +eslint-config-prettier under the MIT license. +eslint-import-resolver-node under the MIT license. +eslint-module-utils under the MIT license. +eslint-plugin-es under the MIT license. +eslint-plugin-es-x under the MIT license. +eslint-plugin-header under the MIT license. +eslint-plugin-import under the MIT license. +eslint-plugin-jsdoc under the BSD 3-Clause license +eslint-plugin-n under the MIT license. +eslint-plugin-node under the MIT license. +eslint-plugin-prettier under the MIT license. +eslint-plugin-promise under the ISC license +eslint-plugin-react-hooks under the MIT license. +eslint-plugin-react-refresh under the MIT license. +eslint-scope under the BSD 2-Clause license +eslint-utils under the MIT license. +eslint-visitor-keys under the Apache-2.0 license. +espree under the BSD 2-Clause license +esprima under the BSD 2-Clause license +esquery under the BSD 3-Clause license +esrecurse under the BSD 2-Clause license +estraverse under the BSD 2-Clause license +estree-walker under the MIT license. +esutils under the BSD 2-Clause license +events under the MIT license. +exceptiongroup under the MIT license. +execa under the MIT license. +exit under the MIT license. +exit-x under the MIT license. +expect under the MIT license. +expect-type under the Apache-2.0 license. +exponential-backoff under the Apache-2.0 license. +fast-deep-equal under the MIT license. +fast-diff under the Apache-2.0 license. +fast-glob under the MIT license. +fast-json-stable-stringify under the MIT license. +fast-levenshtein under the MIT license. +fast-uri under the BSD 3-Clause license +fast-xml-parser under the MIT license. +fastest-levenshtein under the MIT license. fastjsonschema under the 0BSD license. -filelock under the The Unlicense (Unlicense) license(s). +fastparse under the MIT license. +fastq under the ISC license +fb-watchman under the Apache-2.0 license. +fdir under the MIT license. +fetch-blob under the MIT license. +fflate under the MIT license. +file-entry-cache under the MIT license. +file-uri-to-path under the MIT license. +filelock under the Unlicense license. +filelist under the Apache-2.0 license. +fill-range under the MIT license. +find-up under the MIT license. flake8 under the MIT license. +flat-cache under the MIT license. +flatted under the ISC license +for-each under the MIT license. +foreground-child under the ISC license +formdata-polyfill under the MIT license. +frozenlist under the Apache-2.0 license. +fs-extra under the MIT license. +fs-minipass under the ISC license +fs.realpath under the ISC license +fsevents under the MIT license. +function-bind under the MIT license. +function.prototype.name under the MIT license. +functions-have-names under the MIT license. +gauge under the ISC license +gensync under the MIT license. +get-caller-file under the ISC license +get-intrinsic under the MIT license. +get-nonce under the MIT license. +get-package-type under the MIT license. +get-proto under the MIT license. +get-stream under the MIT license. +get-symbol-description under the MIT license. +get-tsconfig under the MIT license. +glob under the ISC license +glob-parent under the ISC license +glob-to-regexp under the BSD 2-Clause license +globby under the MIT license. +globrex under the MIT license. +globalthis under the MIT license. +globals under the MIT license. +gopd under the MIT license. +graceful-fs under the ISC license +graphemer under the MIT license. +graphql under the MIT license. +handlebars under the MIT license. +has under the MIT license. +has-bigints under the MIT license. +has-flag under the MIT license. +has-property-descriptors under the MIT license. +has-proto under the MIT license. +has-symbols under the MIT license. +has-tostringtag under the MIT license. +has-unicode under the ISC license +hasown under the MIT license. +headers-polyfill under the MIT license. +hosted-git-info under the ISC license +html-encoding-sniffer under the MIT license. +html-escaper under the MIT license. +http-cache-semantics under the BSD 2-Clause license +http-proxy-agent under the MIT license. +https-proxy-agent under the MIT license. +human-signals under the Apache-2.0 license. +iconv-lite under the MIT license. +idb under the ISC license. idna under the 0BSD license. +ieee754 under the BSD 3-Clause license +ignore under the MIT license. +ignore-walk under the ISC license +immer under the MIT license. +immutable under the MIT license. +imurmurhash under the MIT license. +import-fresh under the MIT license. +import-local under the MIT license. +indent-string under the MIT license. +inflight under the ISC license +inherits under the ISC license +internal-slot under the MIT license. +ini under the ISC license iniconfig under the MIT license. +init-package-json under the ISC license +install under the MIT license. +intl-messageformat under the BSD 3-Clause license +ip-address under the MIT license. +ip-regex under the MIT license. +is-arguments under the MIT license. +is-array-buffer under the MIT license. +is-arrayish under the MIT license. +is-async-function under the MIT license. +is-bigint under the MIT license. +is-boolean-object under the MIT license. +is-callable under the MIT license. +is-cidr under the BSD 2-Clause license +is-core-module under the MIT license. +is-data-view under the MIT license. +is-date-object under the MIT license. +is-docker under the MIT license. +is-extglob under the MIT license. +is-finalizationregistry under the MIT license. +is-fullwidth-code-point under the MIT license. +is-generator-fn under the MIT license. +is-generator-function under the MIT license. +is-glob under the MIT license. +is-inside-container under the MIT license. +is-map under the MIT license. +is-negative-zero under the MIT license. +is-node-process under the MIT license. +is-number under the MIT license. +is-number-object under the MIT license. +is-path-inside under the MIT license. +is-potential-custom-element-name under the MIT license. +is-regex under the MIT license. +is-set under the MIT license. +is-shared-array-buffer under the MIT license. +is-stream under the MIT license. +is-string under the MIT license. +is-symbol under the MIT license. +is-typed-array under the MIT license. +is-weakmap under the MIT license. +is-weakref under the MIT license. +is-weakset under the MIT license. +is-wsl under the MIT license. +isarray under the MIT license. +isexe under the ISC license isort under the MIT license. -jinja2 under the 0BSD license. +istanbul-lib-coverage under the BSD 3-Clause license +istanbul-lib-instrument under the BSD 3-Clause license +istanbul-lib-report under the BSD 3-Clause license +istanbul-lib-source-maps under the BSD 3-Clause license +istanbul-reports under the BSD 3-Clause license +jackspeak under the BlueOak-1.0.0 license. +jake under the Apache-2.0 license. +jest under the MIT license. +jest-changed-files under the MIT license. +jest-circus under the MIT license. +jest-cli under the MIT license. +jest-config under the MIT license. +jest-diff under the MIT license. +jest-docblock under the MIT license. +jest-each under the MIT license. +jest-environment-node under the MIT license. +jest-get-type under the MIT license. +jest-haste-map under the MIT license. +jest-leak-detector under the MIT license. +jest-matcher-utils under the MIT license. +jest-message-util under the MIT license. +jest-mock under the MIT license. +jest-pnp-resolver under the MIT license. +jest-regex-util under the MIT license. +jest-resolve under the MIT license. +jest-resolve-dependencies under the MIT license. +jest-runner under the MIT license. +jest-runtime under the MIT license. +jest-snapshot under the MIT license. +jest-util under the MIT license. +jest-validate under the MIT license. +jest-watcher under the MIT license. +jest-worker under the MIT license. +jinja2 under the BSD 3-Clause license +jmespath under the MIT license. +js-cookie under the MIT license. +js-tokens under the MIT license. +js-yaml under the MIT license. +jsdom under the MIT license. +jsesc under the MIT license. +json-buffer under the MIT license. +json-parse-even-better-errors under the MIT license. +json-schema-traverse under the MIT license. +json-stable-stringify-without-jsonify under the MIT license. +json-stringify-nice under the ISC license +json-stringify-safe under the ISC license +json5 under the MIT license. +jsonfile under the MIT license. +jsonparse under the MIT license. +jsonpath under the MIT license. jsonpath-ng under the Apache-2.0 license. +jsonschema under the MIT license. jsonschema-path under the Apache-2.0 license. jsonschema-specifications under the MIT license. +just-diff under the MIT license. +just-diff-apply under the MIT license. +just-extend under the MIT license. +keyv under the MIT license. +kleur under the MIT license. lazy-object-proxy under the 0BSD license. -markupsafe under the 0BSD license. +leven under the MIT license. +levn under the MIT license. +libnpmaccess under the ISC license +libnpmdiff under the ISC license +libnpmexec under the ISC license +libnpmfund under the ISC license +libnpmorg under the ISC license +libnpmpack under the ISC license +libnpmpublish under the ISC license +libnpmsearch under the ISC license +libnpmteam under the ISC license +libnpmversion under the ISC license +license-expression under the Apache-2.0 license. +lines-and-columns under the MIT license. +locate-path under the MIT license. +lodash under the MIT license. +lodash.get under the MIT license. +lodash.memoize under the MIT license. +lodash.merge under the MIT license. +lodash.truncate under the MIT license. +loose-envify under the MIT license. +loupe under the MIT license. +lru-cache under the ISC license +luxon under the MIT license. +lz-string under the MIT license. +magic-string under the MIT license. +magicast under the MIT license. +make-dir under the MIT license. +make-error under the ISC license +make-fetch-happen under the ISC license +makeerror under the BSD 3-Clause license +markupsafe under the BSD 3-Clause license +math-intrinsics under the MIT license. mccabe under the MIT license. +merge-stream under the MIT license. +merge2 under the MIT license. +micromatch under the MIT license. +mime-db under the MIT license. +mime-types under the MIT license. +mimic-fn under the MIT license. +min-indent under the MIT license. +minimatch under the ISC license +minimist under the MIT license. +minipass under the ISC license +minipass-collect under the ISC license +minipass-fetch under the MIT license. +minipass-flush under the ISC license +minipass-pipeline under the ISC license +minipass-sized under the ISC license +minizlib under the MIT license. +mnemonist under the MIT license. +mnth under the MIT license. moto under the Apache-2.0 license. +ms under the MIT license. +msw under the MIT license. +multidict under the Apache-2.0 license. +mute-stream under the ISC license mypy under the MIT license. mypy-boto3-cloudformation under the MIT license. mypy-boto3-cloudfront under the MIT license. @@ -801,108 +1015,384 @@ mypy-boto3-sns under the MIT license. mypy-boto3-ssm under the MIT license. mypy-boto3-sts under the MIT license. mypy-extensions under the MIT license. +nanoid under the MIT license. +napi-postinstall under the MIT license. +natural-compare under the MIT license. +natural-compare-lite under the MIT license. +negotiator under the MIT license. +neo-async under the MIT license. +nise under the BSD 3-Clause license +nock under the MIT license. +node-addon-api under the MIT license. +node-domexception under the MIT license. +node-fetch under the MIT license. +node-gyp under the MIT license. +node-int64 under the MIT license. +node-releases under the MIT license. +nopt under the ISC license +normalize-package-data under the BSD 2-Clause license +normalize-path under the MIT license. +npm under the Artistic-2.0 license. +npm-audit-report under the ISC license +npm-bundled under the ISC license +npm-install-checks under the BSD 2-Clause license +npm-normalize-package-bin under the ISC license +npm-package-arg under the ISC license +npm-packlist under the ISC license +npm-pick-manifest under the ISC license +npm-profile under the ISC license +npm-registry-fetch under the ISC license +npm-run-path under the MIT license. +npm-user-validate under the ISC license +npmlog under the ISC license +nwsapi under the MIT license. +object-assign under the MIT license. +object-inspect under the MIT license. +object-keys under the MIT license. +object.assign under the MIT license. +object.fromentries under the MIT license. +object.groupby under the MIT license. +object.values under the MIT license. +obliterator under the MIT license. +once under the ISC license +onetime under the MIT license. +open under the MIT license. openapi-schema-validator under the 0BSD license. openapi-spec-validator under the Apache-2.0 license. +optionator under the MIT license. +outvariant under the MIT license. +own-keys under the MIT license. +p-limit under the MIT license. +p-locate under the MIT license. +p-map under the MIT license. +p-try under the MIT license. +package-json-from-dist under the BlueOak-1.0.0 license. packaging under the Apache-2.0 license. +pacote under the ISC license +pako under the MIT license. +parent-module under the MIT license. +parse-conflict-json under the ISC license +parse-imports-exports under the MIT license. +parse-json under the MIT license. +parse-statements under the MIT license. +parse5 under the MIT license. +path-exists under the MIT license. +path-is-absolute under the MIT license. +path-key under the MIT license. +path-parse under the MIT license. +path-scurry under the BlueOak-1.0.0 license. +path-to-regexp under the MIT license. +path-type under the MIT license. +pathe under the MIT license. pathable under the Apache-2.0 license. pathspec under the MPL-2.0 license. +pathval under the MIT license. +picocolors under the ISC license +picomatch under the MIT license. +pip under the MIT license. +pirates under the MIT license. +pkg-dir under the MIT license. platformdirs under the MIT license. pluggy under the MIT license. ply under the 0BSD license. +pngjs under the MIT license. +possible-typed-array-names under the MIT license. +postcss under the MIT license. +postcss-selector-parser under the MIT license. +prelude-ls under the MIT license. +prettier under the MIT license. +prettier-linter-helpers under the MIT license. +pretty-format under the MIT license. +proc-log under the ISC license +proggy under the ISC license +promise-all-reject-late under the ISC license +promise-call-limit under the ISC license +promise-retry under the MIT license. +prompts under the MIT license. +promzard under the ISC license +prop-types under the MIT license. +propagate under the MIT license. +psl under the MIT license. +pure-rand under the MIT license. +punycode under the MIT license. py-partiql-parser under the MIT license. +pyasn1 under the BSD 2-Clause license pycodestyle under the MIT license. -pycparser under the 0BSD license. +pycparser under the BSD 3-Clause license pydantic under the MIT license. -pydantic_core under the MIT license. +pydantic-core under the MIT license. +pydantic-settings under the MIT license. pyflakes under the MIT license. +pygments under the 0BSD license. pyproject-api under the MIT license. pytest under the MIT license. pytest-cov under the MIT license. pytest-env under the MIT license. pytest-mock under the MIT license. +python-dateutil under the Apache-2.0 license. +python-dotenv under the BSD 3-Clause license +python-shell under the MIT license. pywin32 under the PSF-2.0 license. pyyaml under the MIT license. +qrcode under the MIT license. +qrcode-terminal under the Apache-2.0 license. +querystring under the MIT license. +typing-extensions under the PSF-2.0 license. +querystringify under the MIT license. +queue-microtask under the MIT license. +react under the MIT license. +react-dom under the MIT license. +react-hook-form under the MIT license. +react-is under the MIT license. +react-keyed-flatten-children under the MIT license. +react-redux under the MIT license. +react-remove-scroll under the MIT license. +react-remove-scroll-bar under the MIT license. +react-router under the MIT license. +react-router-dom under the MIT license. +react-style-singleton under the MIT license. +react-transition-group under the BSD 3-Clause license +read under the ISC license +read-cmd-shim under the ISC license +redent under the MIT license. +redux under the MIT license. +redux-thunk under the MIT license. referencing under the MIT license. +reflect.getprototypeof under the MIT license. +regexpp under the MIT license. +regexp.prototype.flags under the MIT license. requests under the Apache-2.0 license. +require-directory under the MIT license. +require-from-string under the MIT license. +require-main-filename under the ISC license +requires-port under the MIT license. +reselect under the MIT license. +resolve under the MIT license. +resolve-cwd under the MIT license. +resolve-from under the MIT license. +resolve-pkg-maps under the MIT license. +resolve.exports under the MIT license. responses under the Apache-2.0 license. +retry under the MIT license. +reusify under the MIT license. rfc3339-validator under the MIT license. +rimraf under the ISC license +rollup under the MIT license. rpds-py under the MIT license. +rrweb-cssom under the MIT license. +rsa under the Apache-2.0 license. +run-applescript under the MIT license. +run-parallel under the MIT license. +rxjs under the Apache-2.0 license. s3transfer under the Apache-2.0 license. +safe-array-concat under the MIT license. +safe-push-apply under the MIT license. +safe-regex-test under the MIT license. +safer-buffer under the MIT license. +sax under the ISC license +saxes under the ISC license +scheduler under the MIT license. +semver under the ISC license +set-blocking under the ISC license +set-cookie-parser under the MIT license. +set-function-length under the MIT license. +set-function-name under the MIT license. +set-proto under the MIT license. +setuptools under the MIT license. +shebang-command under the MIT license. +shebang-regex under the MIT license. +shimmer under the BSD 2-Clause license +side-channel under the MIT license. +side-channel-list under the MIT license. +side-channel-map under the MIT license. +side-channel-weakmap under the MIT license. +siginfo under the ISC license +signal-exit under the ISC license +sigstore under the Apache-2.0 license. +sinon under the BSD 3-Clause license +sisteransi under the MIT license. +six under the MIT license. +slash under the MIT license. +slice-ansi under the MIT license. +smart-buffer under the MIT license. +socks under the MIT license. +socks-proxy-agent under the MIT license. +source-map under the BSD 3-Clause license +source-map-js under the BSD 3-Clause license +source-map-support under the MIT license. +spdx-correct under the Apache-2.0 license. +spdx-exceptions under the CC-BY-3.0 license. +spdx-expression-parse under the MIT license. +spdx-license-ids under the CC0-1.0 license. +sprintf-js under the BSD 3-Clause license +ssri under the ISC license +stack-chain under the MIT license. +stack-utils under the MIT license. +stackback under the MIT license. +static-eval under the MIT license. +statuses under the MIT license. +std-env under the MIT license. +stop-iteration-iterator under the MIT license. +strict-event-emitter under the MIT license. +string-length under the MIT license. +string-width under the MIT license. +string.prototype.trim under the MIT license. +string.prototype.trimend under the MIT license. +string.prototype.trimstart under the MIT license. +strip-ansi under the MIT license. +strip-bom under the MIT license. +strip-final-newline under the MIT license. +strip-indent under the MIT license. +strip-json-comments under the MIT license. +strip-literal under the MIT license. +strnum under the MIT license. +supports-color under the MIT license. +supports-preserve-symlinks-flag under the MIT license. +symbol-tree under the MIT license. +synchronized-promise under the MIT license. +synckit under the MIT license. +table under the BSD 3-Clause license +tapable under the MIT license. +tar under the ISC license +test-exclude under the ISC license +text-table under the MIT license. +tinybench under the MIT license. +tinyexec under the MIT license. +tiny-relative-date under the MIT license. +tinyglobby under the MIT license. +tinypool under the MIT license. +tinyrainbow under the MIT license. +tinyspy under the MIT license. +titleize under the MIT license. +tldts under the MIT license. +tldts-core under the MIT license. +tmpl under the BSD 3-Clause license +to-fast-properties under the MIT license. +to-regex-range under the MIT license. +tomli under the MIT license. +tough-cookie under the BSD 3-Clause license tox under the MIT license. +tr46 under the MIT license. +treeverse under the ISC license +ts-api-utils under the MIT license. +ts-declaration-location under the BSD 3-Clause license +ts-jest under the MIT license. +ts-node under the MIT license. +tsconfig-paths under the MIT license. +tsimportlib under the MIT license. +tslib under the 0BSD license. +tsutils under the MIT license. +tuf-js under the MIT license. +type-check under the MIT license. +type-detect under the MIT license. +type-fest under the MIT license. +typed-array-buffer under the MIT license. +typed-array-byte-length under the MIT license. +typed-array-byte-offset under the MIT license. +typed-array-length under the MIT license. types-awscrt under the MIT license. +types-PyYAML under the Apache-2.0 license. types-s3transfer under the MIT license. types-urllib3 under the Apache-2.0 license. -virtualenv under the MIT license. -werkzeug under the 0BSD license. -xmltodict under the MIT license. -pydantic-core under the MIT license. -pydantic-settings under the MIT license. -python-dotenv under the BSD-3-Clause license. +typescript under the Apache-2.0 license. +pywin32 under the PSF-2.0 license. typing-inspection under the MIT license. -pygments under the 0BSD license. -@aws-sdk/client-cloudformation under the Apache-2.0 license. -@aws-sdk/client-cloudwatch under the Apache-2.0 license. -@aws-sdk/client-ec2 under the Apache-2.0 license. -@aws-sdk/client-iam under the Apache-2.0 license. -@aws-sdk/client-lambda under the Apache-2.0 license. -@aws-sdk/client-sns under the Apache-2.0 license. -@aws-sdk/client-sqs under the Apache-2.0 license. -@aws-sdk/client-ssm under the Apache-2.0 license. -@aws-sdk/middleware-sdk-ec2 under the Apache-2.0 license. -@aws-sdk/middleware-sdk-sqs under the Apache-2.0 license. -@aws-sdk/nested-clients under the Apache-2.0 license. -@aws-sdk/util-format-url under the Apache-2.0 license. -@smithy/middleware-compression under the Apache-2.0 license. -fflate under the MIT license. -@ungap/structured-clone under the ISC license. -@rtsao/scc under the MIT license. -call-bind-apply-helpers under the MIT license. -es-object-atoms under the MIT license. -get-proto under the MIT license. -dunder-proto under the MIT license. -math-intrinsics under the MIT license. -call-bound under the MIT license. -data-view-buffer under the MIT license. -is-data-view under the MIT license. -data-view-byte-length under the MIT license. -data-view-byte-offset under the MIT license. -side-channel-list under the MIT license. -side-channel-map under the MIT license. -side-channel-weakmap under the MIT license. -is-set under the MIT license. -own-keys under the MIT license. -safe-push-apply under the MIT license. -set-proto under the MIT license. -stop-iteration-iterator under the MIT license. -reflect.getprototypeof under the MIT license. +uglify-js under the BSD 2-Clause license +ulid under the MIT license. +unbox-primitive under the MIT license. +undici-types under the MIT license. +underscore under the MIT license. +unique-filename under the ISC license +unique-slug under the ISC license +universalify under the MIT license. +unrs-resolver under the MIT license. +untildify under the MIT license. +update-browserslist-db under the MIT license. +url under the MIT license. +url-parse under the MIT license. +uri-js under the BSD 2-Clause license +urllib3 under the MIT license. +use-callback-ref under the MIT license. +use-isomorphic-layout-effect under the MIT license. +use-sidecar under the MIT license. +use-sync-external-store under the MIT license. +util under the MIT license. +util-deprecate under the MIT license. +uuid under the MIT license. +v8-compile-cache-lib under the MIT license. +v8-to-istanbul under the ISC license +validate-npm-package-license under the Apache-2.0 license. +validate-npm-package-name under the ISC license +vite under the MIT license. +vite-node under the MIT license. +virtualenv under the MIT license. +vitest under the MIT license. +walker under the Apache-2.0 license. +walk-up-path under the ISC license +w3c-xmlserializer under the MIT license. +watchpack under the MIT license. +web-streams-polyfill under the MIT license. +web-vitals under the Apache-2.0 license. +webidl-conversions under the BSD 2-Clause license +weekstart under the MIT license. +werkzeug under the BSD 3-Clause license +whatwg-encoding under the MIT license. +whatwg-mimetype under the MIT license. +whatwg-url under the MIT license. +which under the ISC license +which-boxed-primitive under the MIT license. which-builtin-type under the MIT license. -is-async-function under the MIT license. -async-function under the MIT license. -is-finalizationregistry under the MIT license. which-collection under the MIT license. -is-map under the MIT license. -is-weakmap under the MIT license. -is-weakset under the MIT license. -@pkgr/core under the MIT license. -@babel/helper-globals under the MIT license. -ejs under the Apache-2.0 license. -jake under the Apache-2.0 license. -async under the MIT license. -filelist under the Apache-2.0 license. -@types/chai under the MIT license. -@types/deep-eql under the MIT license. -@jest/get-type under the MIT license. -@jest/diff-sequences under the MIT license. -@jest/pattern under the MIT license. +which-module under the ISC license +which-typed-array under the MIT license. +why-is-node-running under the MIT license. +wide-align under the ISC license +word-wrap under the MIT license. +wordwrap under the MIT license. +wrap-ansi under the MIT license. +wrappy under the ISC license. +wrapt under the 0BSD license. +write-file-atomic under the ISC license +ws under the MIT license. +xstate under the MIT license. +xml-name-validator under the Apache-2.0 license. +xml2js under the MIT license. +xmlbuilder under the MIT license. +xmlchars under the MIT license. +xmltodict under the MIT license. +y18n under the ISC license +yallist under the ISC license +yaml under the ISC license +yargs under the MIT license. +yargs-parser under the ISC license +yarl under the Apache-2.0 license. +yn under the MIT license. +yocto-queue under the MIT license. +yoctocolors-cjs under the MIT license. +zod under the MIT license. +@asr/data-models under the Apache-2.0 license. +mkdirp under the MIT license. +jsbn under the MIT license. +read-package-json-fast under the ISC license. ******************** OPEN SOURCE LICENSES ******************** - -0BSD - https://spdx.org/licenses/0BSD.html -CC-BY-4.0 - https://spdx.org/licenses/CC-BY-4.0.html -ISC - https://spdx.org/licenses/ISC.html -MPL-2.0 - https://spdx.org/licenses/MPL-2.0.html -Python-2.0 - https://spdx.org/licenses/Python-2.0.html -Unlicense - https://spdx.org/licenses/Unlicense.html -BSD-3-Clause - https://spdx.org/licenses/BSD-3-Clause.html \ No newline at end of file +PSF-2.0 - https://spdx.org/licenses/PSF-2.0.html +0BSD - https://opensource.org/licenses/0BSD +Apache-2.0 - https://opensource.org/licenses/Apache-2.0 +BSD-2-Clause - https://opensource.org/licenses/BSD-2-Clause +BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause +BlueOak-1.0.0 - https://opensource.org/licenses/BlueOak-1.0.0 +CC-BY-4.0 - https://opensource.org/licenses/CC-BY-4.0 +CC0-1.0 - https://opensource.org/licenses/CC0-1.0 +ISC - https://opensource.org/licenses/ISC +MIT - https://opensource.org/licenses/MIT +MIT-0 - https://opensource.org/licenses/MIT-0 +MPL-2.0 - https://opensource.org/licenses/MPL-2.0 +Python-2.0 - https://opensource.org/licenses/Python-2.0 +Unlicense - https://opensource.org/licenses/Unlicense +Zlib - http://www.zlib.net/zlib_license.html +Artistic-2.0 - https://spdx.org/licenses/Artistic-2.0.html +CC-BY-3.0 - https://spdx.org/licenses/CC-BY-3.0.html +LGPL - https://spdx.org/licenses/LGPL-2.1-or-later.html \ No newline at end of file diff --git a/README.md b/README.md index 8a75778f..9339465a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Findings feature. ## Architecture Diagram -![](./docs/architecture_diagram.png) +![](./docs/automated-security-response-on-aws-architecture-diagram.png) ## Customizing the Solution @@ -44,9 +44,11 @@ or (2) adding a new playbook for a Security Standard not yet implemented in the - a Linux client with the following software - AWS CLI v2 - Python 3.11+ with pip - - AWS CDK 2.1020.1+ + - AWS CDK 2.1025.0+ - Node.js 22+ with npm - Poetry v2 with plugin to export + - Java Runtime Environment (JRE) version 17.x or newer + - [DynamoDB Local installed and setup](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html#DynamoDBLocal.DownloadingAndRunning.title) - source code downloaded from GitHub @@ -195,7 +197,12 @@ Add the playbook-specific control ID to the list of remediations in `_ ⚠️ **_IMPORTANT:_** You must create this file following the described naming convention in order to successfully build the solution. + +#### Step 5: Create the Remediation IAM Role & Integrate Remediation Runbook Each remediation has its own IAM role with custom permissions required to execute the remediation runbook. In addition, the `RunbookFactory.createRemediationRunbook` method needs to be invoked to add the remediation runbook you created in Step 1 to the solution's CloudFormation templates. In the `remediation-runook-stack.ts`, each remediation has its own code block in the `RemediationRunbookStack` class. The following code block shows the creation of a new IAM role and remediation runbook integration for the ElastiCache.2 remediation: @@ -234,7 +241,7 @@ In the `remediation-runook-stack.ts`, each remediation has its own code block in } ``` -#### Step 5: Update Unit Tests +#### Step 6: Update Unit Tests We recommend updating and running the unit tests after adding a new remediation. First, you must add any new regular expressions (that are not already added) into the `source/test/regex_registry.ts` file. @@ -340,6 +347,10 @@ export ASSET_BUCKET_NAME=$BASE_BUCKET_NAME-$REGION - In your AWS account, create two buckets with these names, e.g. `asr-staging-reference` and `asr-staging-us-east-1`. (The reference bucket will hold the CloudFormation templates, the regional bucket will hold all other assets like the lambda code bundle.) - Your buckets should be encrypted and disallow public access +> ⚠️ **_IMPORTANT:_** If you created your `*-reference` bucket in a region other than us-east-1, +> you must set the `CUSTOM_REFERENCE_BUCKET_REGION` environment variable before running the build script E.g., +> `export CUSTOM_REFERENCE_BUCKET_REGION=us-gov-east-1`. Your reference bucket policy must also give the custom resource Lambda permission to read objects. + ```bash aws s3 mb s3://$TEMPLATE_BUCKET_NAME/ @@ -368,14 +379,32 @@ export SOLUTION_VERSION=v1.0.0.mybuild #### Prerequisites +*Poetry* + In order to run the unit tests locally, you must first install and configure Poetry. Poetry is a tool used for managing dependencies and packaging within Python projects. We recommend using [pipx](https://pipx.pypa.io/stable/installation/) to install and manage Poetry. You can find other ways to install Poetry in the [Poetry installation guide](https://python-poetry.org/docs/#installation). **Note**: You must install Poetry version 2 to execute the `run-unit-tests.sh` script. Since version 2, the `export` command is no longer included by default in Poetry. To use it, you need to install the poetry-plugin-export plugin. Follow these steps to install and setup Poetry on your local machine: -- Install version 2.1.2 of Poetry by running `pipx install poetry==2.1.2` -- Set the `POETRY_HOME` environment variable to be the path to your local installation of Poetry. E.g., `POETRY_HOME=/Users/YOUR_USERNAME/.local/pipx/venvs/poetry` -- Install Poetry export plugin by running `poetry self add poetry-plugin-export@1.9.0` +1. Install version 2.1.2 of Poetry by running `pipx install poetry==2.1.2` +2. Set the `POETRY_HOME` environment variable to be the path to your local installation of Poetry. E.g., `POETRY_HOME=/Users/YOUR_USERNAME/.local/pipx/venvs/poetry` +3. Install Poetry export plugin by running `poetry self add poetry-plugin-export@1.9.0` + +*DynamoDB Local* + +The unit tests also rely on DynamoDB Local, which must be installed and setup prior to running the unit tests. DynamoDB Local is a tool used to develop and test applications without accessing the DynamoDB web service. +You can learn more about DynamoDB Local by visiting the [official AWS documentation page](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html). + +Follow these steps to install and setup DynamoDB Local: +1. Ensure you have installed Java Runtime Environment (JRE) version 17.x or newer. +2. Download DynamoDB local using the links provided in [the documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html). +3. Set the `DDB_LOCAL_HOME` environment variable to be the path to your local installation of DynamoDB Local. E.g., `DDB_LOCAL_HOME=/Users/YOUR_USERNAME/dynamodb_local_latest` +4. Configure local AWS Credentials. Downloadable DynamoDB requires any credentials to work, as shown in the following example: +``` +AWS Access Key ID: "fakeMyKeyId" +AWS Secret Access Key: "fakeSecretAccessKey" +Default Region Name: "fakeRegion" +``` ##### Run Unit Tests @@ -405,11 +434,11 @@ By default, the templates created by build-s3-dist.sh expect the software to be Upload the build artifacts from `global-s3-assets/` to the template bucket and the artifacts from `regional-s3-assets/` to the regional bucket: ```bash -aws s3 ls s3://$TEMPLATE_BUCKET_NAME # test that bucket exists - should not give an error -aws s3 ls s3://$ASSET_BUCKET_NAME # test that bucket exists - should not give an error +aws s3 ls s3://$TEMPLATE_BUCKET_NAME --region $REGION # test that bucket exists - should not give an error +aws s3 ls s3://$ASSET_BUCKET_NAME --region $REGION # test that bucket exists - should not give an error cd ./deployment -aws s3 cp global-s3-assets/ s3://$TEMPLATE_BUCKET_NAME/$SOLUTION_NAME/$SOLUTION_VERSION/ --recursive --acl bucket-owner-full-control -aws s3 cp regional-s3-assets/ s3://$ASSET_BUCKET_NAME/$SOLUTION_NAME/$SOLUTION_VERSION/ --recursive --acl bucket-owner-full-control +aws s3 cp global-s3-assets/ s3://$TEMPLATE_BUCKET_NAME/$SOLUTION_NAME/$SOLUTION_VERSION/ --recursive --acl bucket-owner-full-control --region $REGION +aws s3 cp regional-s3-assets/ s3://$ASSET_BUCKET_NAME/$SOLUTION_NAME/$SOLUTION_VERSION/ --recursive --acl bucket-owner-full-control --region $REGION ``` _✅ All assets are now staged on your S3 buckets. You or any user may use S3 links for deployments_ @@ -425,18 +454,17 @@ If you anticipate that you will need to deploy multiple times during your develo For example: ```bash - export ADMIN_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-admin.template + export ADMIN_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.$REGION.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-admin.template aws cloudformation create-stack \ --capabilities CAPABILITY_NAMED_IAM \ --stack-name ASR-Admin-$(date +%s) \ --template-url $ADMIN_TEMPLATE_URL \ + --region $REGION \ --parameters \ ParameterKey=LoadSCAdminStack,ParameterValue=yes \ ParameterKey=LoadAFSBPAdminStack,ParameterValue=no \ ParameterKey=LoadCIS120AdminStack,ParameterValue=no \ ParameterKey=LoadCIS140AdminStack,ParameterValue=no \ - ParameterKey=TargetAccountIDsStrategy,ParameterValue=INCLUDE \ - ParameterKey=TargetAccountIDs,ParameterValue=ALL \ ParameterKey=LoadCIS300AdminStack,ParameterValue=no \ ParameterKey=LoadNIST80053AdminStack,ParameterValue=no \ ParameterKey=LoadPCI321AdminStack,ParameterValue=no \ @@ -444,14 +472,18 @@ For example: ParameterKey=UseCloudWatchMetrics,ParameterValue=yes \ ParameterKey=UseCloudWatchMetricsAlarms,ParameterValue=yes \ ParameterKey=RemediationFailureAlarmThreshold,ParameterValue=5 \ - ParameterKey=EnableEnhancedCloudWatchMetrics,ParameterValue=no + ParameterKey=EnableEnhancedCloudWatchMetrics,ParameterValue=no \ + ParameterKey=ShouldDeployWebUI,ParameterValue=yes \ + ParameterKey=AdminUserEmail,ParameterValue={AdminUserEmail} \ + ParameterKey=TicketGenFunctionName,ParameterValue="" export NAMESPACE=$(date +%s | tail -c 9) - export MEMBER_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-member.template + export MEMBER_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.$REGION.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-member.template aws cloudformation create-stack \ --capabilities CAPABILITY_NAMED_IAM \ --stack-name ASR-Member-$(date +%s) \ --template-url $MEMBER_TEMPLATE_URL \ + --region $REGION \ --parameters \ ParameterKey=LoadSCMemberStack,ParameterValue=yes \ ParameterKey=LoadAFSBPMemberStack,ParameterValue=no \ @@ -463,13 +495,15 @@ For example: ParameterKey=CreateS3BucketForRedshiftAuditLogging,ParameterValue=no \ ParameterKey=LogGroupName,ParameterValue=random-log-group-123456789012 \ ParameterKey=Namespace,ParameterValue=$NAMESPACE \ - ParameterKey=SecHubAdminAccount,ParameterValue={SecHubAdminAccount} + ParameterKey=SecHubAdminAccount,ParameterValue={SecHubAdminAccount} \ + ParameterKey=EnableCloudTrailForASRActionLog,ParameterValue=no - export MEMBER_ROLES_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-member-roles.template + export MEMBER_ROLES_TEMPLATE_URL=https://$TEMPLATE_BUCKET_NAME.s3.$REGION.amazonaws.com/$SOLUTION_NAME/$SOLUTION_VERSION/automated-security-response-member-roles.template aws cloudformation create-stack \ --capabilities CAPABILITY_NAMED_IAM \ --stack-name ASR-Member-Roles-$(date +%s) \ --template-url $MEMBER_ROLES_TEMPLATE_URL \ + --region $REGION \ --parameters \ ParameterKey=Namespace,ParameterValue=$NAMESPACE \ ParameterKey=SecHubAdminAccount,ParameterValue={SecHubAdminAccount} @@ -509,11 +543,9 @@ For example: |-test/ [ CDK and SSM document unit tests ] -## Collection of operational metrics +## Data Collection -This solution collects anonymized operational metrics to help AWS improve the quality of features of the solution. For -more information, including how to disable this capability, please see the [Implementation -Guide](https://docs.aws.amazon.com/solutions/latest/automated-security-response-on-aws/collection-of-operational-metrics.html) +This solution sends operational metrics to AWS (the “Data”) about the use of this solution. We use this Data to better understand how customers use this solution and related services and products. AWS’s collection of this Data is subject to the [AWS Privacy Notice](https://aws.amazon.com/privacy/). ## License diff --git a/deployment/build-open-source-dist.sh b/deployment/build-open-source-dist.sh index 02dd0f2b..cd9dd5f4 100755 --- a/deployment/build-open-source-dist.sh +++ b/deployment/build-open-source-dist.sh @@ -33,7 +33,13 @@ main() { -x "codescan-*.sh" \ -x "Config" \ -x ".nightswatch/*" \ - -x "build-tools/*" + -x "buildspec.yml" \ + -x "AWSSD-README.md" \ + -x "AWSSD-DevNotes.md" \ + -x "build-tools/*" \ + -x "redpencil-suppressions.json" \ + -x "source/data-models/cjs/*" \ + -x "source/data-models/esm/*" \ popd } diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index 991e4134..0720cbe4 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash +# # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +# [[ $DEBUG ]] && set -x set -eu -o pipefail @@ -41,11 +43,11 @@ clean() { # - version-code: version of the package main() { local root_dir=$(dirname "$(cd -P -- "$(dirname "$0")" && pwd -P)") - local template_dir="$root_dir"/deployment - local template_dist_dir="$template_dir"/global-s3-assets - local build_dist_dir="$template_dir/"regional-s3-assets + local deployment_dir="$root_dir"/deployment + local template_dist_dir="$deployment_dir"/global-s3-assets + local build_dist_dir="$deployment_dir/"regional-s3-assets local source_dir="$root_dir"/source - local temp_work_dir="${template_dir}"/temp + local temp_work_dir="${deployment_dir}"/temp local devtest="" local clean_dirs=("$template_dist_dir" "$build_dist_dir" "$temp_work_dir") @@ -73,14 +75,14 @@ main() { clean "${clean_dirs[@]}" # Save in environmental variables to simplify builds (?) - echo "export DIST_OUTPUT_BUCKET=$bucket" > "$template_dir"/setenv.sh - echo "export DIST_VERSION=$version" >> "$template_dir"/setenv.sh + echo "export DIST_OUTPUT_BUCKET=$bucket" > "$deployment_dir"/setenv.sh + echo "export DIST_VERSION=$version" >> "$deployment_dir"/setenv.sh - if [[ ! -e "$template_dir"/solution_env.sh ]]; then + if [[ ! -e "$deployment_dir"/solution_env.sh ]]; then echo "solution_env.sh is missing from the solution root." && exit 1 fi - source "$template_dir"/solution_env.sh + source "$deployment_dir"/solution_env.sh if [[ -z "$SOLUTION_ID" ]] || [[ -z "$SOLUTION_NAME" ]] || [[ -z "$SOLUTION_TRADEMARKEDNAME" ]]; then echo "Missing one of SOLUTION_ID, SOLUTION_NAME, or SOLUTION_TRADEMARKEDNAME from solution_env.sh" && exit 1 @@ -93,9 +95,19 @@ main() { export SOLUTION_NAME export SOLUTION_TRADEMARKEDNAME + # You must set BUILD_ENV=development if you wish to run the frontend locally + if [[ "${BUILD_ENV:-}" != "development" ]]; then + echo -e "\033[1;33m===============================================================================\033[0m" + echo -e "\033[1;33m⚠️ WARNING: BUILD_ENV is not set to 'development'. Localhost URLs will not be included in Cognito UserPoolClient configuration.\033[0m" + echo -e "\033[1;33mTo include localhost URLs for development, run: BUILD_ENV=development $0 $*\033[0m" + echo -e "\033[1;33m===============================================================================\033[0m" + echo "" + sleep 2 + fi + echo "export DIST_SOLUTION_NAME=$SOLUTION_TRADEMARKEDNAME" >> ./setenv.sh - source "$template_dir"/setenv.sh + source "$deployment_dir"/setenv.sh header "Building $SOLUTION_NAME ($SOLUTION_ID) version $version for bucket $bucket" @@ -125,7 +137,7 @@ main() { mkdir -p "$temp_work_dir"/source/solution_deploy/lambdalayer/python/layer mkdir -p "$temp_work_dir"/source/solution_deploy/lambdalayer/python/lib/python3.11/site-packages cp "$source_dir"/layer/*.py "$temp_work_dir"/source/solution_deploy/lambdalayer/python/layer - pip install -r "$template_dir"/requirements.txt -t "$temp_work_dir"/source/solution_deploy/lambdalayer/python/lib/python3.11/site-packages + pip install -r "$deployment_dir"/requirements.txt -t "$temp_work_dir"/source/solution_deploy/lambdalayer/python/lib/python3.11/site-packages popd pushd "$temp_work_dir"/source/solution_deploy/lambdalayer @@ -144,6 +156,12 @@ main() { zip -q ${build_dist_dir}/lambda/deployment_metrics_custom_resource.zip deployment_metrics_custom_resource.py cfnresponse.py popd + header "[Pack] Remediation Configuration Custom Action Lambda" + + pushd "$source_dir"/solution_deploy/source + zip -q ${build_dist_dir}/lambda/remediation_config_provider.zip remediation_config_provider.py cfnresponse.py + popd + header "[Pack] Wait Provider Lambda" @@ -160,6 +178,17 @@ main() { done popd + header "[Build] Data-models" + pushd "$source_dir"/data-models + npm run clean && npm install && npm run build + popd + + header "[Pack] Non-Orchestrator Lambdas" + pushd "$source_dir"/lambdas + npm run build:clean && npm run build:install && npm run build:ts + zip -r -q "$build_dist_dir"/lambda/asr_lambdas.zip . -x "__tests__/*" "*.ts" "**/*.ts" "**/jest.config.js" + popd + header "[Pack] Blueprint Lambdas" pushd "$source_dir"/blueprints @@ -179,6 +208,7 @@ main() { done popd + # Blueprint lambdas dependency layer pushd "$build_dist_dir"/lambda/blueprints mkdir -p "$build_dist_dir"/lambda/blueprints/python "$POETRY_COMMAND" export --without dev -f requirements.txt --output requirements.txt --without-hashes @@ -187,6 +217,42 @@ main() { rm -r python popd + + header "Run UI Builds" + + cd "$source_dir/webui/" || exit 1 + npm install + GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false npm run build + + if [ $? -eq 0 ] + then + header "UI build succeeded" + else + header "UI build FAILED" + exit 1 + fi + mkdir -p "$build_dist_dir"/webui/ + cp -r ./dist/* "$build_dist_dir"/webui/ + + + header "Generate webui manifest file (webui-manifest.json)" + # Build webui-manifest.json so that it can be deployed with the ui code afterwards + # + # Details: The deployWebui custom resource needs this list in order to copy + # files from $build_dist_dir/webui to the CloudFront S3 bucket. + # Since the manifest file is computed during build time, the custom resource + # can use that to figure out what files to copy instead of doing a list bucket operation, + # which would require ListBucket permission. + # Furthermore, the S3 bucket used to host AWS solutions disallows ListBucket + # access, so the only way to copy the webui files from that bucket from + # to CloudFront S3 bucket is to use a manifest file. + + cd $deployment_dir/manifest-generator + [ -e node_modules ] && rm -rf node_modules + npm ci + node app.js --target "$build_dist_dir/webui" --output webui-manifest.json + mv webui-manifest.json $build_dist_dir/webui/webui-manifest.json + header "[Create] Playbooks" for playbook in $(ls "$source_dir"/playbooks); do @@ -233,7 +299,7 @@ main() { done popd - [ -e "$template_dir"/*.template ] && cp "$template_dir"/*.template "$template_dist_dir"/ + [ -e "$deployment_dir"/*.template ] && cp "$deployment_dir"/*.template "$template_dist_dir"/ mv "$template_dist_dir"/SolutionDeployStack.template "$template_dist_dir"/automated-security-response-admin.template mv "$template_dist_dir"/MemberStack.template "$template_dist_dir"/automated-security-response-member.template @@ -241,8 +307,11 @@ main() { mv "$template_dist_dir"/RunbookStack.template "$template_dist_dir"/automated-security-response-remediation-runbooks.template mv "$template_dist_dir"/OrchestratorLogStack.template "$template_dist_dir"/automated-security-response-orchestrator-log.template mv "$template_dist_dir"/MemberRolesStack.template "$template_dist_dir"/automated-security-response-member-roles.template - + mv "$template_dist_dir"/SolutionDeployStackWebUINestedStack*.template "$template_dist_dir"/automated-security-response-webui-nested-stack.template rm "$template_dist_dir"/*.nested.template + + header "[Create] List of Supported Control Ids (supported-controls.json)" + node "$deployment_dir"/utils/generate-controls-list.js "$version" } main "$@" diff --git a/deployment/manifest-generator/app.js b/deployment/manifest-generator/app.js new file mode 100644 index 00000000..f8faf886 --- /dev/null +++ b/deployment/manifest-generator/app.js @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const args = require('minimist')(process.argv.slice(2)); + +// List all files in a directory and subdirectories recursively +let listChildrenRecursively = function(file) { + if (!fs.statSync(file).isDirectory()) + return [file]; + + let children = fs.readdirSync(file); + return children.flatMap(child => listChildrenRecursively(path.join(file, child))); +}; + +function validateArgs(argumentList) { + if (!argumentList.hasOwnProperty('target')) { + console.log( + '--target parameter missing. This should be the target directory containing content for the manifest.' + ); + process.exit(1); + } + + if (!argumentList.hasOwnProperty('output')) { + console.log( + '--ouput parameter missing. This should be the out directory where the manifest file will be generated.' + ); + process.exit(1); + } +} + +function generateManifestFile(sourceDir) { + console.log( + `Generating a manifest file ${args.output} for directory ${sourceDir}` + ); + + const filelist = listChildrenRecursively(sourceDir); + + return { + files: filelist.map(it => it.replace(`${sourceDir}/`, '')) + }; +} + +validateArgs(args); + +const webUiDir = args.target; +const _manifest = generateManifestFile(webUiDir); + +fs.writeFileSync(args.output, JSON.stringify(_manifest, null, 4)); +console.log(`Manifest file ${args.output} generated.`); diff --git a/deployment/manifest-generator/package-lock.json b/deployment/manifest-generator/package-lock.json new file mode 100644 index 00000000..795337af --- /dev/null +++ b/deployment/manifest-generator/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "manifest-generator", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "manifest-generator", + "version": "0.0.0", + "dependencies": { + "minimist": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + } + }, + "dependencies": { + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + } + } +} diff --git a/deployment/manifest-generator/package.json b/deployment/manifest-generator/package.json new file mode 100644 index 00000000..ffbd51b1 --- /dev/null +++ b/deployment/manifest-generator/package.json @@ -0,0 +1,13 @@ +{ + "name": "manifest-generator", + "version": "0.0.0", + "private": true, + "description": "Create a manifest.json that lists all files to include in a WebUI deployment", + "main": "app.js", + "author": { + "name": "aws-solutions-builder" + }, + "dependencies": { + "minimist": "*" + } +} diff --git a/deployment/poetry.lock b/deployment/poetry.lock index 291a4710..d6af2b9d 100644 --- a/deployment/poetry.lock +++ b/deployment/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -34,14 +34,14 @@ tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" a [[package]] name = "aws-encryption-sdk" -version = "4.0.1" +version = "3.3.0" description = "AWS Encryption SDK implementation for Python" optional = false python-versions = "*" groups = ["main", "dev"] files = [ - {file = "aws-encryption-sdk-4.0.1.tar.gz", hash = "sha256:7320dc4cf8d8d5a9b4c88a343be93835da18756e05308d3536554be0ca2889a5"}, - {file = "aws_encryption_sdk-4.0.1-py2.py3-none-any.whl", hash = "sha256:5c2ca9a207e1732542a1370ac7efd630ab6e04d05f98e68badf20927eb95ed1d"}, + {file = "aws-encryption-sdk-3.3.0.tar.gz", hash = "sha256:eb2adba14f481cd83d7169ab8e642994896d39a4a64e1796904a6b49256613b0"}, + {file = "aws_encryption_sdk-3.3.0-py2.py3-none-any.whl", hash = "sha256:c2a967ebe70820f64dea1eb7000f60fe54f56b23276a592e1b77ec475e823304"}, ] [package.dependencies] @@ -50,9 +50,6 @@ boto3 = ">=1.10.0" cryptography = ">=3.4.6" wrapt = ">=1.10.11" -[package.extras] -mpl = ["aws-cryptographic-material-providers (>=1.7.4,<=1.10.0)"] - [[package]] name = "aws-lambda-context" version = "1.1.0" @@ -70,36 +67,34 @@ tests = ["black (==19.3b0)", "bump2version (==0.5.10)", "flake8 (==3.7.8)", "iso [[package]] name = "aws-lambda-powertools" -version = "3.14.0" +version = "3.1.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false -python-versions = "<4.0.0,>=3.9" +python-versions = "<4.0.0,>=3.8" groups = ["main", "dev"] files = [ - {file = "aws_lambda_powertools-3.14.0-py3-none-any.whl", hash = "sha256:4a36dbf44b4e0648c5e0e097ecbde974f18524c3096f50ee0f1ce7dcf0d64ef0"}, - {file = "aws_lambda_powertools-3.14.0.tar.gz", hash = "sha256:11e4d8a2c7855d1f3109a15c4ed6250465ae5e2e2762b42b99b67352cf246f1a"}, + {file = "aws_lambda_powertools-3.1.0-py3-none-any.whl", hash = "sha256:fdc834678d131e230052ccd684f969be417ce0165d65ee35c053e1a966e46e4c"}, + {file = "aws_lambda_powertools-3.1.0.tar.gz", hash = "sha256:758a8e5d668ae759051d064d542decff777d9c7a0a5612f0c05ab78fb6f20365"}, ] [package.dependencies] -aws-encryption-sdk = {version = ">=3.1.1,<5.0.0", optional = true, markers = "extra == \"all\" or extra == \"datamasking\""} +aws-encryption-sdk = {version = ">=3.1.1,<4.0.0", optional = true, markers = "extra == \"all\" or extra == \"datamasking\""} aws-xray-sdk = {version = ">=2.8.0,<3.0.0", optional = true, markers = "extra == \"tracer\" or extra == \"all\""} fastjsonschema = {version = ">=2.14.5,<3.0.0", optional = true, markers = "extra == \"validation\" or extra == \"all\""} jmespath = ">=1.0.1,<2.0.0" jsonpath-ng = {version = ">=1.6.0,<2.0.0", optional = true, markers = "extra == \"all\" or extra == \"datamasking\""} -pydantic = {version = ">=2.4.0,<3.0.0", optional = true, markers = "extra == \"parser\" or extra == \"all\""} -pydantic-settings = {version = ">=2.6.1,<3.0.0", optional = true, markers = "extra == \"all\""} +pydantic = {version = ">=2.0.3,<3.0.0", optional = true, markers = "extra == \"parser\" or extra == \"all\""} typing-extensions = ">=4.11.0,<5.0.0" [package.extras] -all = ["aws-encryption-sdk (>=3.1.1,<5.0.0)", "aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)", "pydantic (>=2.4.0,<3.0.0)", "pydantic-settings (>=2.6.1,<3.0.0)"] +all = ["aws-encryption-sdk (>=3.1.1,<4.0.0)", "aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)", "pydantic (>=2.0.3,<3.0.0)"] aws-sdk = ["boto3 (>=1.34.32,<2.0.0)"] -datadog = ["datadog-lambda (>=6.106.0,<7.0.0)"] -datamasking = ["aws-encryption-sdk (>=3.1.1,<5.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)"] -parser = ["pydantic (>=2.4.0,<3.0.0)"] -redis = ["redis (>=4.4,<7.0)"] +datadog = ["datadog-lambda (>=4.77,<7.0)"] +datamasking = ["aws-encryption-sdk (>=3.1.1,<4.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)"] +parser = ["pydantic (>=2.0.3,<3.0.0)"] +redis = ["redis (>=4.4,<6.0)"] tracer = ["aws-xray-sdk (>=2.8.0,<3.0.0)"] validation = ["fastjsonschema (>=2.14.5,<3.0.0)"] -valkey = ["valkey-glide (>=1.3.5,<2.0)"] [[package]] name = "aws-xray-sdk" @@ -164,470 +159,479 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.38.34" +version = "1.40.39" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "boto3-1.38.34-py3-none-any.whl", hash = "sha256:7d9409be63a11c1684427a9b06d6820ec72785cb275b56affe437f3709a80eb3"}, - {file = "boto3-1.38.34.tar.gz", hash = "sha256:25e76b9fec8db8e21adaf84df0de5c58fa779be121bc327e07e920c7c0870394"}, + {file = "boto3-1.40.39-py3-none-any.whl", hash = "sha256:e2cab5606269fe9f428981892aa592b7e0c087a038774475fa4cd6c8b5fe0a99"}, + {file = "boto3-1.40.39.tar.gz", hash = "sha256:27ca06d4d6f838b056b4935c9eceb92c8d125dbe0e895c5583bcf7130627dcd2"}, ] [package.dependencies] -botocore = ">=1.38.34,<1.39.0" +botocore = ">=1.40.39,<1.41.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.13.0,<0.14.0" +s3transfer = ">=0.14.0,<0.15.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs-lite" -version = "1.38.34" -description = "Lite type annotations for boto3 1.38.34 generated with mypy-boto3-builder 8.11.0" +version = "1.40.8" +description = "Lite type annotations for boto3 1.40.8 generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "boto3_stubs_lite-1.38.34-py3-none-any.whl", hash = "sha256:2cd9cd358874f8995aafdc2e64dde3e1ef7a84000f251dc6c199b895a3bafc57"}, - {file = "boto3_stubs_lite-1.38.34.tar.gz", hash = "sha256:acb46cef191268ea3242c73131d79a53b6b863d1e8921131745e8a0eade43340"}, + {file = "boto3_stubs_lite-1.40.8-py3-none-any.whl", hash = "sha256:915ce305f3ed030b0eb2f23d377ab807854ace6d9a5827f3e48d34f363c353c4"}, + {file = "boto3_stubs_lite-1.40.8.tar.gz", hash = "sha256:d5192aff15e3f74b2fcf40c1a49f029ac8b3694f32ae0d2319df7c085c918f79"}, ] [package.dependencies] botocore-stubs = "*" -mypy-boto3-cloudformation = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"cloudformation\""} -mypy-boto3-cloudfront = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"cloudfront\""} -mypy-boto3-cloudwatch = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"cloudwatch\""} -mypy-boto3-ec2 = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"ec2\""} -mypy-boto3-iam = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"iam\""} -mypy-boto3-s3 = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"s3\""} -mypy-boto3-sns = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"sns\""} -mypy-boto3-ssm = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"ssm\""} -mypy-boto3-sts = {version = ">=1.38.0,<1.39.0", optional = true, markers = "extra == \"sts\""} +mypy-boto3-cloudformation = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"cloudformation\""} +mypy-boto3-cloudfront = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"cloudfront\""} +mypy-boto3-cloudwatch = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"cloudwatch\""} +mypy-boto3-ec2 = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"ec2\""} +mypy-boto3-iam = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"iam\""} +mypy-boto3-s3 = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"s3\""} +mypy-boto3-sns = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"sns\""} +mypy-boto3-ssm = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"ssm\""} +mypy-boto3-sts = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"sts\""} types-s3transfer = "*" typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} [package.extras] -accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.38.0,<1.39.0)"] -account = ["mypy-boto3-account (>=1.38.0,<1.39.0)"] -acm = ["mypy-boto3-acm (>=1.38.0,<1.39.0)"] -acm-pca = ["mypy-boto3-acm-pca (>=1.38.0,<1.39.0)"] -all = ["mypy-boto3-accessanalyzer (>=1.38.0,<1.39.0)", "mypy-boto3-account (>=1.38.0,<1.39.0)", "mypy-boto3-acm (>=1.38.0,<1.39.0)", "mypy-boto3-acm-pca (>=1.38.0,<1.39.0)", "mypy-boto3-amp (>=1.38.0,<1.39.0)", "mypy-boto3-amplify (>=1.38.0,<1.39.0)", "mypy-boto3-amplifybackend (>=1.38.0,<1.39.0)", "mypy-boto3-amplifyuibuilder (>=1.38.0,<1.39.0)", "mypy-boto3-apigateway (>=1.38.0,<1.39.0)", "mypy-boto3-apigatewaymanagementapi (>=1.38.0,<1.39.0)", "mypy-boto3-apigatewayv2 (>=1.38.0,<1.39.0)", "mypy-boto3-appconfig (>=1.38.0,<1.39.0)", "mypy-boto3-appconfigdata (>=1.38.0,<1.39.0)", "mypy-boto3-appfabric (>=1.38.0,<1.39.0)", "mypy-boto3-appflow (>=1.38.0,<1.39.0)", "mypy-boto3-appintegrations (>=1.38.0,<1.39.0)", "mypy-boto3-application-autoscaling (>=1.38.0,<1.39.0)", "mypy-boto3-application-insights (>=1.38.0,<1.39.0)", "mypy-boto3-application-signals (>=1.38.0,<1.39.0)", "mypy-boto3-applicationcostprofiler (>=1.38.0,<1.39.0)", "mypy-boto3-appmesh (>=1.38.0,<1.39.0)", "mypy-boto3-apprunner (>=1.38.0,<1.39.0)", "mypy-boto3-appstream (>=1.38.0,<1.39.0)", "mypy-boto3-appsync (>=1.38.0,<1.39.0)", "mypy-boto3-apptest (>=1.38.0,<1.39.0)", "mypy-boto3-arc-zonal-shift (>=1.38.0,<1.39.0)", "mypy-boto3-artifact (>=1.38.0,<1.39.0)", "mypy-boto3-athena (>=1.38.0,<1.39.0)", "mypy-boto3-auditmanager (>=1.38.0,<1.39.0)", "mypy-boto3-autoscaling (>=1.38.0,<1.39.0)", "mypy-boto3-autoscaling-plans (>=1.38.0,<1.39.0)", "mypy-boto3-b2bi (>=1.38.0,<1.39.0)", "mypy-boto3-backup (>=1.38.0,<1.39.0)", "mypy-boto3-backup-gateway (>=1.38.0,<1.39.0)", "mypy-boto3-backupsearch (>=1.38.0,<1.39.0)", "mypy-boto3-batch (>=1.38.0,<1.39.0)", "mypy-boto3-bcm-data-exports (>=1.38.0,<1.39.0)", "mypy-boto3-bcm-pricing-calculator (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-agent (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-agent-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-data-automation (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-billing (>=1.38.0,<1.39.0)", "mypy-boto3-billingconductor (>=1.38.0,<1.39.0)", "mypy-boto3-braket (>=1.38.0,<1.39.0)", "mypy-boto3-budgets (>=1.38.0,<1.39.0)", "mypy-boto3-ce (>=1.38.0,<1.39.0)", "mypy-boto3-chatbot (>=1.38.0,<1.39.0)", "mypy-boto3-chime (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-identity (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-meetings (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-messaging (>=1.38.0,<1.39.0)", "mypy-boto3-chime-sdk-voice (>=1.38.0,<1.39.0)", "mypy-boto3-cleanrooms (>=1.38.0,<1.39.0)", "mypy-boto3-cleanroomsml (>=1.38.0,<1.39.0)", "mypy-boto3-cloud9 (>=1.38.0,<1.39.0)", "mypy-boto3-cloudcontrol (>=1.38.0,<1.39.0)", "mypy-boto3-clouddirectory (>=1.38.0,<1.39.0)", "mypy-boto3-cloudformation (>=1.38.0,<1.39.0)", "mypy-boto3-cloudfront (>=1.38.0,<1.39.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.38.0,<1.39.0)", "mypy-boto3-cloudhsm (>=1.38.0,<1.39.0)", "mypy-boto3-cloudhsmv2 (>=1.38.0,<1.39.0)", "mypy-boto3-cloudsearch (>=1.38.0,<1.39.0)", "mypy-boto3-cloudsearchdomain (>=1.38.0,<1.39.0)", "mypy-boto3-cloudtrail (>=1.38.0,<1.39.0)", "mypy-boto3-cloudtrail-data (>=1.38.0,<1.39.0)", "mypy-boto3-cloudwatch (>=1.38.0,<1.39.0)", "mypy-boto3-codeartifact (>=1.38.0,<1.39.0)", "mypy-boto3-codebuild (>=1.38.0,<1.39.0)", "mypy-boto3-codecatalyst (>=1.38.0,<1.39.0)", "mypy-boto3-codecommit (>=1.38.0,<1.39.0)", "mypy-boto3-codeconnections (>=1.38.0,<1.39.0)", "mypy-boto3-codedeploy (>=1.38.0,<1.39.0)", "mypy-boto3-codeguru-reviewer (>=1.38.0,<1.39.0)", "mypy-boto3-codeguru-security (>=1.38.0,<1.39.0)", "mypy-boto3-codeguruprofiler (>=1.38.0,<1.39.0)", "mypy-boto3-codepipeline (>=1.38.0,<1.39.0)", "mypy-boto3-codestar-connections (>=1.38.0,<1.39.0)", "mypy-boto3-codestar-notifications (>=1.38.0,<1.39.0)", "mypy-boto3-cognito-identity (>=1.38.0,<1.39.0)", "mypy-boto3-cognito-idp (>=1.38.0,<1.39.0)", "mypy-boto3-cognito-sync (>=1.38.0,<1.39.0)", "mypy-boto3-comprehend (>=1.38.0,<1.39.0)", "mypy-boto3-comprehendmedical (>=1.38.0,<1.39.0)", "mypy-boto3-compute-optimizer (>=1.38.0,<1.39.0)", "mypy-boto3-config (>=1.38.0,<1.39.0)", "mypy-boto3-connect (>=1.38.0,<1.39.0)", "mypy-boto3-connect-contact-lens (>=1.38.0,<1.39.0)", "mypy-boto3-connectcampaigns (>=1.38.0,<1.39.0)", "mypy-boto3-connectcampaignsv2 (>=1.38.0,<1.39.0)", "mypy-boto3-connectcases (>=1.38.0,<1.39.0)", "mypy-boto3-connectparticipant (>=1.38.0,<1.39.0)", "mypy-boto3-controlcatalog (>=1.38.0,<1.39.0)", "mypy-boto3-controltower (>=1.38.0,<1.39.0)", "mypy-boto3-cost-optimization-hub (>=1.38.0,<1.39.0)", "mypy-boto3-cur (>=1.38.0,<1.39.0)", "mypy-boto3-customer-profiles (>=1.38.0,<1.39.0)", "mypy-boto3-databrew (>=1.38.0,<1.39.0)", "mypy-boto3-dataexchange (>=1.38.0,<1.39.0)", "mypy-boto3-datapipeline (>=1.38.0,<1.39.0)", "mypy-boto3-datasync (>=1.38.0,<1.39.0)", "mypy-boto3-datazone (>=1.38.0,<1.39.0)", "mypy-boto3-dax (>=1.38.0,<1.39.0)", "mypy-boto3-deadline (>=1.38.0,<1.39.0)", "mypy-boto3-detective (>=1.38.0,<1.39.0)", "mypy-boto3-devicefarm (>=1.38.0,<1.39.0)", "mypy-boto3-devops-guru (>=1.38.0,<1.39.0)", "mypy-boto3-directconnect (>=1.38.0,<1.39.0)", "mypy-boto3-discovery (>=1.38.0,<1.39.0)", "mypy-boto3-dlm (>=1.38.0,<1.39.0)", "mypy-boto3-dms (>=1.38.0,<1.39.0)", "mypy-boto3-docdb (>=1.38.0,<1.39.0)", "mypy-boto3-docdb-elastic (>=1.38.0,<1.39.0)", "mypy-boto3-drs (>=1.38.0,<1.39.0)", "mypy-boto3-ds (>=1.38.0,<1.39.0)", "mypy-boto3-ds-data (>=1.38.0,<1.39.0)", "mypy-boto3-dsql (>=1.38.0,<1.39.0)", "mypy-boto3-dynamodb (>=1.38.0,<1.39.0)", "mypy-boto3-dynamodbstreams (>=1.38.0,<1.39.0)", "mypy-boto3-ebs (>=1.38.0,<1.39.0)", "mypy-boto3-ec2 (>=1.38.0,<1.39.0)", "mypy-boto3-ec2-instance-connect (>=1.38.0,<1.39.0)", "mypy-boto3-ecr (>=1.38.0,<1.39.0)", "mypy-boto3-ecr-public (>=1.38.0,<1.39.0)", "mypy-boto3-ecs (>=1.38.0,<1.39.0)", "mypy-boto3-efs (>=1.38.0,<1.39.0)", "mypy-boto3-eks (>=1.38.0,<1.39.0)", "mypy-boto3-eks-auth (>=1.38.0,<1.39.0)", "mypy-boto3-elasticache (>=1.38.0,<1.39.0)", "mypy-boto3-elasticbeanstalk (>=1.38.0,<1.39.0)", "mypy-boto3-elastictranscoder (>=1.38.0,<1.39.0)", "mypy-boto3-elb (>=1.38.0,<1.39.0)", "mypy-boto3-elbv2 (>=1.38.0,<1.39.0)", "mypy-boto3-emr (>=1.38.0,<1.39.0)", "mypy-boto3-emr-containers (>=1.38.0,<1.39.0)", "mypy-boto3-emr-serverless (>=1.38.0,<1.39.0)", "mypy-boto3-entityresolution (>=1.38.0,<1.39.0)", "mypy-boto3-es (>=1.38.0,<1.39.0)", "mypy-boto3-events (>=1.38.0,<1.39.0)", "mypy-boto3-evidently (>=1.38.0,<1.39.0)", "mypy-boto3-evs (>=1.38.0,<1.39.0)", "mypy-boto3-finspace (>=1.38.0,<1.39.0)", "mypy-boto3-finspace-data (>=1.38.0,<1.39.0)", "mypy-boto3-firehose (>=1.38.0,<1.39.0)", "mypy-boto3-fis (>=1.38.0,<1.39.0)", "mypy-boto3-fms (>=1.38.0,<1.39.0)", "mypy-boto3-forecast (>=1.38.0,<1.39.0)", "mypy-boto3-forecastquery (>=1.38.0,<1.39.0)", "mypy-boto3-frauddetector (>=1.38.0,<1.39.0)", "mypy-boto3-freetier (>=1.38.0,<1.39.0)", "mypy-boto3-fsx (>=1.38.0,<1.39.0)", "mypy-boto3-gamelift (>=1.38.0,<1.39.0)", "mypy-boto3-gameliftstreams (>=1.38.0,<1.39.0)", "mypy-boto3-geo-maps (>=1.38.0,<1.39.0)", "mypy-boto3-geo-places (>=1.38.0,<1.39.0)", "mypy-boto3-geo-routes (>=1.38.0,<1.39.0)", "mypy-boto3-glacier (>=1.38.0,<1.39.0)", "mypy-boto3-globalaccelerator (>=1.38.0,<1.39.0)", "mypy-boto3-glue (>=1.38.0,<1.39.0)", "mypy-boto3-grafana (>=1.38.0,<1.39.0)", "mypy-boto3-greengrass (>=1.38.0,<1.39.0)", "mypy-boto3-greengrassv2 (>=1.38.0,<1.39.0)", "mypy-boto3-groundstation (>=1.38.0,<1.39.0)", "mypy-boto3-guardduty (>=1.38.0,<1.39.0)", "mypy-boto3-health (>=1.38.0,<1.39.0)", "mypy-boto3-healthlake (>=1.38.0,<1.39.0)", "mypy-boto3-iam (>=1.38.0,<1.39.0)", "mypy-boto3-identitystore (>=1.38.0,<1.39.0)", "mypy-boto3-imagebuilder (>=1.38.0,<1.39.0)", "mypy-boto3-importexport (>=1.38.0,<1.39.0)", "mypy-boto3-inspector (>=1.38.0,<1.39.0)", "mypy-boto3-inspector-scan (>=1.38.0,<1.39.0)", "mypy-boto3-inspector2 (>=1.38.0,<1.39.0)", "mypy-boto3-internetmonitor (>=1.38.0,<1.39.0)", "mypy-boto3-invoicing (>=1.38.0,<1.39.0)", "mypy-boto3-iot (>=1.38.0,<1.39.0)", "mypy-boto3-iot-data (>=1.38.0,<1.39.0)", "mypy-boto3-iot-jobs-data (>=1.38.0,<1.39.0)", "mypy-boto3-iot-managed-integrations (>=1.38.0,<1.39.0)", "mypy-boto3-iotanalytics (>=1.38.0,<1.39.0)", "mypy-boto3-iotdeviceadvisor (>=1.38.0,<1.39.0)", "mypy-boto3-iotevents (>=1.38.0,<1.39.0)", "mypy-boto3-iotevents-data (>=1.38.0,<1.39.0)", "mypy-boto3-iotfleethub (>=1.38.0,<1.39.0)", "mypy-boto3-iotfleetwise (>=1.38.0,<1.39.0)", "mypy-boto3-iotsecuretunneling (>=1.38.0,<1.39.0)", "mypy-boto3-iotsitewise (>=1.38.0,<1.39.0)", "mypy-boto3-iotthingsgraph (>=1.38.0,<1.39.0)", "mypy-boto3-iottwinmaker (>=1.38.0,<1.39.0)", "mypy-boto3-iotwireless (>=1.38.0,<1.39.0)", "mypy-boto3-ivs (>=1.38.0,<1.39.0)", "mypy-boto3-ivs-realtime (>=1.38.0,<1.39.0)", "mypy-boto3-ivschat (>=1.38.0,<1.39.0)", "mypy-boto3-kafka (>=1.38.0,<1.39.0)", "mypy-boto3-kafkaconnect (>=1.38.0,<1.39.0)", "mypy-boto3-kendra (>=1.38.0,<1.39.0)", "mypy-boto3-kendra-ranking (>=1.38.0,<1.39.0)", "mypy-boto3-keyspaces (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-archived-media (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-media (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-signaling (>=1.38.0,<1.39.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.38.0,<1.39.0)", "mypy-boto3-kinesisanalytics (>=1.38.0,<1.39.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.38.0,<1.39.0)", "mypy-boto3-kinesisvideo (>=1.38.0,<1.39.0)", "mypy-boto3-kms (>=1.38.0,<1.39.0)", "mypy-boto3-lakeformation (>=1.38.0,<1.39.0)", "mypy-boto3-lambda (>=1.38.0,<1.39.0)", "mypy-boto3-launch-wizard (>=1.38.0,<1.39.0)", "mypy-boto3-lex-models (>=1.38.0,<1.39.0)", "mypy-boto3-lex-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-lexv2-models (>=1.38.0,<1.39.0)", "mypy-boto3-lexv2-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-license-manager (>=1.38.0,<1.39.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.38.0,<1.39.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.38.0,<1.39.0)", "mypy-boto3-lightsail (>=1.38.0,<1.39.0)", "mypy-boto3-location (>=1.38.0,<1.39.0)", "mypy-boto3-logs (>=1.38.0,<1.39.0)", "mypy-boto3-lookoutequipment (>=1.38.0,<1.39.0)", "mypy-boto3-lookoutmetrics (>=1.38.0,<1.39.0)", "mypy-boto3-lookoutvision (>=1.38.0,<1.39.0)", "mypy-boto3-m2 (>=1.38.0,<1.39.0)", "mypy-boto3-machinelearning (>=1.38.0,<1.39.0)", "mypy-boto3-macie2 (>=1.38.0,<1.39.0)", "mypy-boto3-mailmanager (>=1.38.0,<1.39.0)", "mypy-boto3-managedblockchain (>=1.38.0,<1.39.0)", "mypy-boto3-managedblockchain-query (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-agreement (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-catalog (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-deployment (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-entitlement (>=1.38.0,<1.39.0)", "mypy-boto3-marketplace-reporting (>=1.38.0,<1.39.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.38.0,<1.39.0)", "mypy-boto3-mediaconnect (>=1.38.0,<1.39.0)", "mypy-boto3-mediaconvert (>=1.38.0,<1.39.0)", "mypy-boto3-medialive (>=1.38.0,<1.39.0)", "mypy-boto3-mediapackage (>=1.38.0,<1.39.0)", "mypy-boto3-mediapackage-vod (>=1.38.0,<1.39.0)", "mypy-boto3-mediapackagev2 (>=1.38.0,<1.39.0)", "mypy-boto3-mediastore (>=1.38.0,<1.39.0)", "mypy-boto3-mediastore-data (>=1.38.0,<1.39.0)", "mypy-boto3-mediatailor (>=1.38.0,<1.39.0)", "mypy-boto3-medical-imaging (>=1.38.0,<1.39.0)", "mypy-boto3-memorydb (>=1.38.0,<1.39.0)", "mypy-boto3-meteringmarketplace (>=1.38.0,<1.39.0)", "mypy-boto3-mgh (>=1.38.0,<1.39.0)", "mypy-boto3-mgn (>=1.38.0,<1.39.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.38.0,<1.39.0)", "mypy-boto3-migrationhub-config (>=1.38.0,<1.39.0)", "mypy-boto3-migrationhuborchestrator (>=1.38.0,<1.39.0)", "mypy-boto3-migrationhubstrategy (>=1.38.0,<1.39.0)", "mypy-boto3-mq (>=1.38.0,<1.39.0)", "mypy-boto3-mturk (>=1.38.0,<1.39.0)", "mypy-boto3-mwaa (>=1.38.0,<1.39.0)", "mypy-boto3-neptune (>=1.38.0,<1.39.0)", "mypy-boto3-neptune-graph (>=1.38.0,<1.39.0)", "mypy-boto3-neptunedata (>=1.38.0,<1.39.0)", "mypy-boto3-network-firewall (>=1.38.0,<1.39.0)", "mypy-boto3-networkflowmonitor (>=1.38.0,<1.39.0)", "mypy-boto3-networkmanager (>=1.38.0,<1.39.0)", "mypy-boto3-networkmonitor (>=1.38.0,<1.39.0)", "mypy-boto3-notifications (>=1.38.0,<1.39.0)", "mypy-boto3-notificationscontacts (>=1.38.0,<1.39.0)", "mypy-boto3-oam (>=1.38.0,<1.39.0)", "mypy-boto3-observabilityadmin (>=1.38.0,<1.39.0)", "mypy-boto3-omics (>=1.38.0,<1.39.0)", "mypy-boto3-opensearch (>=1.38.0,<1.39.0)", "mypy-boto3-opensearchserverless (>=1.38.0,<1.39.0)", "mypy-boto3-opsworks (>=1.38.0,<1.39.0)", "mypy-boto3-opsworkscm (>=1.38.0,<1.39.0)", "mypy-boto3-organizations (>=1.38.0,<1.39.0)", "mypy-boto3-osis (>=1.38.0,<1.39.0)", "mypy-boto3-outposts (>=1.38.0,<1.39.0)", "mypy-boto3-panorama (>=1.38.0,<1.39.0)", "mypy-boto3-partnercentral-selling (>=1.38.0,<1.39.0)", "mypy-boto3-payment-cryptography (>=1.38.0,<1.39.0)", "mypy-boto3-payment-cryptography-data (>=1.38.0,<1.39.0)", "mypy-boto3-pca-connector-ad (>=1.38.0,<1.39.0)", "mypy-boto3-pca-connector-scep (>=1.38.0,<1.39.0)", "mypy-boto3-pcs (>=1.38.0,<1.39.0)", "mypy-boto3-personalize (>=1.38.0,<1.39.0)", "mypy-boto3-personalize-events (>=1.38.0,<1.39.0)", "mypy-boto3-personalize-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-pi (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint-email (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint-sms-voice (>=1.38.0,<1.39.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.38.0,<1.39.0)", "mypy-boto3-pipes (>=1.38.0,<1.39.0)", "mypy-boto3-polly (>=1.38.0,<1.39.0)", "mypy-boto3-pricing (>=1.38.0,<1.39.0)", "mypy-boto3-proton (>=1.38.0,<1.39.0)", "mypy-boto3-qapps (>=1.38.0,<1.39.0)", "mypy-boto3-qbusiness (>=1.38.0,<1.39.0)", "mypy-boto3-qconnect (>=1.38.0,<1.39.0)", "mypy-boto3-qldb (>=1.38.0,<1.39.0)", "mypy-boto3-qldb-session (>=1.38.0,<1.39.0)", "mypy-boto3-quicksight (>=1.38.0,<1.39.0)", "mypy-boto3-ram (>=1.38.0,<1.39.0)", "mypy-boto3-rbin (>=1.38.0,<1.39.0)", "mypy-boto3-rds (>=1.38.0,<1.39.0)", "mypy-boto3-rds-data (>=1.38.0,<1.39.0)", "mypy-boto3-redshift (>=1.38.0,<1.39.0)", "mypy-boto3-redshift-data (>=1.38.0,<1.39.0)", "mypy-boto3-redshift-serverless (>=1.38.0,<1.39.0)", "mypy-boto3-rekognition (>=1.38.0,<1.39.0)", "mypy-boto3-repostspace (>=1.38.0,<1.39.0)", "mypy-boto3-resiliencehub (>=1.38.0,<1.39.0)", "mypy-boto3-resource-explorer-2 (>=1.38.0,<1.39.0)", "mypy-boto3-resource-groups (>=1.38.0,<1.39.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.38.0,<1.39.0)", "mypy-boto3-robomaker (>=1.38.0,<1.39.0)", "mypy-boto3-rolesanywhere (>=1.38.0,<1.39.0)", "mypy-boto3-route53 (>=1.38.0,<1.39.0)", "mypy-boto3-route53-recovery-cluster (>=1.38.0,<1.39.0)", "mypy-boto3-route53-recovery-control-config (>=1.38.0,<1.39.0)", "mypy-boto3-route53-recovery-readiness (>=1.38.0,<1.39.0)", "mypy-boto3-route53domains (>=1.38.0,<1.39.0)", "mypy-boto3-route53profiles (>=1.38.0,<1.39.0)", "mypy-boto3-route53resolver (>=1.38.0,<1.39.0)", "mypy-boto3-rum (>=1.38.0,<1.39.0)", "mypy-boto3-s3 (>=1.38.0,<1.39.0)", "mypy-boto3-s3control (>=1.38.0,<1.39.0)", "mypy-boto3-s3outposts (>=1.38.0,<1.39.0)", "mypy-boto3-s3tables (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-edge (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-geospatial (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-metrics (>=1.38.0,<1.39.0)", "mypy-boto3-sagemaker-runtime (>=1.38.0,<1.39.0)", "mypy-boto3-savingsplans (>=1.38.0,<1.39.0)", "mypy-boto3-scheduler (>=1.38.0,<1.39.0)", "mypy-boto3-schemas (>=1.38.0,<1.39.0)", "mypy-boto3-sdb (>=1.38.0,<1.39.0)", "mypy-boto3-secretsmanager (>=1.38.0,<1.39.0)", "mypy-boto3-security-ir (>=1.38.0,<1.39.0)", "mypy-boto3-securityhub (>=1.38.0,<1.39.0)", "mypy-boto3-securitylake (>=1.38.0,<1.39.0)", "mypy-boto3-serverlessrepo (>=1.38.0,<1.39.0)", "mypy-boto3-service-quotas (>=1.38.0,<1.39.0)", "mypy-boto3-servicecatalog (>=1.38.0,<1.39.0)", "mypy-boto3-servicecatalog-appregistry (>=1.38.0,<1.39.0)", "mypy-boto3-servicediscovery (>=1.38.0,<1.39.0)", "mypy-boto3-ses (>=1.38.0,<1.39.0)", "mypy-boto3-sesv2 (>=1.38.0,<1.39.0)", "mypy-boto3-shield (>=1.38.0,<1.39.0)", "mypy-boto3-signer (>=1.38.0,<1.39.0)", "mypy-boto3-simspaceweaver (>=1.38.0,<1.39.0)", "mypy-boto3-sms (>=1.38.0,<1.39.0)", "mypy-boto3-snow-device-management (>=1.38.0,<1.39.0)", "mypy-boto3-snowball (>=1.38.0,<1.39.0)", "mypy-boto3-sns (>=1.38.0,<1.39.0)", "mypy-boto3-socialmessaging (>=1.38.0,<1.39.0)", "mypy-boto3-sqs (>=1.38.0,<1.39.0)", "mypy-boto3-ssm (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-contacts (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-guiconnect (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-incidents (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-quicksetup (>=1.38.0,<1.39.0)", "mypy-boto3-ssm-sap (>=1.38.0,<1.39.0)", "mypy-boto3-sso (>=1.38.0,<1.39.0)", "mypy-boto3-sso-admin (>=1.38.0,<1.39.0)", "mypy-boto3-sso-oidc (>=1.38.0,<1.39.0)", "mypy-boto3-stepfunctions (>=1.38.0,<1.39.0)", "mypy-boto3-storagegateway (>=1.38.0,<1.39.0)", "mypy-boto3-sts (>=1.38.0,<1.39.0)", "mypy-boto3-supplychain (>=1.38.0,<1.39.0)", "mypy-boto3-support (>=1.38.0,<1.39.0)", "mypy-boto3-support-app (>=1.38.0,<1.39.0)", "mypy-boto3-swf (>=1.38.0,<1.39.0)", "mypy-boto3-synthetics (>=1.38.0,<1.39.0)", "mypy-boto3-taxsettings (>=1.38.0,<1.39.0)", "mypy-boto3-textract (>=1.38.0,<1.39.0)", "mypy-boto3-timestream-influxdb (>=1.38.0,<1.39.0)", "mypy-boto3-timestream-query (>=1.38.0,<1.39.0)", "mypy-boto3-timestream-write (>=1.38.0,<1.39.0)", "mypy-boto3-tnb (>=1.38.0,<1.39.0)", "mypy-boto3-transcribe (>=1.38.0,<1.39.0)", "mypy-boto3-transfer (>=1.38.0,<1.39.0)", "mypy-boto3-translate (>=1.38.0,<1.39.0)", "mypy-boto3-trustedadvisor (>=1.38.0,<1.39.0)", "mypy-boto3-verifiedpermissions (>=1.38.0,<1.39.0)", "mypy-boto3-voice-id (>=1.38.0,<1.39.0)", "mypy-boto3-vpc-lattice (>=1.38.0,<1.39.0)", "mypy-boto3-waf (>=1.38.0,<1.39.0)", "mypy-boto3-waf-regional (>=1.38.0,<1.39.0)", "mypy-boto3-wafv2 (>=1.38.0,<1.39.0)", "mypy-boto3-wellarchitected (>=1.38.0,<1.39.0)", "mypy-boto3-wisdom (>=1.38.0,<1.39.0)", "mypy-boto3-workdocs (>=1.38.0,<1.39.0)", "mypy-boto3-workmail (>=1.38.0,<1.39.0)", "mypy-boto3-workmailmessageflow (>=1.38.0,<1.39.0)", "mypy-boto3-workspaces (>=1.38.0,<1.39.0)", "mypy-boto3-workspaces-thin-client (>=1.38.0,<1.39.0)", "mypy-boto3-workspaces-web (>=1.38.0,<1.39.0)", "mypy-boto3-xray (>=1.38.0,<1.39.0)"] -amp = ["mypy-boto3-amp (>=1.38.0,<1.39.0)"] -amplify = ["mypy-boto3-amplify (>=1.38.0,<1.39.0)"] -amplifybackend = ["mypy-boto3-amplifybackend (>=1.38.0,<1.39.0)"] -amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.38.0,<1.39.0)"] -apigateway = ["mypy-boto3-apigateway (>=1.38.0,<1.39.0)"] -apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.38.0,<1.39.0)"] -apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.38.0,<1.39.0)"] -appconfig = ["mypy-boto3-appconfig (>=1.38.0,<1.39.0)"] -appconfigdata = ["mypy-boto3-appconfigdata (>=1.38.0,<1.39.0)"] -appfabric = ["mypy-boto3-appfabric (>=1.38.0,<1.39.0)"] -appflow = ["mypy-boto3-appflow (>=1.38.0,<1.39.0)"] -appintegrations = ["mypy-boto3-appintegrations (>=1.38.0,<1.39.0)"] -application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.38.0,<1.39.0)"] -application-insights = ["mypy-boto3-application-insights (>=1.38.0,<1.39.0)"] -application-signals = ["mypy-boto3-application-signals (>=1.38.0,<1.39.0)"] -applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.38.0,<1.39.0)"] -appmesh = ["mypy-boto3-appmesh (>=1.38.0,<1.39.0)"] -apprunner = ["mypy-boto3-apprunner (>=1.38.0,<1.39.0)"] -appstream = ["mypy-boto3-appstream (>=1.38.0,<1.39.0)"] -appsync = ["mypy-boto3-appsync (>=1.38.0,<1.39.0)"] -apptest = ["mypy-boto3-apptest (>=1.38.0,<1.39.0)"] -arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.38.0,<1.39.0)"] -artifact = ["mypy-boto3-artifact (>=1.38.0,<1.39.0)"] -athena = ["mypy-boto3-athena (>=1.38.0,<1.39.0)"] -auditmanager = ["mypy-boto3-auditmanager (>=1.38.0,<1.39.0)"] -autoscaling = ["mypy-boto3-autoscaling (>=1.38.0,<1.39.0)"] -autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.38.0,<1.39.0)"] -b2bi = ["mypy-boto3-b2bi (>=1.38.0,<1.39.0)"] -backup = ["mypy-boto3-backup (>=1.38.0,<1.39.0)"] -backup-gateway = ["mypy-boto3-backup-gateway (>=1.38.0,<1.39.0)"] -backupsearch = ["mypy-boto3-backupsearch (>=1.38.0,<1.39.0)"] -batch = ["mypy-boto3-batch (>=1.38.0,<1.39.0)"] -bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.38.0,<1.39.0)"] -bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.38.0,<1.39.0)"] -bedrock = ["mypy-boto3-bedrock (>=1.38.0,<1.39.0)"] -bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.38.0,<1.39.0)"] -bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.38.0,<1.39.0)"] -bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.38.0,<1.39.0)"] -bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.38.0,<1.39.0)"] -bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"] -billing = ["mypy-boto3-billing (>=1.38.0,<1.39.0)"] -billingconductor = ["mypy-boto3-billingconductor (>=1.38.0,<1.39.0)"] -boto3 = ["boto3 (==1.38.34)"] -braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"] -budgets = ["mypy-boto3-budgets (>=1.38.0,<1.39.0)"] -ce = ["mypy-boto3-ce (>=1.38.0,<1.39.0)"] -chatbot = ["mypy-boto3-chatbot (>=1.38.0,<1.39.0)"] -chime = ["mypy-boto3-chime (>=1.38.0,<1.39.0)"] -chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.38.0,<1.39.0)"] -chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.38.0,<1.39.0)"] -chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.38.0,<1.39.0)"] -chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.38.0,<1.39.0)"] -chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.38.0,<1.39.0)"] -cleanrooms = ["mypy-boto3-cleanrooms (>=1.38.0,<1.39.0)"] -cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.38.0,<1.39.0)"] -cloud9 = ["mypy-boto3-cloud9 (>=1.38.0,<1.39.0)"] -cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.38.0,<1.39.0)"] -clouddirectory = ["mypy-boto3-clouddirectory (>=1.38.0,<1.39.0)"] -cloudformation = ["mypy-boto3-cloudformation (>=1.38.0,<1.39.0)"] -cloudfront = ["mypy-boto3-cloudfront (>=1.38.0,<1.39.0)"] -cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.38.0,<1.39.0)"] -cloudhsm = ["mypy-boto3-cloudhsm (>=1.38.0,<1.39.0)"] -cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.38.0,<1.39.0)"] -cloudsearch = ["mypy-boto3-cloudsearch (>=1.38.0,<1.39.0)"] -cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.38.0,<1.39.0)"] -cloudtrail = ["mypy-boto3-cloudtrail (>=1.38.0,<1.39.0)"] -cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.38.0,<1.39.0)"] -cloudwatch = ["mypy-boto3-cloudwatch (>=1.38.0,<1.39.0)"] -codeartifact = ["mypy-boto3-codeartifact (>=1.38.0,<1.39.0)"] -codebuild = ["mypy-boto3-codebuild (>=1.38.0,<1.39.0)"] -codecatalyst = ["mypy-boto3-codecatalyst (>=1.38.0,<1.39.0)"] -codecommit = ["mypy-boto3-codecommit (>=1.38.0,<1.39.0)"] -codeconnections = ["mypy-boto3-codeconnections (>=1.38.0,<1.39.0)"] -codedeploy = ["mypy-boto3-codedeploy (>=1.38.0,<1.39.0)"] -codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.38.0,<1.39.0)"] -codeguru-security = ["mypy-boto3-codeguru-security (>=1.38.0,<1.39.0)"] -codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.38.0,<1.39.0)"] -codepipeline = ["mypy-boto3-codepipeline (>=1.38.0,<1.39.0)"] -codestar-connections = ["mypy-boto3-codestar-connections (>=1.38.0,<1.39.0)"] -codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.38.0,<1.39.0)"] -cognito-identity = ["mypy-boto3-cognito-identity (>=1.38.0,<1.39.0)"] -cognito-idp = ["mypy-boto3-cognito-idp (>=1.38.0,<1.39.0)"] -cognito-sync = ["mypy-boto3-cognito-sync (>=1.38.0,<1.39.0)"] -comprehend = ["mypy-boto3-comprehend (>=1.38.0,<1.39.0)"] -comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.38.0,<1.39.0)"] -compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.38.0,<1.39.0)"] -config = ["mypy-boto3-config (>=1.38.0,<1.39.0)"] -connect = ["mypy-boto3-connect (>=1.38.0,<1.39.0)"] -connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.38.0,<1.39.0)"] -connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.38.0,<1.39.0)"] -connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.38.0,<1.39.0)"] -connectcases = ["mypy-boto3-connectcases (>=1.38.0,<1.39.0)"] -connectparticipant = ["mypy-boto3-connectparticipant (>=1.38.0,<1.39.0)"] -controlcatalog = ["mypy-boto3-controlcatalog (>=1.38.0,<1.39.0)"] -controltower = ["mypy-boto3-controltower (>=1.38.0,<1.39.0)"] -cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.38.0,<1.39.0)"] -cur = ["mypy-boto3-cur (>=1.38.0,<1.39.0)"] -customer-profiles = ["mypy-boto3-customer-profiles (>=1.38.0,<1.39.0)"] -databrew = ["mypy-boto3-databrew (>=1.38.0,<1.39.0)"] -dataexchange = ["mypy-boto3-dataexchange (>=1.38.0,<1.39.0)"] -datapipeline = ["mypy-boto3-datapipeline (>=1.38.0,<1.39.0)"] -datasync = ["mypy-boto3-datasync (>=1.38.0,<1.39.0)"] -datazone = ["mypy-boto3-datazone (>=1.38.0,<1.39.0)"] -dax = ["mypy-boto3-dax (>=1.38.0,<1.39.0)"] -deadline = ["mypy-boto3-deadline (>=1.38.0,<1.39.0)"] -detective = ["mypy-boto3-detective (>=1.38.0,<1.39.0)"] -devicefarm = ["mypy-boto3-devicefarm (>=1.38.0,<1.39.0)"] -devops-guru = ["mypy-boto3-devops-guru (>=1.38.0,<1.39.0)"] -directconnect = ["mypy-boto3-directconnect (>=1.38.0,<1.39.0)"] -discovery = ["mypy-boto3-discovery (>=1.38.0,<1.39.0)"] -dlm = ["mypy-boto3-dlm (>=1.38.0,<1.39.0)"] -dms = ["mypy-boto3-dms (>=1.38.0,<1.39.0)"] -docdb = ["mypy-boto3-docdb (>=1.38.0,<1.39.0)"] -docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.38.0,<1.39.0)"] -drs = ["mypy-boto3-drs (>=1.38.0,<1.39.0)"] -ds = ["mypy-boto3-ds (>=1.38.0,<1.39.0)"] -ds-data = ["mypy-boto3-ds-data (>=1.38.0,<1.39.0)"] -dsql = ["mypy-boto3-dsql (>=1.38.0,<1.39.0)"] -dynamodb = ["mypy-boto3-dynamodb (>=1.38.0,<1.39.0)"] -dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.38.0,<1.39.0)"] -ebs = ["mypy-boto3-ebs (>=1.38.0,<1.39.0)"] -ec2 = ["mypy-boto3-ec2 (>=1.38.0,<1.39.0)"] -ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.38.0,<1.39.0)"] -ecr = ["mypy-boto3-ecr (>=1.38.0,<1.39.0)"] -ecr-public = ["mypy-boto3-ecr-public (>=1.38.0,<1.39.0)"] -ecs = ["mypy-boto3-ecs (>=1.38.0,<1.39.0)"] -efs = ["mypy-boto3-efs (>=1.38.0,<1.39.0)"] -eks = ["mypy-boto3-eks (>=1.38.0,<1.39.0)"] -eks-auth = ["mypy-boto3-eks-auth (>=1.38.0,<1.39.0)"] -elasticache = ["mypy-boto3-elasticache (>=1.38.0,<1.39.0)"] -elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.38.0,<1.39.0)"] -elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.38.0,<1.39.0)"] -elb = ["mypy-boto3-elb (>=1.38.0,<1.39.0)"] -elbv2 = ["mypy-boto3-elbv2 (>=1.38.0,<1.39.0)"] -emr = ["mypy-boto3-emr (>=1.38.0,<1.39.0)"] -emr-containers = ["mypy-boto3-emr-containers (>=1.38.0,<1.39.0)"] -emr-serverless = ["mypy-boto3-emr-serverless (>=1.38.0,<1.39.0)"] -entityresolution = ["mypy-boto3-entityresolution (>=1.38.0,<1.39.0)"] -es = ["mypy-boto3-es (>=1.38.0,<1.39.0)"] -essential = ["mypy-boto3-cloudformation (>=1.38.0,<1.39.0)", "mypy-boto3-dynamodb (>=1.38.0,<1.39.0)", "mypy-boto3-ec2 (>=1.38.0,<1.39.0)", "mypy-boto3-lambda (>=1.38.0,<1.39.0)", "mypy-boto3-rds (>=1.38.0,<1.39.0)", "mypy-boto3-s3 (>=1.38.0,<1.39.0)", "mypy-boto3-sqs (>=1.38.0,<1.39.0)"] -events = ["mypy-boto3-events (>=1.38.0,<1.39.0)"] -evidently = ["mypy-boto3-evidently (>=1.38.0,<1.39.0)"] -evs = ["mypy-boto3-evs (>=1.38.0,<1.39.0)"] -finspace = ["mypy-boto3-finspace (>=1.38.0,<1.39.0)"] -finspace-data = ["mypy-boto3-finspace-data (>=1.38.0,<1.39.0)"] -firehose = ["mypy-boto3-firehose (>=1.38.0,<1.39.0)"] -fis = ["mypy-boto3-fis (>=1.38.0,<1.39.0)"] -fms = ["mypy-boto3-fms (>=1.38.0,<1.39.0)"] -forecast = ["mypy-boto3-forecast (>=1.38.0,<1.39.0)"] -forecastquery = ["mypy-boto3-forecastquery (>=1.38.0,<1.39.0)"] -frauddetector = ["mypy-boto3-frauddetector (>=1.38.0,<1.39.0)"] -freetier = ["mypy-boto3-freetier (>=1.38.0,<1.39.0)"] -fsx = ["mypy-boto3-fsx (>=1.38.0,<1.39.0)"] -full = ["boto3-stubs-full (>=1.38.0,<1.39.0)"] -gamelift = ["mypy-boto3-gamelift (>=1.38.0,<1.39.0)"] -gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.38.0,<1.39.0)"] -geo-maps = ["mypy-boto3-geo-maps (>=1.38.0,<1.39.0)"] -geo-places = ["mypy-boto3-geo-places (>=1.38.0,<1.39.0)"] -geo-routes = ["mypy-boto3-geo-routes (>=1.38.0,<1.39.0)"] -glacier = ["mypy-boto3-glacier (>=1.38.0,<1.39.0)"] -globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.38.0,<1.39.0)"] -glue = ["mypy-boto3-glue (>=1.38.0,<1.39.0)"] -grafana = ["mypy-boto3-grafana (>=1.38.0,<1.39.0)"] -greengrass = ["mypy-boto3-greengrass (>=1.38.0,<1.39.0)"] -greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.38.0,<1.39.0)"] -groundstation = ["mypy-boto3-groundstation (>=1.38.0,<1.39.0)"] -guardduty = ["mypy-boto3-guardduty (>=1.38.0,<1.39.0)"] -health = ["mypy-boto3-health (>=1.38.0,<1.39.0)"] -healthlake = ["mypy-boto3-healthlake (>=1.38.0,<1.39.0)"] -iam = ["mypy-boto3-iam (>=1.38.0,<1.39.0)"] -identitystore = ["mypy-boto3-identitystore (>=1.38.0,<1.39.0)"] -imagebuilder = ["mypy-boto3-imagebuilder (>=1.38.0,<1.39.0)"] -importexport = ["mypy-boto3-importexport (>=1.38.0,<1.39.0)"] -inspector = ["mypy-boto3-inspector (>=1.38.0,<1.39.0)"] -inspector-scan = ["mypy-boto3-inspector-scan (>=1.38.0,<1.39.0)"] -inspector2 = ["mypy-boto3-inspector2 (>=1.38.0,<1.39.0)"] -internetmonitor = ["mypy-boto3-internetmonitor (>=1.38.0,<1.39.0)"] -invoicing = ["mypy-boto3-invoicing (>=1.38.0,<1.39.0)"] -iot = ["mypy-boto3-iot (>=1.38.0,<1.39.0)"] -iot-data = ["mypy-boto3-iot-data (>=1.38.0,<1.39.0)"] -iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.38.0,<1.39.0)"] -iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.38.0,<1.39.0)"] -iotanalytics = ["mypy-boto3-iotanalytics (>=1.38.0,<1.39.0)"] -iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.38.0,<1.39.0)"] -iotevents = ["mypy-boto3-iotevents (>=1.38.0,<1.39.0)"] -iotevents-data = ["mypy-boto3-iotevents-data (>=1.38.0,<1.39.0)"] -iotfleethub = ["mypy-boto3-iotfleethub (>=1.38.0,<1.39.0)"] -iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.38.0,<1.39.0)"] -iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.38.0,<1.39.0)"] -iotsitewise = ["mypy-boto3-iotsitewise (>=1.38.0,<1.39.0)"] -iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.38.0,<1.39.0)"] -iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.38.0,<1.39.0)"] -iotwireless = ["mypy-boto3-iotwireless (>=1.38.0,<1.39.0)"] -ivs = ["mypy-boto3-ivs (>=1.38.0,<1.39.0)"] -ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.38.0,<1.39.0)"] -ivschat = ["mypy-boto3-ivschat (>=1.38.0,<1.39.0)"] -kafka = ["mypy-boto3-kafka (>=1.38.0,<1.39.0)"] -kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.38.0,<1.39.0)"] -kendra = ["mypy-boto3-kendra (>=1.38.0,<1.39.0)"] -kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.38.0,<1.39.0)"] -keyspaces = ["mypy-boto3-keyspaces (>=1.38.0,<1.39.0)"] -kinesis = ["mypy-boto3-kinesis (>=1.38.0,<1.39.0)"] -kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.38.0,<1.39.0)"] -kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.38.0,<1.39.0)"] -kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.38.0,<1.39.0)"] -kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.38.0,<1.39.0)"] -kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.38.0,<1.39.0)"] -kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.38.0,<1.39.0)"] -kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.38.0,<1.39.0)"] -kms = ["mypy-boto3-kms (>=1.38.0,<1.39.0)"] -lakeformation = ["mypy-boto3-lakeformation (>=1.38.0,<1.39.0)"] -lambda = ["mypy-boto3-lambda (>=1.38.0,<1.39.0)"] -launch-wizard = ["mypy-boto3-launch-wizard (>=1.38.0,<1.39.0)"] -lex-models = ["mypy-boto3-lex-models (>=1.38.0,<1.39.0)"] -lex-runtime = ["mypy-boto3-lex-runtime (>=1.38.0,<1.39.0)"] -lexv2-models = ["mypy-boto3-lexv2-models (>=1.38.0,<1.39.0)"] -lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.38.0,<1.39.0)"] -license-manager = ["mypy-boto3-license-manager (>=1.38.0,<1.39.0)"] -license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.38.0,<1.39.0)"] -license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.38.0,<1.39.0)"] -lightsail = ["mypy-boto3-lightsail (>=1.38.0,<1.39.0)"] -location = ["mypy-boto3-location (>=1.38.0,<1.39.0)"] -logs = ["mypy-boto3-logs (>=1.38.0,<1.39.0)"] -lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.38.0,<1.39.0)"] -lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.38.0,<1.39.0)"] -lookoutvision = ["mypy-boto3-lookoutvision (>=1.38.0,<1.39.0)"] -m2 = ["mypy-boto3-m2 (>=1.38.0,<1.39.0)"] -machinelearning = ["mypy-boto3-machinelearning (>=1.38.0,<1.39.0)"] -macie2 = ["mypy-boto3-macie2 (>=1.38.0,<1.39.0)"] -mailmanager = ["mypy-boto3-mailmanager (>=1.38.0,<1.39.0)"] -managedblockchain = ["mypy-boto3-managedblockchain (>=1.38.0,<1.39.0)"] -managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.38.0,<1.39.0)"] -marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.38.0,<1.39.0)"] -marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.38.0,<1.39.0)"] -marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.38.0,<1.39.0)"] -marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.38.0,<1.39.0)"] -marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.38.0,<1.39.0)"] -marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.38.0,<1.39.0)"] -mediaconnect = ["mypy-boto3-mediaconnect (>=1.38.0,<1.39.0)"] -mediaconvert = ["mypy-boto3-mediaconvert (>=1.38.0,<1.39.0)"] -medialive = ["mypy-boto3-medialive (>=1.38.0,<1.39.0)"] -mediapackage = ["mypy-boto3-mediapackage (>=1.38.0,<1.39.0)"] -mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.38.0,<1.39.0)"] -mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.38.0,<1.39.0)"] -mediastore = ["mypy-boto3-mediastore (>=1.38.0,<1.39.0)"] -mediastore-data = ["mypy-boto3-mediastore-data (>=1.38.0,<1.39.0)"] -mediatailor = ["mypy-boto3-mediatailor (>=1.38.0,<1.39.0)"] -medical-imaging = ["mypy-boto3-medical-imaging (>=1.38.0,<1.39.0)"] -memorydb = ["mypy-boto3-memorydb (>=1.38.0,<1.39.0)"] -meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.38.0,<1.39.0)"] -mgh = ["mypy-boto3-mgh (>=1.38.0,<1.39.0)"] -mgn = ["mypy-boto3-mgn (>=1.38.0,<1.39.0)"] -migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.38.0,<1.39.0)"] -migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.38.0,<1.39.0)"] -migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.38.0,<1.39.0)"] -migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.38.0,<1.39.0)"] -mq = ["mypy-boto3-mq (>=1.38.0,<1.39.0)"] -mturk = ["mypy-boto3-mturk (>=1.38.0,<1.39.0)"] -mwaa = ["mypy-boto3-mwaa (>=1.38.0,<1.39.0)"] -neptune = ["mypy-boto3-neptune (>=1.38.0,<1.39.0)"] -neptune-graph = ["mypy-boto3-neptune-graph (>=1.38.0,<1.39.0)"] -neptunedata = ["mypy-boto3-neptunedata (>=1.38.0,<1.39.0)"] -network-firewall = ["mypy-boto3-network-firewall (>=1.38.0,<1.39.0)"] -networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.38.0,<1.39.0)"] -networkmanager = ["mypy-boto3-networkmanager (>=1.38.0,<1.39.0)"] -networkmonitor = ["mypy-boto3-networkmonitor (>=1.38.0,<1.39.0)"] -notifications = ["mypy-boto3-notifications (>=1.38.0,<1.39.0)"] -notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.38.0,<1.39.0)"] -oam = ["mypy-boto3-oam (>=1.38.0,<1.39.0)"] -observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.38.0,<1.39.0)"] -omics = ["mypy-boto3-omics (>=1.38.0,<1.39.0)"] -opensearch = ["mypy-boto3-opensearch (>=1.38.0,<1.39.0)"] -opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.38.0,<1.39.0)"] -opsworks = ["mypy-boto3-opsworks (>=1.38.0,<1.39.0)"] -opsworkscm = ["mypy-boto3-opsworkscm (>=1.38.0,<1.39.0)"] -organizations = ["mypy-boto3-organizations (>=1.38.0,<1.39.0)"] -osis = ["mypy-boto3-osis (>=1.38.0,<1.39.0)"] -outposts = ["mypy-boto3-outposts (>=1.38.0,<1.39.0)"] -panorama = ["mypy-boto3-panorama (>=1.38.0,<1.39.0)"] -partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.38.0,<1.39.0)"] -payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.38.0,<1.39.0)"] -payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.38.0,<1.39.0)"] -pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.38.0,<1.39.0)"] -pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.38.0,<1.39.0)"] -pcs = ["mypy-boto3-pcs (>=1.38.0,<1.39.0)"] -personalize = ["mypy-boto3-personalize (>=1.38.0,<1.39.0)"] -personalize-events = ["mypy-boto3-personalize-events (>=1.38.0,<1.39.0)"] -personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.38.0,<1.39.0)"] -pi = ["mypy-boto3-pi (>=1.38.0,<1.39.0)"] -pinpoint = ["mypy-boto3-pinpoint (>=1.38.0,<1.39.0)"] -pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.38.0,<1.39.0)"] -pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.38.0,<1.39.0)"] -pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.38.0,<1.39.0)"] -pipes = ["mypy-boto3-pipes (>=1.38.0,<1.39.0)"] -polly = ["mypy-boto3-polly (>=1.38.0,<1.39.0)"] -pricing = ["mypy-boto3-pricing (>=1.38.0,<1.39.0)"] -proton = ["mypy-boto3-proton (>=1.38.0,<1.39.0)"] -qapps = ["mypy-boto3-qapps (>=1.38.0,<1.39.0)"] -qbusiness = ["mypy-boto3-qbusiness (>=1.38.0,<1.39.0)"] -qconnect = ["mypy-boto3-qconnect (>=1.38.0,<1.39.0)"] -qldb = ["mypy-boto3-qldb (>=1.38.0,<1.39.0)"] -qldb-session = ["mypy-boto3-qldb-session (>=1.38.0,<1.39.0)"] -quicksight = ["mypy-boto3-quicksight (>=1.38.0,<1.39.0)"] -ram = ["mypy-boto3-ram (>=1.38.0,<1.39.0)"] -rbin = ["mypy-boto3-rbin (>=1.38.0,<1.39.0)"] -rds = ["mypy-boto3-rds (>=1.38.0,<1.39.0)"] -rds-data = ["mypy-boto3-rds-data (>=1.38.0,<1.39.0)"] -redshift = ["mypy-boto3-redshift (>=1.38.0,<1.39.0)"] -redshift-data = ["mypy-boto3-redshift-data (>=1.38.0,<1.39.0)"] -redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.38.0,<1.39.0)"] -rekognition = ["mypy-boto3-rekognition (>=1.38.0,<1.39.0)"] -repostspace = ["mypy-boto3-repostspace (>=1.38.0,<1.39.0)"] -resiliencehub = ["mypy-boto3-resiliencehub (>=1.38.0,<1.39.0)"] -resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.38.0,<1.39.0)"] -resource-groups = ["mypy-boto3-resource-groups (>=1.38.0,<1.39.0)"] -resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.38.0,<1.39.0)"] -robomaker = ["mypy-boto3-robomaker (>=1.38.0,<1.39.0)"] -rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.38.0,<1.39.0)"] -route53 = ["mypy-boto3-route53 (>=1.38.0,<1.39.0)"] -route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.38.0,<1.39.0)"] -route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.38.0,<1.39.0)"] -route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.38.0,<1.39.0)"] -route53domains = ["mypy-boto3-route53domains (>=1.38.0,<1.39.0)"] -route53profiles = ["mypy-boto3-route53profiles (>=1.38.0,<1.39.0)"] -route53resolver = ["mypy-boto3-route53resolver (>=1.38.0,<1.39.0)"] -rum = ["mypy-boto3-rum (>=1.38.0,<1.39.0)"] -s3 = ["mypy-boto3-s3 (>=1.38.0,<1.39.0)"] -s3control = ["mypy-boto3-s3control (>=1.38.0,<1.39.0)"] -s3outposts = ["mypy-boto3-s3outposts (>=1.38.0,<1.39.0)"] -s3tables = ["mypy-boto3-s3tables (>=1.38.0,<1.39.0)"] -sagemaker = ["mypy-boto3-sagemaker (>=1.38.0,<1.39.0)"] -sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.38.0,<1.39.0)"] -sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.38.0,<1.39.0)"] -sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.38.0,<1.39.0)"] -sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.38.0,<1.39.0)"] -sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.38.0,<1.39.0)"] -sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.38.0,<1.39.0)"] -savingsplans = ["mypy-boto3-savingsplans (>=1.38.0,<1.39.0)"] -scheduler = ["mypy-boto3-scheduler (>=1.38.0,<1.39.0)"] -schemas = ["mypy-boto3-schemas (>=1.38.0,<1.39.0)"] -sdb = ["mypy-boto3-sdb (>=1.38.0,<1.39.0)"] -secretsmanager = ["mypy-boto3-secretsmanager (>=1.38.0,<1.39.0)"] -security-ir = ["mypy-boto3-security-ir (>=1.38.0,<1.39.0)"] -securityhub = ["mypy-boto3-securityhub (>=1.38.0,<1.39.0)"] -securitylake = ["mypy-boto3-securitylake (>=1.38.0,<1.39.0)"] -serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.38.0,<1.39.0)"] -service-quotas = ["mypy-boto3-service-quotas (>=1.38.0,<1.39.0)"] -servicecatalog = ["mypy-boto3-servicecatalog (>=1.38.0,<1.39.0)"] -servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.38.0,<1.39.0)"] -servicediscovery = ["mypy-boto3-servicediscovery (>=1.38.0,<1.39.0)"] -ses = ["mypy-boto3-ses (>=1.38.0,<1.39.0)"] -sesv2 = ["mypy-boto3-sesv2 (>=1.38.0,<1.39.0)"] -shield = ["mypy-boto3-shield (>=1.38.0,<1.39.0)"] -signer = ["mypy-boto3-signer (>=1.38.0,<1.39.0)"] -simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.38.0,<1.39.0)"] -sms = ["mypy-boto3-sms (>=1.38.0,<1.39.0)"] -snow-device-management = ["mypy-boto3-snow-device-management (>=1.38.0,<1.39.0)"] -snowball = ["mypy-boto3-snowball (>=1.38.0,<1.39.0)"] -sns = ["mypy-boto3-sns (>=1.38.0,<1.39.0)"] -socialmessaging = ["mypy-boto3-socialmessaging (>=1.38.0,<1.39.0)"] -sqs = ["mypy-boto3-sqs (>=1.38.0,<1.39.0)"] -ssm = ["mypy-boto3-ssm (>=1.38.0,<1.39.0)"] -ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.38.0,<1.39.0)"] -ssm-guiconnect = ["mypy-boto3-ssm-guiconnect (>=1.38.0,<1.39.0)"] -ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.38.0,<1.39.0)"] -ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.38.0,<1.39.0)"] -ssm-sap = ["mypy-boto3-ssm-sap (>=1.38.0,<1.39.0)"] -sso = ["mypy-boto3-sso (>=1.38.0,<1.39.0)"] -sso-admin = ["mypy-boto3-sso-admin (>=1.38.0,<1.39.0)"] -sso-oidc = ["mypy-boto3-sso-oidc (>=1.38.0,<1.39.0)"] -stepfunctions = ["mypy-boto3-stepfunctions (>=1.38.0,<1.39.0)"] -storagegateway = ["mypy-boto3-storagegateway (>=1.38.0,<1.39.0)"] -sts = ["mypy-boto3-sts (>=1.38.0,<1.39.0)"] -supplychain = ["mypy-boto3-supplychain (>=1.38.0,<1.39.0)"] -support = ["mypy-boto3-support (>=1.38.0,<1.39.0)"] -support-app = ["mypy-boto3-support-app (>=1.38.0,<1.39.0)"] -swf = ["mypy-boto3-swf (>=1.38.0,<1.39.0)"] -synthetics = ["mypy-boto3-synthetics (>=1.38.0,<1.39.0)"] -taxsettings = ["mypy-boto3-taxsettings (>=1.38.0,<1.39.0)"] -textract = ["mypy-boto3-textract (>=1.38.0,<1.39.0)"] -timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.38.0,<1.39.0)"] -timestream-query = ["mypy-boto3-timestream-query (>=1.38.0,<1.39.0)"] -timestream-write = ["mypy-boto3-timestream-write (>=1.38.0,<1.39.0)"] -tnb = ["mypy-boto3-tnb (>=1.38.0,<1.39.0)"] -transcribe = ["mypy-boto3-transcribe (>=1.38.0,<1.39.0)"] -transfer = ["mypy-boto3-transfer (>=1.38.0,<1.39.0)"] -translate = ["mypy-boto3-translate (>=1.38.0,<1.39.0)"] -trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.38.0,<1.39.0)"] -verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.38.0,<1.39.0)"] -voice-id = ["mypy-boto3-voice-id (>=1.38.0,<1.39.0)"] -vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.38.0,<1.39.0)"] -waf = ["mypy-boto3-waf (>=1.38.0,<1.39.0)"] -waf-regional = ["mypy-boto3-waf-regional (>=1.38.0,<1.39.0)"] -wafv2 = ["mypy-boto3-wafv2 (>=1.38.0,<1.39.0)"] -wellarchitected = ["mypy-boto3-wellarchitected (>=1.38.0,<1.39.0)"] -wisdom = ["mypy-boto3-wisdom (>=1.38.0,<1.39.0)"] -workdocs = ["mypy-boto3-workdocs (>=1.38.0,<1.39.0)"] -workmail = ["mypy-boto3-workmail (>=1.38.0,<1.39.0)"] -workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.38.0,<1.39.0)"] -workspaces = ["mypy-boto3-workspaces (>=1.38.0,<1.39.0)"] -workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.38.0,<1.39.0)"] -workspaces-web = ["mypy-boto3-workspaces-web (>=1.38.0,<1.39.0)"] -xray = ["mypy-boto3-xray (>=1.38.0,<1.39.0)"] +accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.40.0,<1.41.0)"] +account = ["mypy-boto3-account (>=1.40.0,<1.41.0)"] +acm = ["mypy-boto3-acm (>=1.40.0,<1.41.0)"] +acm-pca = ["mypy-boto3-acm-pca (>=1.40.0,<1.41.0)"] +aiops = ["mypy-boto3-aiops (>=1.40.0,<1.41.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.40.0,<1.41.0)", "mypy-boto3-account (>=1.40.0,<1.41.0)", "mypy-boto3-acm (>=1.40.0,<1.41.0)", "mypy-boto3-acm-pca (>=1.40.0,<1.41.0)", "mypy-boto3-aiops (>=1.40.0,<1.41.0)", "mypy-boto3-amp (>=1.40.0,<1.41.0)", "mypy-boto3-amplify (>=1.40.0,<1.41.0)", "mypy-boto3-amplifybackend (>=1.40.0,<1.41.0)", "mypy-boto3-amplifyuibuilder (>=1.40.0,<1.41.0)", "mypy-boto3-apigateway (>=1.40.0,<1.41.0)", "mypy-boto3-apigatewaymanagementapi (>=1.40.0,<1.41.0)", "mypy-boto3-apigatewayv2 (>=1.40.0,<1.41.0)", "mypy-boto3-appconfig (>=1.40.0,<1.41.0)", "mypy-boto3-appconfigdata (>=1.40.0,<1.41.0)", "mypy-boto3-appfabric (>=1.40.0,<1.41.0)", "mypy-boto3-appflow (>=1.40.0,<1.41.0)", "mypy-boto3-appintegrations (>=1.40.0,<1.41.0)", "mypy-boto3-application-autoscaling (>=1.40.0,<1.41.0)", "mypy-boto3-application-insights (>=1.40.0,<1.41.0)", "mypy-boto3-application-signals (>=1.40.0,<1.41.0)", "mypy-boto3-applicationcostprofiler (>=1.40.0,<1.41.0)", "mypy-boto3-appmesh (>=1.40.0,<1.41.0)", "mypy-boto3-apprunner (>=1.40.0,<1.41.0)", "mypy-boto3-appstream (>=1.40.0,<1.41.0)", "mypy-boto3-appsync (>=1.40.0,<1.41.0)", "mypy-boto3-apptest (>=1.40.0,<1.41.0)", "mypy-boto3-arc-region-switch (>=1.40.0,<1.41.0)", "mypy-boto3-arc-zonal-shift (>=1.40.0,<1.41.0)", "mypy-boto3-artifact (>=1.40.0,<1.41.0)", "mypy-boto3-athena (>=1.40.0,<1.41.0)", "mypy-boto3-auditmanager (>=1.40.0,<1.41.0)", "mypy-boto3-autoscaling (>=1.40.0,<1.41.0)", "mypy-boto3-autoscaling-plans (>=1.40.0,<1.41.0)", "mypy-boto3-b2bi (>=1.40.0,<1.41.0)", "mypy-boto3-backup (>=1.40.0,<1.41.0)", "mypy-boto3-backup-gateway (>=1.40.0,<1.41.0)", "mypy-boto3-backupsearch (>=1.40.0,<1.41.0)", "mypy-boto3-batch (>=1.40.0,<1.41.0)", "mypy-boto3-bcm-data-exports (>=1.40.0,<1.41.0)", "mypy-boto3-bcm-pricing-calculator (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agent (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agent-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agentcore (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agentcore-control (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-data-automation (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-billing (>=1.40.0,<1.41.0)", "mypy-boto3-billingconductor (>=1.40.0,<1.41.0)", "mypy-boto3-braket (>=1.40.0,<1.41.0)", "mypy-boto3-budgets (>=1.40.0,<1.41.0)", "mypy-boto3-ce (>=1.40.0,<1.41.0)", "mypy-boto3-chatbot (>=1.40.0,<1.41.0)", "mypy-boto3-chime (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-identity (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-meetings (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-messaging (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-voice (>=1.40.0,<1.41.0)", "mypy-boto3-cleanrooms (>=1.40.0,<1.41.0)", "mypy-boto3-cleanroomsml (>=1.40.0,<1.41.0)", "mypy-boto3-cloud9 (>=1.40.0,<1.41.0)", "mypy-boto3-cloudcontrol (>=1.40.0,<1.41.0)", "mypy-boto3-clouddirectory (>=1.40.0,<1.41.0)", "mypy-boto3-cloudformation (>=1.40.0,<1.41.0)", "mypy-boto3-cloudfront (>=1.40.0,<1.41.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.40.0,<1.41.0)", "mypy-boto3-cloudhsm (>=1.40.0,<1.41.0)", "mypy-boto3-cloudhsmv2 (>=1.40.0,<1.41.0)", "mypy-boto3-cloudsearch (>=1.40.0,<1.41.0)", "mypy-boto3-cloudsearchdomain (>=1.40.0,<1.41.0)", "mypy-boto3-cloudtrail (>=1.40.0,<1.41.0)", "mypy-boto3-cloudtrail-data (>=1.40.0,<1.41.0)", "mypy-boto3-cloudwatch (>=1.40.0,<1.41.0)", "mypy-boto3-codeartifact (>=1.40.0,<1.41.0)", "mypy-boto3-codebuild (>=1.40.0,<1.41.0)", "mypy-boto3-codecatalyst (>=1.40.0,<1.41.0)", "mypy-boto3-codecommit (>=1.40.0,<1.41.0)", "mypy-boto3-codeconnections (>=1.40.0,<1.41.0)", "mypy-boto3-codedeploy (>=1.40.0,<1.41.0)", "mypy-boto3-codeguru-reviewer (>=1.40.0,<1.41.0)", "mypy-boto3-codeguru-security (>=1.40.0,<1.41.0)", "mypy-boto3-codeguruprofiler (>=1.40.0,<1.41.0)", "mypy-boto3-codepipeline (>=1.40.0,<1.41.0)", "mypy-boto3-codestar-connections (>=1.40.0,<1.41.0)", "mypy-boto3-codestar-notifications (>=1.40.0,<1.41.0)", "mypy-boto3-cognito-identity (>=1.40.0,<1.41.0)", "mypy-boto3-cognito-idp (>=1.40.0,<1.41.0)", "mypy-boto3-cognito-sync (>=1.40.0,<1.41.0)", "mypy-boto3-comprehend (>=1.40.0,<1.41.0)", "mypy-boto3-comprehendmedical (>=1.40.0,<1.41.0)", "mypy-boto3-compute-optimizer (>=1.40.0,<1.41.0)", "mypy-boto3-config (>=1.40.0,<1.41.0)", "mypy-boto3-connect (>=1.40.0,<1.41.0)", "mypy-boto3-connect-contact-lens (>=1.40.0,<1.41.0)", "mypy-boto3-connectcampaigns (>=1.40.0,<1.41.0)", "mypy-boto3-connectcampaignsv2 (>=1.40.0,<1.41.0)", "mypy-boto3-connectcases (>=1.40.0,<1.41.0)", "mypy-boto3-connectparticipant (>=1.40.0,<1.41.0)", "mypy-boto3-controlcatalog (>=1.40.0,<1.41.0)", "mypy-boto3-controltower (>=1.40.0,<1.41.0)", "mypy-boto3-cost-optimization-hub (>=1.40.0,<1.41.0)", "mypy-boto3-cur (>=1.40.0,<1.41.0)", "mypy-boto3-customer-profiles (>=1.40.0,<1.41.0)", "mypy-boto3-databrew (>=1.40.0,<1.41.0)", "mypy-boto3-dataexchange (>=1.40.0,<1.41.0)", "mypy-boto3-datapipeline (>=1.40.0,<1.41.0)", "mypy-boto3-datasync (>=1.40.0,<1.41.0)", "mypy-boto3-datazone (>=1.40.0,<1.41.0)", "mypy-boto3-dax (>=1.40.0,<1.41.0)", "mypy-boto3-deadline (>=1.40.0,<1.41.0)", "mypy-boto3-detective (>=1.40.0,<1.41.0)", "mypy-boto3-devicefarm (>=1.40.0,<1.41.0)", "mypy-boto3-devops-guru (>=1.40.0,<1.41.0)", "mypy-boto3-directconnect (>=1.40.0,<1.41.0)", "mypy-boto3-discovery (>=1.40.0,<1.41.0)", "mypy-boto3-dlm (>=1.40.0,<1.41.0)", "mypy-boto3-dms (>=1.40.0,<1.41.0)", "mypy-boto3-docdb (>=1.40.0,<1.41.0)", "mypy-boto3-docdb-elastic (>=1.40.0,<1.41.0)", "mypy-boto3-drs (>=1.40.0,<1.41.0)", "mypy-boto3-ds (>=1.40.0,<1.41.0)", "mypy-boto3-ds-data (>=1.40.0,<1.41.0)", "mypy-boto3-dsql (>=1.40.0,<1.41.0)", "mypy-boto3-dynamodb (>=1.40.0,<1.41.0)", "mypy-boto3-dynamodbstreams (>=1.40.0,<1.41.0)", "mypy-boto3-ebs (>=1.40.0,<1.41.0)", "mypy-boto3-ec2 (>=1.40.0,<1.41.0)", "mypy-boto3-ec2-instance-connect (>=1.40.0,<1.41.0)", "mypy-boto3-ecr (>=1.40.0,<1.41.0)", "mypy-boto3-ecr-public (>=1.40.0,<1.41.0)", "mypy-boto3-ecs (>=1.40.0,<1.41.0)", "mypy-boto3-efs (>=1.40.0,<1.41.0)", "mypy-boto3-eks (>=1.40.0,<1.41.0)", "mypy-boto3-eks-auth (>=1.40.0,<1.41.0)", "mypy-boto3-elasticache (>=1.40.0,<1.41.0)", "mypy-boto3-elasticbeanstalk (>=1.40.0,<1.41.0)", "mypy-boto3-elastictranscoder (>=1.40.0,<1.41.0)", "mypy-boto3-elb (>=1.40.0,<1.41.0)", "mypy-boto3-elbv2 (>=1.40.0,<1.41.0)", "mypy-boto3-emr (>=1.40.0,<1.41.0)", "mypy-boto3-emr-containers (>=1.40.0,<1.41.0)", "mypy-boto3-emr-serverless (>=1.40.0,<1.41.0)", "mypy-boto3-entityresolution (>=1.40.0,<1.41.0)", "mypy-boto3-es (>=1.40.0,<1.41.0)", "mypy-boto3-events (>=1.40.0,<1.41.0)", "mypy-boto3-evidently (>=1.40.0,<1.41.0)", "mypy-boto3-evs (>=1.40.0,<1.41.0)", "mypy-boto3-finspace (>=1.40.0,<1.41.0)", "mypy-boto3-finspace-data (>=1.40.0,<1.41.0)", "mypy-boto3-firehose (>=1.40.0,<1.41.0)", "mypy-boto3-fis (>=1.40.0,<1.41.0)", "mypy-boto3-fms (>=1.40.0,<1.41.0)", "mypy-boto3-forecast (>=1.40.0,<1.41.0)", "mypy-boto3-forecastquery (>=1.40.0,<1.41.0)", "mypy-boto3-frauddetector (>=1.40.0,<1.41.0)", "mypy-boto3-freetier (>=1.40.0,<1.41.0)", "mypy-boto3-fsx (>=1.40.0,<1.41.0)", "mypy-boto3-gamelift (>=1.40.0,<1.41.0)", "mypy-boto3-gameliftstreams (>=1.40.0,<1.41.0)", "mypy-boto3-geo-maps (>=1.40.0,<1.41.0)", "mypy-boto3-geo-places (>=1.40.0,<1.41.0)", "mypy-boto3-geo-routes (>=1.40.0,<1.41.0)", "mypy-boto3-glacier (>=1.40.0,<1.41.0)", "mypy-boto3-globalaccelerator (>=1.40.0,<1.41.0)", "mypy-boto3-glue (>=1.40.0,<1.41.0)", "mypy-boto3-grafana (>=1.40.0,<1.41.0)", "mypy-boto3-greengrass (>=1.40.0,<1.41.0)", "mypy-boto3-greengrassv2 (>=1.40.0,<1.41.0)", "mypy-boto3-groundstation (>=1.40.0,<1.41.0)", "mypy-boto3-guardduty (>=1.40.0,<1.41.0)", "mypy-boto3-health (>=1.40.0,<1.41.0)", "mypy-boto3-healthlake (>=1.40.0,<1.41.0)", "mypy-boto3-iam (>=1.40.0,<1.41.0)", "mypy-boto3-identitystore (>=1.40.0,<1.41.0)", "mypy-boto3-imagebuilder (>=1.40.0,<1.41.0)", "mypy-boto3-importexport (>=1.40.0,<1.41.0)", "mypy-boto3-inspector (>=1.40.0,<1.41.0)", "mypy-boto3-inspector-scan (>=1.40.0,<1.41.0)", "mypy-boto3-inspector2 (>=1.40.0,<1.41.0)", "mypy-boto3-internetmonitor (>=1.40.0,<1.41.0)", "mypy-boto3-invoicing (>=1.40.0,<1.41.0)", "mypy-boto3-iot (>=1.40.0,<1.41.0)", "mypy-boto3-iot-data (>=1.40.0,<1.41.0)", "mypy-boto3-iot-jobs-data (>=1.40.0,<1.41.0)", "mypy-boto3-iot-managed-integrations (>=1.40.0,<1.41.0)", "mypy-boto3-iotanalytics (>=1.40.0,<1.41.0)", "mypy-boto3-iotdeviceadvisor (>=1.40.0,<1.41.0)", "mypy-boto3-iotevents (>=1.40.0,<1.41.0)", "mypy-boto3-iotevents-data (>=1.40.0,<1.41.0)", "mypy-boto3-iotfleethub (>=1.40.0,<1.41.0)", "mypy-boto3-iotfleetwise (>=1.40.0,<1.41.0)", "mypy-boto3-iotsecuretunneling (>=1.40.0,<1.41.0)", "mypy-boto3-iotsitewise (>=1.40.0,<1.41.0)", "mypy-boto3-iotthingsgraph (>=1.40.0,<1.41.0)", "mypy-boto3-iottwinmaker (>=1.40.0,<1.41.0)", "mypy-boto3-iotwireless (>=1.40.0,<1.41.0)", "mypy-boto3-ivs (>=1.40.0,<1.41.0)", "mypy-boto3-ivs-realtime (>=1.40.0,<1.41.0)", "mypy-boto3-ivschat (>=1.40.0,<1.41.0)", "mypy-boto3-kafka (>=1.40.0,<1.41.0)", "mypy-boto3-kafkaconnect (>=1.40.0,<1.41.0)", "mypy-boto3-kendra (>=1.40.0,<1.41.0)", "mypy-boto3-kendra-ranking (>=1.40.0,<1.41.0)", "mypy-boto3-keyspaces (>=1.40.0,<1.41.0)", "mypy-boto3-keyspacesstreams (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-archived-media (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-media (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-signaling (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.40.0,<1.41.0)", "mypy-boto3-kinesisanalytics (>=1.40.0,<1.41.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.40.0,<1.41.0)", "mypy-boto3-kinesisvideo (>=1.40.0,<1.41.0)", "mypy-boto3-kms (>=1.40.0,<1.41.0)", "mypy-boto3-lakeformation (>=1.40.0,<1.41.0)", "mypy-boto3-lambda (>=1.40.0,<1.41.0)", "mypy-boto3-launch-wizard (>=1.40.0,<1.41.0)", "mypy-boto3-lex-models (>=1.40.0,<1.41.0)", "mypy-boto3-lex-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-lexv2-models (>=1.40.0,<1.41.0)", "mypy-boto3-lexv2-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-license-manager (>=1.40.0,<1.41.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.40.0,<1.41.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.40.0,<1.41.0)", "mypy-boto3-lightsail (>=1.40.0,<1.41.0)", "mypy-boto3-location (>=1.40.0,<1.41.0)", "mypy-boto3-logs (>=1.40.0,<1.41.0)", "mypy-boto3-lookoutequipment (>=1.40.0,<1.41.0)", "mypy-boto3-lookoutmetrics (>=1.40.0,<1.41.0)", "mypy-boto3-lookoutvision (>=1.40.0,<1.41.0)", "mypy-boto3-m2 (>=1.40.0,<1.41.0)", "mypy-boto3-machinelearning (>=1.40.0,<1.41.0)", "mypy-boto3-macie2 (>=1.40.0,<1.41.0)", "mypy-boto3-mailmanager (>=1.40.0,<1.41.0)", "mypy-boto3-managedblockchain (>=1.40.0,<1.41.0)", "mypy-boto3-managedblockchain-query (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-agreement (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-catalog (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-deployment (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-entitlement (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-reporting (>=1.40.0,<1.41.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.40.0,<1.41.0)", "mypy-boto3-mediaconnect (>=1.40.0,<1.41.0)", "mypy-boto3-mediaconvert (>=1.40.0,<1.41.0)", "mypy-boto3-medialive (>=1.40.0,<1.41.0)", "mypy-boto3-mediapackage (>=1.40.0,<1.41.0)", "mypy-boto3-mediapackage-vod (>=1.40.0,<1.41.0)", "mypy-boto3-mediapackagev2 (>=1.40.0,<1.41.0)", "mypy-boto3-mediastore (>=1.40.0,<1.41.0)", "mypy-boto3-mediastore-data (>=1.40.0,<1.41.0)", "mypy-boto3-mediatailor (>=1.40.0,<1.41.0)", "mypy-boto3-medical-imaging (>=1.40.0,<1.41.0)", "mypy-boto3-memorydb (>=1.40.0,<1.41.0)", "mypy-boto3-meteringmarketplace (>=1.40.0,<1.41.0)", "mypy-boto3-mgh (>=1.40.0,<1.41.0)", "mypy-boto3-mgn (>=1.40.0,<1.41.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.40.0,<1.41.0)", "mypy-boto3-migrationhub-config (>=1.40.0,<1.41.0)", "mypy-boto3-migrationhuborchestrator (>=1.40.0,<1.41.0)", "mypy-boto3-migrationhubstrategy (>=1.40.0,<1.41.0)", "mypy-boto3-mpa (>=1.40.0,<1.41.0)", "mypy-boto3-mq (>=1.40.0,<1.41.0)", "mypy-boto3-mturk (>=1.40.0,<1.41.0)", "mypy-boto3-mwaa (>=1.40.0,<1.41.0)", "mypy-boto3-neptune (>=1.40.0,<1.41.0)", "mypy-boto3-neptune-graph (>=1.40.0,<1.41.0)", "mypy-boto3-neptunedata (>=1.40.0,<1.41.0)", "mypy-boto3-network-firewall (>=1.40.0,<1.41.0)", "mypy-boto3-networkflowmonitor (>=1.40.0,<1.41.0)", "mypy-boto3-networkmanager (>=1.40.0,<1.41.0)", "mypy-boto3-networkmonitor (>=1.40.0,<1.41.0)", "mypy-boto3-notifications (>=1.40.0,<1.41.0)", "mypy-boto3-notificationscontacts (>=1.40.0,<1.41.0)", "mypy-boto3-oam (>=1.40.0,<1.41.0)", "mypy-boto3-observabilityadmin (>=1.40.0,<1.41.0)", "mypy-boto3-odb (>=1.40.0,<1.41.0)", "mypy-boto3-omics (>=1.40.0,<1.41.0)", "mypy-boto3-opensearch (>=1.40.0,<1.41.0)", "mypy-boto3-opensearchserverless (>=1.40.0,<1.41.0)", "mypy-boto3-opsworks (>=1.40.0,<1.41.0)", "mypy-boto3-opsworkscm (>=1.40.0,<1.41.0)", "mypy-boto3-organizations (>=1.40.0,<1.41.0)", "mypy-boto3-osis (>=1.40.0,<1.41.0)", "mypy-boto3-outposts (>=1.40.0,<1.41.0)", "mypy-boto3-panorama (>=1.40.0,<1.41.0)", "mypy-boto3-partnercentral-selling (>=1.40.0,<1.41.0)", "mypy-boto3-payment-cryptography (>=1.40.0,<1.41.0)", "mypy-boto3-payment-cryptography-data (>=1.40.0,<1.41.0)", "mypy-boto3-pca-connector-ad (>=1.40.0,<1.41.0)", "mypy-boto3-pca-connector-scep (>=1.40.0,<1.41.0)", "mypy-boto3-pcs (>=1.40.0,<1.41.0)", "mypy-boto3-personalize (>=1.40.0,<1.41.0)", "mypy-boto3-personalize-events (>=1.40.0,<1.41.0)", "mypy-boto3-personalize-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-pi (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint-email (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint-sms-voice (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.40.0,<1.41.0)", "mypy-boto3-pipes (>=1.40.0,<1.41.0)", "mypy-boto3-polly (>=1.40.0,<1.41.0)", "mypy-boto3-pricing (>=1.40.0,<1.41.0)", "mypy-boto3-proton (>=1.40.0,<1.41.0)", "mypy-boto3-qapps (>=1.40.0,<1.41.0)", "mypy-boto3-qbusiness (>=1.40.0,<1.41.0)", "mypy-boto3-qconnect (>=1.40.0,<1.41.0)", "mypy-boto3-qldb (>=1.40.0,<1.41.0)", "mypy-boto3-qldb-session (>=1.40.0,<1.41.0)", "mypy-boto3-quicksight (>=1.40.0,<1.41.0)", "mypy-boto3-ram (>=1.40.0,<1.41.0)", "mypy-boto3-rbin (>=1.40.0,<1.41.0)", "mypy-boto3-rds (>=1.40.0,<1.41.0)", "mypy-boto3-rds-data (>=1.40.0,<1.41.0)", "mypy-boto3-redshift (>=1.40.0,<1.41.0)", "mypy-boto3-redshift-data (>=1.40.0,<1.41.0)", "mypy-boto3-redshift-serverless (>=1.40.0,<1.41.0)", "mypy-boto3-rekognition (>=1.40.0,<1.41.0)", "mypy-boto3-repostspace (>=1.40.0,<1.41.0)", "mypy-boto3-resiliencehub (>=1.40.0,<1.41.0)", "mypy-boto3-resource-explorer-2 (>=1.40.0,<1.41.0)", "mypy-boto3-resource-groups (>=1.40.0,<1.41.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.40.0,<1.41.0)", "mypy-boto3-robomaker (>=1.40.0,<1.41.0)", "mypy-boto3-rolesanywhere (>=1.40.0,<1.41.0)", "mypy-boto3-route53 (>=1.40.0,<1.41.0)", "mypy-boto3-route53-recovery-cluster (>=1.40.0,<1.41.0)", "mypy-boto3-route53-recovery-control-config (>=1.40.0,<1.41.0)", "mypy-boto3-route53-recovery-readiness (>=1.40.0,<1.41.0)", "mypy-boto3-route53domains (>=1.40.0,<1.41.0)", "mypy-boto3-route53profiles (>=1.40.0,<1.41.0)", "mypy-boto3-route53resolver (>=1.40.0,<1.41.0)", "mypy-boto3-rum (>=1.40.0,<1.41.0)", "mypy-boto3-s3 (>=1.40.0,<1.41.0)", "mypy-boto3-s3control (>=1.40.0,<1.41.0)", "mypy-boto3-s3outposts (>=1.40.0,<1.41.0)", "mypy-boto3-s3tables (>=1.40.0,<1.41.0)", "mypy-boto3-s3vectors (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-edge (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-geospatial (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-metrics (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-savingsplans (>=1.40.0,<1.41.0)", "mypy-boto3-scheduler (>=1.40.0,<1.41.0)", "mypy-boto3-schemas (>=1.40.0,<1.41.0)", "mypy-boto3-sdb (>=1.40.0,<1.41.0)", "mypy-boto3-secretsmanager (>=1.40.0,<1.41.0)", "mypy-boto3-security-ir (>=1.40.0,<1.41.0)", "mypy-boto3-securityhub (>=1.40.0,<1.41.0)", "mypy-boto3-securitylake (>=1.40.0,<1.41.0)", "mypy-boto3-serverlessrepo (>=1.40.0,<1.41.0)", "mypy-boto3-service-quotas (>=1.40.0,<1.41.0)", "mypy-boto3-servicecatalog (>=1.40.0,<1.41.0)", "mypy-boto3-servicecatalog-appregistry (>=1.40.0,<1.41.0)", "mypy-boto3-servicediscovery (>=1.40.0,<1.41.0)", "mypy-boto3-ses (>=1.40.0,<1.41.0)", "mypy-boto3-sesv2 (>=1.40.0,<1.41.0)", "mypy-boto3-shield (>=1.40.0,<1.41.0)", "mypy-boto3-signer (>=1.40.0,<1.41.0)", "mypy-boto3-simspaceweaver (>=1.40.0,<1.41.0)", "mypy-boto3-sms (>=1.40.0,<1.41.0)", "mypy-boto3-snow-device-management (>=1.40.0,<1.41.0)", "mypy-boto3-snowball (>=1.40.0,<1.41.0)", "mypy-boto3-sns (>=1.40.0,<1.41.0)", "mypy-boto3-socialmessaging (>=1.40.0,<1.41.0)", "mypy-boto3-sqs (>=1.40.0,<1.41.0)", "mypy-boto3-ssm (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-contacts (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-guiconnect (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-incidents (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-quicksetup (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-sap (>=1.40.0,<1.41.0)", "mypy-boto3-sso (>=1.40.0,<1.41.0)", "mypy-boto3-sso-admin (>=1.40.0,<1.41.0)", "mypy-boto3-sso-oidc (>=1.40.0,<1.41.0)", "mypy-boto3-stepfunctions (>=1.40.0,<1.41.0)", "mypy-boto3-storagegateway (>=1.40.0,<1.41.0)", "mypy-boto3-sts (>=1.40.0,<1.41.0)", "mypy-boto3-supplychain (>=1.40.0,<1.41.0)", "mypy-boto3-support (>=1.40.0,<1.41.0)", "mypy-boto3-support-app (>=1.40.0,<1.41.0)", "mypy-boto3-swf (>=1.40.0,<1.41.0)", "mypy-boto3-synthetics (>=1.40.0,<1.41.0)", "mypy-boto3-taxsettings (>=1.40.0,<1.41.0)", "mypy-boto3-textract (>=1.40.0,<1.41.0)", "mypy-boto3-timestream-influxdb (>=1.40.0,<1.41.0)", "mypy-boto3-timestream-query (>=1.40.0,<1.41.0)", "mypy-boto3-timestream-write (>=1.40.0,<1.41.0)", "mypy-boto3-tnb (>=1.40.0,<1.41.0)", "mypy-boto3-transcribe (>=1.40.0,<1.41.0)", "mypy-boto3-transfer (>=1.40.0,<1.41.0)", "mypy-boto3-translate (>=1.40.0,<1.41.0)", "mypy-boto3-trustedadvisor (>=1.40.0,<1.41.0)", "mypy-boto3-verifiedpermissions (>=1.40.0,<1.41.0)", "mypy-boto3-voice-id (>=1.40.0,<1.41.0)", "mypy-boto3-vpc-lattice (>=1.40.0,<1.41.0)", "mypy-boto3-waf (>=1.40.0,<1.41.0)", "mypy-boto3-waf-regional (>=1.40.0,<1.41.0)", "mypy-boto3-wafv2 (>=1.40.0,<1.41.0)", "mypy-boto3-wellarchitected (>=1.40.0,<1.41.0)", "mypy-boto3-wisdom (>=1.40.0,<1.41.0)", "mypy-boto3-workdocs (>=1.40.0,<1.41.0)", "mypy-boto3-workmail (>=1.40.0,<1.41.0)", "mypy-boto3-workmailmessageflow (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces-instances (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces-thin-client (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces-web (>=1.40.0,<1.41.0)", "mypy-boto3-xray (>=1.40.0,<1.41.0)"] +amp = ["mypy-boto3-amp (>=1.40.0,<1.41.0)"] +amplify = ["mypy-boto3-amplify (>=1.40.0,<1.41.0)"] +amplifybackend = ["mypy-boto3-amplifybackend (>=1.40.0,<1.41.0)"] +amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.40.0,<1.41.0)"] +apigateway = ["mypy-boto3-apigateway (>=1.40.0,<1.41.0)"] +apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.40.0,<1.41.0)"] +apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.40.0,<1.41.0)"] +appconfig = ["mypy-boto3-appconfig (>=1.40.0,<1.41.0)"] +appconfigdata = ["mypy-boto3-appconfigdata (>=1.40.0,<1.41.0)"] +appfabric = ["mypy-boto3-appfabric (>=1.40.0,<1.41.0)"] +appflow = ["mypy-boto3-appflow (>=1.40.0,<1.41.0)"] +appintegrations = ["mypy-boto3-appintegrations (>=1.40.0,<1.41.0)"] +application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.40.0,<1.41.0)"] +application-insights = ["mypy-boto3-application-insights (>=1.40.0,<1.41.0)"] +application-signals = ["mypy-boto3-application-signals (>=1.40.0,<1.41.0)"] +applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.40.0,<1.41.0)"] +appmesh = ["mypy-boto3-appmesh (>=1.40.0,<1.41.0)"] +apprunner = ["mypy-boto3-apprunner (>=1.40.0,<1.41.0)"] +appstream = ["mypy-boto3-appstream (>=1.40.0,<1.41.0)"] +appsync = ["mypy-boto3-appsync (>=1.40.0,<1.41.0)"] +apptest = ["mypy-boto3-apptest (>=1.40.0,<1.41.0)"] +arc-region-switch = ["mypy-boto3-arc-region-switch (>=1.40.0,<1.41.0)"] +arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.40.0,<1.41.0)"] +artifact = ["mypy-boto3-artifact (>=1.40.0,<1.41.0)"] +athena = ["mypy-boto3-athena (>=1.40.0,<1.41.0)"] +auditmanager = ["mypy-boto3-auditmanager (>=1.40.0,<1.41.0)"] +autoscaling = ["mypy-boto3-autoscaling (>=1.40.0,<1.41.0)"] +autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.40.0,<1.41.0)"] +b2bi = ["mypy-boto3-b2bi (>=1.40.0,<1.41.0)"] +backup = ["mypy-boto3-backup (>=1.40.0,<1.41.0)"] +backup-gateway = ["mypy-boto3-backup-gateway (>=1.40.0,<1.41.0)"] +backupsearch = ["mypy-boto3-backupsearch (>=1.40.0,<1.41.0)"] +batch = ["mypy-boto3-batch (>=1.40.0,<1.41.0)"] +bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.40.0,<1.41.0)"] +bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.40.0,<1.41.0)"] +bedrock = ["mypy-boto3-bedrock (>=1.40.0,<1.41.0)"] +bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.40.0,<1.41.0)"] +bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.40.0,<1.41.0)"] +bedrock-agentcore = ["mypy-boto3-bedrock-agentcore (>=1.40.0,<1.41.0)"] +bedrock-agentcore-control = ["mypy-boto3-bedrock-agentcore-control (>=1.40.0,<1.41.0)"] +bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.40.0,<1.41.0)"] +bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.40.0,<1.41.0)"] +bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.40.0,<1.41.0)"] +billing = ["mypy-boto3-billing (>=1.40.0,<1.41.0)"] +billingconductor = ["mypy-boto3-billingconductor (>=1.40.0,<1.41.0)"] +boto3 = ["boto3 (==1.40.8)"] +braket = ["mypy-boto3-braket (>=1.40.0,<1.41.0)"] +budgets = ["mypy-boto3-budgets (>=1.40.0,<1.41.0)"] +ce = ["mypy-boto3-ce (>=1.40.0,<1.41.0)"] +chatbot = ["mypy-boto3-chatbot (>=1.40.0,<1.41.0)"] +chime = ["mypy-boto3-chime (>=1.40.0,<1.41.0)"] +chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.40.0,<1.41.0)"] +chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.40.0,<1.41.0)"] +chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.40.0,<1.41.0)"] +chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.40.0,<1.41.0)"] +chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.40.0,<1.41.0)"] +cleanrooms = ["mypy-boto3-cleanrooms (>=1.40.0,<1.41.0)"] +cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.40.0,<1.41.0)"] +cloud9 = ["mypy-boto3-cloud9 (>=1.40.0,<1.41.0)"] +cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.40.0,<1.41.0)"] +clouddirectory = ["mypy-boto3-clouddirectory (>=1.40.0,<1.41.0)"] +cloudformation = ["mypy-boto3-cloudformation (>=1.40.0,<1.41.0)"] +cloudfront = ["mypy-boto3-cloudfront (>=1.40.0,<1.41.0)"] +cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.40.0,<1.41.0)"] +cloudhsm = ["mypy-boto3-cloudhsm (>=1.40.0,<1.41.0)"] +cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.40.0,<1.41.0)"] +cloudsearch = ["mypy-boto3-cloudsearch (>=1.40.0,<1.41.0)"] +cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.40.0,<1.41.0)"] +cloudtrail = ["mypy-boto3-cloudtrail (>=1.40.0,<1.41.0)"] +cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.40.0,<1.41.0)"] +cloudwatch = ["mypy-boto3-cloudwatch (>=1.40.0,<1.41.0)"] +codeartifact = ["mypy-boto3-codeartifact (>=1.40.0,<1.41.0)"] +codebuild = ["mypy-boto3-codebuild (>=1.40.0,<1.41.0)"] +codecatalyst = ["mypy-boto3-codecatalyst (>=1.40.0,<1.41.0)"] +codecommit = ["mypy-boto3-codecommit (>=1.40.0,<1.41.0)"] +codeconnections = ["mypy-boto3-codeconnections (>=1.40.0,<1.41.0)"] +codedeploy = ["mypy-boto3-codedeploy (>=1.40.0,<1.41.0)"] +codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.40.0,<1.41.0)"] +codeguru-security = ["mypy-boto3-codeguru-security (>=1.40.0,<1.41.0)"] +codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.40.0,<1.41.0)"] +codepipeline = ["mypy-boto3-codepipeline (>=1.40.0,<1.41.0)"] +codestar-connections = ["mypy-boto3-codestar-connections (>=1.40.0,<1.41.0)"] +codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.40.0,<1.41.0)"] +cognito-identity = ["mypy-boto3-cognito-identity (>=1.40.0,<1.41.0)"] +cognito-idp = ["mypy-boto3-cognito-idp (>=1.40.0,<1.41.0)"] +cognito-sync = ["mypy-boto3-cognito-sync (>=1.40.0,<1.41.0)"] +comprehend = ["mypy-boto3-comprehend (>=1.40.0,<1.41.0)"] +comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.40.0,<1.41.0)"] +compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.40.0,<1.41.0)"] +config = ["mypy-boto3-config (>=1.40.0,<1.41.0)"] +connect = ["mypy-boto3-connect (>=1.40.0,<1.41.0)"] +connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.40.0,<1.41.0)"] +connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.40.0,<1.41.0)"] +connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.40.0,<1.41.0)"] +connectcases = ["mypy-boto3-connectcases (>=1.40.0,<1.41.0)"] +connectparticipant = ["mypy-boto3-connectparticipant (>=1.40.0,<1.41.0)"] +controlcatalog = ["mypy-boto3-controlcatalog (>=1.40.0,<1.41.0)"] +controltower = ["mypy-boto3-controltower (>=1.40.0,<1.41.0)"] +cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.40.0,<1.41.0)"] +cur = ["mypy-boto3-cur (>=1.40.0,<1.41.0)"] +customer-profiles = ["mypy-boto3-customer-profiles (>=1.40.0,<1.41.0)"] +databrew = ["mypy-boto3-databrew (>=1.40.0,<1.41.0)"] +dataexchange = ["mypy-boto3-dataexchange (>=1.40.0,<1.41.0)"] +datapipeline = ["mypy-boto3-datapipeline (>=1.40.0,<1.41.0)"] +datasync = ["mypy-boto3-datasync (>=1.40.0,<1.41.0)"] +datazone = ["mypy-boto3-datazone (>=1.40.0,<1.41.0)"] +dax = ["mypy-boto3-dax (>=1.40.0,<1.41.0)"] +deadline = ["mypy-boto3-deadline (>=1.40.0,<1.41.0)"] +detective = ["mypy-boto3-detective (>=1.40.0,<1.41.0)"] +devicefarm = ["mypy-boto3-devicefarm (>=1.40.0,<1.41.0)"] +devops-guru = ["mypy-boto3-devops-guru (>=1.40.0,<1.41.0)"] +directconnect = ["mypy-boto3-directconnect (>=1.40.0,<1.41.0)"] +discovery = ["mypy-boto3-discovery (>=1.40.0,<1.41.0)"] +dlm = ["mypy-boto3-dlm (>=1.40.0,<1.41.0)"] +dms = ["mypy-boto3-dms (>=1.40.0,<1.41.0)"] +docdb = ["mypy-boto3-docdb (>=1.40.0,<1.41.0)"] +docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.40.0,<1.41.0)"] +drs = ["mypy-boto3-drs (>=1.40.0,<1.41.0)"] +ds = ["mypy-boto3-ds (>=1.40.0,<1.41.0)"] +ds-data = ["mypy-boto3-ds-data (>=1.40.0,<1.41.0)"] +dsql = ["mypy-boto3-dsql (>=1.40.0,<1.41.0)"] +dynamodb = ["mypy-boto3-dynamodb (>=1.40.0,<1.41.0)"] +dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.40.0,<1.41.0)"] +ebs = ["mypy-boto3-ebs (>=1.40.0,<1.41.0)"] +ec2 = ["mypy-boto3-ec2 (>=1.40.0,<1.41.0)"] +ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.40.0,<1.41.0)"] +ecr = ["mypy-boto3-ecr (>=1.40.0,<1.41.0)"] +ecr-public = ["mypy-boto3-ecr-public (>=1.40.0,<1.41.0)"] +ecs = ["mypy-boto3-ecs (>=1.40.0,<1.41.0)"] +efs = ["mypy-boto3-efs (>=1.40.0,<1.41.0)"] +eks = ["mypy-boto3-eks (>=1.40.0,<1.41.0)"] +eks-auth = ["mypy-boto3-eks-auth (>=1.40.0,<1.41.0)"] +elasticache = ["mypy-boto3-elasticache (>=1.40.0,<1.41.0)"] +elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.40.0,<1.41.0)"] +elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.40.0,<1.41.0)"] +elb = ["mypy-boto3-elb (>=1.40.0,<1.41.0)"] +elbv2 = ["mypy-boto3-elbv2 (>=1.40.0,<1.41.0)"] +emr = ["mypy-boto3-emr (>=1.40.0,<1.41.0)"] +emr-containers = ["mypy-boto3-emr-containers (>=1.40.0,<1.41.0)"] +emr-serverless = ["mypy-boto3-emr-serverless (>=1.40.0,<1.41.0)"] +entityresolution = ["mypy-boto3-entityresolution (>=1.40.0,<1.41.0)"] +es = ["mypy-boto3-es (>=1.40.0,<1.41.0)"] +essential = ["mypy-boto3-cloudformation (>=1.40.0,<1.41.0)", "mypy-boto3-dynamodb (>=1.40.0,<1.41.0)", "mypy-boto3-ec2 (>=1.40.0,<1.41.0)", "mypy-boto3-lambda (>=1.40.0,<1.41.0)", "mypy-boto3-rds (>=1.40.0,<1.41.0)", "mypy-boto3-s3 (>=1.40.0,<1.41.0)", "mypy-boto3-sqs (>=1.40.0,<1.41.0)"] +events = ["mypy-boto3-events (>=1.40.0,<1.41.0)"] +evidently = ["mypy-boto3-evidently (>=1.40.0,<1.41.0)"] +evs = ["mypy-boto3-evs (>=1.40.0,<1.41.0)"] +finspace = ["mypy-boto3-finspace (>=1.40.0,<1.41.0)"] +finspace-data = ["mypy-boto3-finspace-data (>=1.40.0,<1.41.0)"] +firehose = ["mypy-boto3-firehose (>=1.40.0,<1.41.0)"] +fis = ["mypy-boto3-fis (>=1.40.0,<1.41.0)"] +fms = ["mypy-boto3-fms (>=1.40.0,<1.41.0)"] +forecast = ["mypy-boto3-forecast (>=1.40.0,<1.41.0)"] +forecastquery = ["mypy-boto3-forecastquery (>=1.40.0,<1.41.0)"] +frauddetector = ["mypy-boto3-frauddetector (>=1.40.0,<1.41.0)"] +freetier = ["mypy-boto3-freetier (>=1.40.0,<1.41.0)"] +fsx = ["mypy-boto3-fsx (>=1.40.0,<1.41.0)"] +full = ["boto3-stubs-full (>=1.40.0,<1.41.0)"] +gamelift = ["mypy-boto3-gamelift (>=1.40.0,<1.41.0)"] +gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.40.0,<1.41.0)"] +geo-maps = ["mypy-boto3-geo-maps (>=1.40.0,<1.41.0)"] +geo-places = ["mypy-boto3-geo-places (>=1.40.0,<1.41.0)"] +geo-routes = ["mypy-boto3-geo-routes (>=1.40.0,<1.41.0)"] +glacier = ["mypy-boto3-glacier (>=1.40.0,<1.41.0)"] +globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.40.0,<1.41.0)"] +glue = ["mypy-boto3-glue (>=1.40.0,<1.41.0)"] +grafana = ["mypy-boto3-grafana (>=1.40.0,<1.41.0)"] +greengrass = ["mypy-boto3-greengrass (>=1.40.0,<1.41.0)"] +greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.40.0,<1.41.0)"] +groundstation = ["mypy-boto3-groundstation (>=1.40.0,<1.41.0)"] +guardduty = ["mypy-boto3-guardduty (>=1.40.0,<1.41.0)"] +health = ["mypy-boto3-health (>=1.40.0,<1.41.0)"] +healthlake = ["mypy-boto3-healthlake (>=1.40.0,<1.41.0)"] +iam = ["mypy-boto3-iam (>=1.40.0,<1.41.0)"] +identitystore = ["mypy-boto3-identitystore (>=1.40.0,<1.41.0)"] +imagebuilder = ["mypy-boto3-imagebuilder (>=1.40.0,<1.41.0)"] +importexport = ["mypy-boto3-importexport (>=1.40.0,<1.41.0)"] +inspector = ["mypy-boto3-inspector (>=1.40.0,<1.41.0)"] +inspector-scan = ["mypy-boto3-inspector-scan (>=1.40.0,<1.41.0)"] +inspector2 = ["mypy-boto3-inspector2 (>=1.40.0,<1.41.0)"] +internetmonitor = ["mypy-boto3-internetmonitor (>=1.40.0,<1.41.0)"] +invoicing = ["mypy-boto3-invoicing (>=1.40.0,<1.41.0)"] +iot = ["mypy-boto3-iot (>=1.40.0,<1.41.0)"] +iot-data = ["mypy-boto3-iot-data (>=1.40.0,<1.41.0)"] +iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.40.0,<1.41.0)"] +iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.40.0,<1.41.0)"] +iotanalytics = ["mypy-boto3-iotanalytics (>=1.40.0,<1.41.0)"] +iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.40.0,<1.41.0)"] +iotevents = ["mypy-boto3-iotevents (>=1.40.0,<1.41.0)"] +iotevents-data = ["mypy-boto3-iotevents-data (>=1.40.0,<1.41.0)"] +iotfleethub = ["mypy-boto3-iotfleethub (>=1.40.0,<1.41.0)"] +iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.40.0,<1.41.0)"] +iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.40.0,<1.41.0)"] +iotsitewise = ["mypy-boto3-iotsitewise (>=1.40.0,<1.41.0)"] +iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.40.0,<1.41.0)"] +iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.40.0,<1.41.0)"] +iotwireless = ["mypy-boto3-iotwireless (>=1.40.0,<1.41.0)"] +ivs = ["mypy-boto3-ivs (>=1.40.0,<1.41.0)"] +ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.40.0,<1.41.0)"] +ivschat = ["mypy-boto3-ivschat (>=1.40.0,<1.41.0)"] +kafka = ["mypy-boto3-kafka (>=1.40.0,<1.41.0)"] +kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.40.0,<1.41.0)"] +kendra = ["mypy-boto3-kendra (>=1.40.0,<1.41.0)"] +kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.40.0,<1.41.0)"] +keyspaces = ["mypy-boto3-keyspaces (>=1.40.0,<1.41.0)"] +keyspacesstreams = ["mypy-boto3-keyspacesstreams (>=1.40.0,<1.41.0)"] +kinesis = ["mypy-boto3-kinesis (>=1.40.0,<1.41.0)"] +kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.40.0,<1.41.0)"] +kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.40.0,<1.41.0)"] +kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.40.0,<1.41.0)"] +kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.40.0,<1.41.0)"] +kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.40.0,<1.41.0)"] +kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.40.0,<1.41.0)"] +kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.40.0,<1.41.0)"] +kms = ["mypy-boto3-kms (>=1.40.0,<1.41.0)"] +lakeformation = ["mypy-boto3-lakeformation (>=1.40.0,<1.41.0)"] +lambda = ["mypy-boto3-lambda (>=1.40.0,<1.41.0)"] +launch-wizard = ["mypy-boto3-launch-wizard (>=1.40.0,<1.41.0)"] +lex-models = ["mypy-boto3-lex-models (>=1.40.0,<1.41.0)"] +lex-runtime = ["mypy-boto3-lex-runtime (>=1.40.0,<1.41.0)"] +lexv2-models = ["mypy-boto3-lexv2-models (>=1.40.0,<1.41.0)"] +lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.40.0,<1.41.0)"] +license-manager = ["mypy-boto3-license-manager (>=1.40.0,<1.41.0)"] +license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.40.0,<1.41.0)"] +license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.40.0,<1.41.0)"] +lightsail = ["mypy-boto3-lightsail (>=1.40.0,<1.41.0)"] +location = ["mypy-boto3-location (>=1.40.0,<1.41.0)"] +logs = ["mypy-boto3-logs (>=1.40.0,<1.41.0)"] +lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.40.0,<1.41.0)"] +lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.40.0,<1.41.0)"] +lookoutvision = ["mypy-boto3-lookoutvision (>=1.40.0,<1.41.0)"] +m2 = ["mypy-boto3-m2 (>=1.40.0,<1.41.0)"] +machinelearning = ["mypy-boto3-machinelearning (>=1.40.0,<1.41.0)"] +macie2 = ["mypy-boto3-macie2 (>=1.40.0,<1.41.0)"] +mailmanager = ["mypy-boto3-mailmanager (>=1.40.0,<1.41.0)"] +managedblockchain = ["mypy-boto3-managedblockchain (>=1.40.0,<1.41.0)"] +managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.40.0,<1.41.0)"] +marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.40.0,<1.41.0)"] +marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.40.0,<1.41.0)"] +marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.40.0,<1.41.0)"] +marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.40.0,<1.41.0)"] +marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.40.0,<1.41.0)"] +marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.40.0,<1.41.0)"] +mediaconnect = ["mypy-boto3-mediaconnect (>=1.40.0,<1.41.0)"] +mediaconvert = ["mypy-boto3-mediaconvert (>=1.40.0,<1.41.0)"] +medialive = ["mypy-boto3-medialive (>=1.40.0,<1.41.0)"] +mediapackage = ["mypy-boto3-mediapackage (>=1.40.0,<1.41.0)"] +mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.40.0,<1.41.0)"] +mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.40.0,<1.41.0)"] +mediastore = ["mypy-boto3-mediastore (>=1.40.0,<1.41.0)"] +mediastore-data = ["mypy-boto3-mediastore-data (>=1.40.0,<1.41.0)"] +mediatailor = ["mypy-boto3-mediatailor (>=1.40.0,<1.41.0)"] +medical-imaging = ["mypy-boto3-medical-imaging (>=1.40.0,<1.41.0)"] +memorydb = ["mypy-boto3-memorydb (>=1.40.0,<1.41.0)"] +meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.40.0,<1.41.0)"] +mgh = ["mypy-boto3-mgh (>=1.40.0,<1.41.0)"] +mgn = ["mypy-boto3-mgn (>=1.40.0,<1.41.0)"] +migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.40.0,<1.41.0)"] +migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.40.0,<1.41.0)"] +migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.40.0,<1.41.0)"] +migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.40.0,<1.41.0)"] +mpa = ["mypy-boto3-mpa (>=1.40.0,<1.41.0)"] +mq = ["mypy-boto3-mq (>=1.40.0,<1.41.0)"] +mturk = ["mypy-boto3-mturk (>=1.40.0,<1.41.0)"] +mwaa = ["mypy-boto3-mwaa (>=1.40.0,<1.41.0)"] +neptune = ["mypy-boto3-neptune (>=1.40.0,<1.41.0)"] +neptune-graph = ["mypy-boto3-neptune-graph (>=1.40.0,<1.41.0)"] +neptunedata = ["mypy-boto3-neptunedata (>=1.40.0,<1.41.0)"] +network-firewall = ["mypy-boto3-network-firewall (>=1.40.0,<1.41.0)"] +networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.40.0,<1.41.0)"] +networkmanager = ["mypy-boto3-networkmanager (>=1.40.0,<1.41.0)"] +networkmonitor = ["mypy-boto3-networkmonitor (>=1.40.0,<1.41.0)"] +notifications = ["mypy-boto3-notifications (>=1.40.0,<1.41.0)"] +notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.40.0,<1.41.0)"] +oam = ["mypy-boto3-oam (>=1.40.0,<1.41.0)"] +observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.40.0,<1.41.0)"] +odb = ["mypy-boto3-odb (>=1.40.0,<1.41.0)"] +omics = ["mypy-boto3-omics (>=1.40.0,<1.41.0)"] +opensearch = ["mypy-boto3-opensearch (>=1.40.0,<1.41.0)"] +opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.40.0,<1.41.0)"] +opsworks = ["mypy-boto3-opsworks (>=1.40.0,<1.41.0)"] +opsworkscm = ["mypy-boto3-opsworkscm (>=1.40.0,<1.41.0)"] +organizations = ["mypy-boto3-organizations (>=1.40.0,<1.41.0)"] +osis = ["mypy-boto3-osis (>=1.40.0,<1.41.0)"] +outposts = ["mypy-boto3-outposts (>=1.40.0,<1.41.0)"] +panorama = ["mypy-boto3-panorama (>=1.40.0,<1.41.0)"] +partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.40.0,<1.41.0)"] +payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.40.0,<1.41.0)"] +payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.40.0,<1.41.0)"] +pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.40.0,<1.41.0)"] +pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.40.0,<1.41.0)"] +pcs = ["mypy-boto3-pcs (>=1.40.0,<1.41.0)"] +personalize = ["mypy-boto3-personalize (>=1.40.0,<1.41.0)"] +personalize-events = ["mypy-boto3-personalize-events (>=1.40.0,<1.41.0)"] +personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.40.0,<1.41.0)"] +pi = ["mypy-boto3-pi (>=1.40.0,<1.41.0)"] +pinpoint = ["mypy-boto3-pinpoint (>=1.40.0,<1.41.0)"] +pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.40.0,<1.41.0)"] +pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.40.0,<1.41.0)"] +pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.40.0,<1.41.0)"] +pipes = ["mypy-boto3-pipes (>=1.40.0,<1.41.0)"] +polly = ["mypy-boto3-polly (>=1.40.0,<1.41.0)"] +pricing = ["mypy-boto3-pricing (>=1.40.0,<1.41.0)"] +proton = ["mypy-boto3-proton (>=1.40.0,<1.41.0)"] +qapps = ["mypy-boto3-qapps (>=1.40.0,<1.41.0)"] +qbusiness = ["mypy-boto3-qbusiness (>=1.40.0,<1.41.0)"] +qconnect = ["mypy-boto3-qconnect (>=1.40.0,<1.41.0)"] +qldb = ["mypy-boto3-qldb (>=1.40.0,<1.41.0)"] +qldb-session = ["mypy-boto3-qldb-session (>=1.40.0,<1.41.0)"] +quicksight = ["mypy-boto3-quicksight (>=1.40.0,<1.41.0)"] +ram = ["mypy-boto3-ram (>=1.40.0,<1.41.0)"] +rbin = ["mypy-boto3-rbin (>=1.40.0,<1.41.0)"] +rds = ["mypy-boto3-rds (>=1.40.0,<1.41.0)"] +rds-data = ["mypy-boto3-rds-data (>=1.40.0,<1.41.0)"] +redshift = ["mypy-boto3-redshift (>=1.40.0,<1.41.0)"] +redshift-data = ["mypy-boto3-redshift-data (>=1.40.0,<1.41.0)"] +redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.40.0,<1.41.0)"] +rekognition = ["mypy-boto3-rekognition (>=1.40.0,<1.41.0)"] +repostspace = ["mypy-boto3-repostspace (>=1.40.0,<1.41.0)"] +resiliencehub = ["mypy-boto3-resiliencehub (>=1.40.0,<1.41.0)"] +resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.40.0,<1.41.0)"] +resource-groups = ["mypy-boto3-resource-groups (>=1.40.0,<1.41.0)"] +resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.40.0,<1.41.0)"] +robomaker = ["mypy-boto3-robomaker (>=1.40.0,<1.41.0)"] +rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.40.0,<1.41.0)"] +route53 = ["mypy-boto3-route53 (>=1.40.0,<1.41.0)"] +route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.40.0,<1.41.0)"] +route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.40.0,<1.41.0)"] +route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.40.0,<1.41.0)"] +route53domains = ["mypy-boto3-route53domains (>=1.40.0,<1.41.0)"] +route53profiles = ["mypy-boto3-route53profiles (>=1.40.0,<1.41.0)"] +route53resolver = ["mypy-boto3-route53resolver (>=1.40.0,<1.41.0)"] +rum = ["mypy-boto3-rum (>=1.40.0,<1.41.0)"] +s3 = ["mypy-boto3-s3 (>=1.40.0,<1.41.0)"] +s3control = ["mypy-boto3-s3control (>=1.40.0,<1.41.0)"] +s3outposts = ["mypy-boto3-s3outposts (>=1.40.0,<1.41.0)"] +s3tables = ["mypy-boto3-s3tables (>=1.40.0,<1.41.0)"] +s3vectors = ["mypy-boto3-s3vectors (>=1.40.0,<1.41.0)"] +sagemaker = ["mypy-boto3-sagemaker (>=1.40.0,<1.41.0)"] +sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.40.0,<1.41.0)"] +sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.40.0,<1.41.0)"] +sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.40.0,<1.41.0)"] +sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.40.0,<1.41.0)"] +sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.40.0,<1.41.0)"] +sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.40.0,<1.41.0)"] +savingsplans = ["mypy-boto3-savingsplans (>=1.40.0,<1.41.0)"] +scheduler = ["mypy-boto3-scheduler (>=1.40.0,<1.41.0)"] +schemas = ["mypy-boto3-schemas (>=1.40.0,<1.41.0)"] +sdb = ["mypy-boto3-sdb (>=1.40.0,<1.41.0)"] +secretsmanager = ["mypy-boto3-secretsmanager (>=1.40.0,<1.41.0)"] +security-ir = ["mypy-boto3-security-ir (>=1.40.0,<1.41.0)"] +securityhub = ["mypy-boto3-securityhub (>=1.40.0,<1.41.0)"] +securitylake = ["mypy-boto3-securitylake (>=1.40.0,<1.41.0)"] +serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.40.0,<1.41.0)"] +service-quotas = ["mypy-boto3-service-quotas (>=1.40.0,<1.41.0)"] +servicecatalog = ["mypy-boto3-servicecatalog (>=1.40.0,<1.41.0)"] +servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.40.0,<1.41.0)"] +servicediscovery = ["mypy-boto3-servicediscovery (>=1.40.0,<1.41.0)"] +ses = ["mypy-boto3-ses (>=1.40.0,<1.41.0)"] +sesv2 = ["mypy-boto3-sesv2 (>=1.40.0,<1.41.0)"] +shield = ["mypy-boto3-shield (>=1.40.0,<1.41.0)"] +signer = ["mypy-boto3-signer (>=1.40.0,<1.41.0)"] +simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.40.0,<1.41.0)"] +sms = ["mypy-boto3-sms (>=1.40.0,<1.41.0)"] +snow-device-management = ["mypy-boto3-snow-device-management (>=1.40.0,<1.41.0)"] +snowball = ["mypy-boto3-snowball (>=1.40.0,<1.41.0)"] +sns = ["mypy-boto3-sns (>=1.40.0,<1.41.0)"] +socialmessaging = ["mypy-boto3-socialmessaging (>=1.40.0,<1.41.0)"] +sqs = ["mypy-boto3-sqs (>=1.40.0,<1.41.0)"] +ssm = ["mypy-boto3-ssm (>=1.40.0,<1.41.0)"] +ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.40.0,<1.41.0)"] +ssm-guiconnect = ["mypy-boto3-ssm-guiconnect (>=1.40.0,<1.41.0)"] +ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.40.0,<1.41.0)"] +ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.40.0,<1.41.0)"] +ssm-sap = ["mypy-boto3-ssm-sap (>=1.40.0,<1.41.0)"] +sso = ["mypy-boto3-sso (>=1.40.0,<1.41.0)"] +sso-admin = ["mypy-boto3-sso-admin (>=1.40.0,<1.41.0)"] +sso-oidc = ["mypy-boto3-sso-oidc (>=1.40.0,<1.41.0)"] +stepfunctions = ["mypy-boto3-stepfunctions (>=1.40.0,<1.41.0)"] +storagegateway = ["mypy-boto3-storagegateway (>=1.40.0,<1.41.0)"] +sts = ["mypy-boto3-sts (>=1.40.0,<1.41.0)"] +supplychain = ["mypy-boto3-supplychain (>=1.40.0,<1.41.0)"] +support = ["mypy-boto3-support (>=1.40.0,<1.41.0)"] +support-app = ["mypy-boto3-support-app (>=1.40.0,<1.41.0)"] +swf = ["mypy-boto3-swf (>=1.40.0,<1.41.0)"] +synthetics = ["mypy-boto3-synthetics (>=1.40.0,<1.41.0)"] +taxsettings = ["mypy-boto3-taxsettings (>=1.40.0,<1.41.0)"] +textract = ["mypy-boto3-textract (>=1.40.0,<1.41.0)"] +timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.40.0,<1.41.0)"] +timestream-query = ["mypy-boto3-timestream-query (>=1.40.0,<1.41.0)"] +timestream-write = ["mypy-boto3-timestream-write (>=1.40.0,<1.41.0)"] +tnb = ["mypy-boto3-tnb (>=1.40.0,<1.41.0)"] +transcribe = ["mypy-boto3-transcribe (>=1.40.0,<1.41.0)"] +transfer = ["mypy-boto3-transfer (>=1.40.0,<1.41.0)"] +translate = ["mypy-boto3-translate (>=1.40.0,<1.41.0)"] +trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.40.0,<1.41.0)"] +verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.40.0,<1.41.0)"] +voice-id = ["mypy-boto3-voice-id (>=1.40.0,<1.41.0)"] +vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.40.0,<1.41.0)"] +waf = ["mypy-boto3-waf (>=1.40.0,<1.41.0)"] +waf-regional = ["mypy-boto3-waf-regional (>=1.40.0,<1.41.0)"] +wafv2 = ["mypy-boto3-wafv2 (>=1.40.0,<1.41.0)"] +wellarchitected = ["mypy-boto3-wellarchitected (>=1.40.0,<1.41.0)"] +wisdom = ["mypy-boto3-wisdom (>=1.40.0,<1.41.0)"] +workdocs = ["mypy-boto3-workdocs (>=1.40.0,<1.41.0)"] +workmail = ["mypy-boto3-workmail (>=1.40.0,<1.41.0)"] +workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.40.0,<1.41.0)"] +workspaces = ["mypy-boto3-workspaces (>=1.40.0,<1.41.0)"] +workspaces-instances = ["mypy-boto3-workspaces-instances (>=1.40.0,<1.41.0)"] +workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.40.0,<1.41.0)"] +workspaces-web = ["mypy-boto3-workspaces-web (>=1.40.0,<1.41.0)"] +xray = ["mypy-boto3-xray (>=1.40.0,<1.41.0)"] [[package]] name = "botocore" -version = "1.38.34" +version = "1.40.39" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "botocore-1.38.34-py3-none-any.whl", hash = "sha256:95ff2c4819498e94b321c9b5ac65d02267df93ff7ce7617323b19f19ea7cb545"}, - {file = "botocore-1.38.34.tar.gz", hash = "sha256:a105f4d941f329aa72c43ddf42371ec4bee50ab3619fc1ef35d0005520219612"}, + {file = "botocore-1.40.39-py3-none-any.whl", hash = "sha256:144e0e887a9fc198c6772f660fc006028bd1a9ce5eea3caddd848db3e421bc79"}, + {file = "botocore-1.40.39.tar.gz", hash = "sha256:c6efc55cac341811ba90c693d20097db6e2ce903451d94496bccd3f672b1709d"}, ] [package.dependencies] @@ -636,18 +640,18 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.23.8)"] +crt = ["awscrt (==0.27.6)"] [[package]] name = "botocore-stubs" -version = "1.38.30" +version = "1.38.46" description = "Type annotations and code completion for botocore" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "botocore_stubs-1.38.30-py3-none-any.whl", hash = "sha256:2efb8bdf36504aff596c670d875d8f7dd15205277c15c4cea54afdba8200c266"}, - {file = "botocore_stubs-1.38.30.tar.gz", hash = "sha256:291d7bf39a316c00a8a55b7255489b02c0cea1a343482e7784e8d1e235bae995"}, + {file = "botocore_stubs-1.38.46-py3-none-any.whl", hash = "sha256:cc21d9a7dd994bdd90872db4664d817c4719b51cda8004fd507a4bf65b085a75"}, + {file = "botocore_stubs-1.38.46.tar.gz", hash = "sha256:a04e69766ab8bae338911c1897492f88d05cd489cd75f06e6eb4f135f9da8c7b"}, ] [package.dependencies] @@ -658,26 +662,26 @@ botocore = ["botocore"] [[package]] name = "cachetools" -version = "6.0.0" +version = "6.1.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "cachetools-6.0.0-py3-none-any.whl", hash = "sha256:82e73ba88f7b30228b5507dce1a1f878498fc669d972aef2dde4f3a3c24f103e"}, - {file = "cachetools-6.0.0.tar.gz", hash = "sha256:f225782b84438f828328fc2ad74346522f27e5b1440f4e9fd18b20ebfd1aa2cf"}, + {file = "cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e"}, + {file = "cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587"}, ] [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, - {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, ] [[package]] @@ -775,104 +779,91 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.2" +version = "3.4.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, ] [[package]] @@ -904,79 +895,100 @@ files = [ [[package]] name = "coverage" -version = "7.8.2" +version = "7.10.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a"}, - {file = "coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6"}, - {file = "coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3"}, - {file = "coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404"}, - {file = "coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7"}, - {file = "coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347"}, - {file = "coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9"}, - {file = "coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5"}, - {file = "coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb"}, - {file = "coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54"}, - {file = "coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a"}, - {file = "coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975"}, - {file = "coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53"}, - {file = "coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c"}, - {file = "coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99"}, - {file = "coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57"}, - {file = "coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f"}, - {file = "coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8"}, - {file = "coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223"}, - {file = "coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f"}, - {file = "coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca"}, - {file = "coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257"}, - {file = "coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050"}, - {file = "coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48"}, - {file = "coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7"}, - {file = "coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3"}, - {file = "coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7"}, - {file = "coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008"}, - {file = "coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be"}, - {file = "coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b"}, - {file = "coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199"}, - {file = "coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8"}, - {file = "coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d"}, - {file = "coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b"}, - {file = "coverage-7.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:496948261eaac5ac9cf43f5d0a9f6eb7a6d4cb3bedb2c5d294138142f5c18f2a"}, - {file = "coverage-7.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eacd2de0d30871eff893bab0b67840a96445edcb3c8fd915e6b11ac4b2f3fa6d"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b039ffddc99ad65d5078ef300e0c7eed08c270dc26570440e3ef18beb816c1ca"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e49824808d4375ede9dd84e9961a59c47f9113039f1a525e6be170aa4f5c34d"}, - {file = "coverage-7.8.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b069938961dfad881dc2f8d02b47645cd2f455d3809ba92a8a687bf513839787"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de77c3ba8bb686d1c411e78ee1b97e6e0b963fb98b1637658dd9ad2c875cf9d7"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1676628065a498943bd3f64f099bb573e08cf1bc6088bbe33cf4424e0876f4b3"}, - {file = "coverage-7.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e1a26e7e50076e35f7afafde570ca2b4d7900a491174ca357d29dece5aacee7"}, - {file = "coverage-7.8.2-cp39-cp39-win32.whl", hash = "sha256:6782a12bf76fa61ad9350d5a6ef5f3f020b57f5e6305cbc663803f2ebd0f270a"}, - {file = "coverage-7.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:1efa4166ba75ccefd647f2d78b64f53f14fb82622bc94c5a5cb0a622f50f1c9e"}, - {file = "coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837"}, - {file = "coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32"}, - {file = "coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27"}, + {file = "coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe"}, + {file = "coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00"}, + {file = "coverage-7.10.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2a90dd4505d3cc68b847ab10c5ee81822a968b5191664e8a0801778fa60459fa"}, + {file = "coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596"}, + {file = "coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5"}, + {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4"}, + {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:992f48bf35b720e174e7fae916d943599f1a66501a2710d06c5f8104e0756ee1"}, + {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb"}, + {file = "coverage-7.10.3-cp310-cp310-win32.whl", hash = "sha256:9e92fa1f2bd5a57df9d00cf9ce1eb4ef6fccca4ceabec1c984837de55329db34"}, + {file = "coverage-7.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b96524d6e4a3ce6a75c56bb15dbd08023b0ae2289c254e15b9fbdddf0c577416"}, + {file = "coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397"}, + {file = "coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85"}, + {file = "coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157"}, + {file = "coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54"}, + {file = "coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a"}, + {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84"}, + {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160"}, + {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124"}, + {file = "coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8"}, + {file = "coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117"}, + {file = "coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770"}, + {file = "coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42"}, + {file = "coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294"}, + {file = "coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7"}, + {file = "coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437"}, + {file = "coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587"}, + {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea"}, + {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613"}, + {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb"}, + {file = "coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a"}, + {file = "coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5"}, + {file = "coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571"}, + {file = "coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a"}, + {file = "coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a"}, + {file = "coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46"}, + {file = "coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4"}, + {file = "coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a"}, + {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3"}, + {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c"}, + {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21"}, + {file = "coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0"}, + {file = "coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c"}, + {file = "coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87"}, + {file = "coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84"}, + {file = "coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e"}, + {file = "coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f"}, + {file = "coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5"}, + {file = "coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8"}, + {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1"}, + {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256"}, + {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b"}, + {file = "coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e"}, + {file = "coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c"}, + {file = "coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098"}, + {file = "coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de"}, + {file = "coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8"}, + {file = "coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667"}, + {file = "coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4"}, + {file = "coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26"}, + {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a"}, + {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd"}, + {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec"}, + {file = "coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5"}, + {file = "coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833"}, + {file = "coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4"}, + {file = "coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6"}, + {file = "coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241"}, + {file = "coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e"}, + {file = "coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5"}, + {file = "coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b"}, + {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0"}, + {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1"}, + {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c"}, + {file = "coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869"}, + {file = "coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64"}, + {file = "coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35"}, + {file = "coverage-7.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da749daa7e141985487e1ff90a68315b0845930ed53dc397f4ae8f8bab25b551"}, + {file = "coverage-7.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3126fb6a47d287f461d9b1aa5d1a8c97034d1dffb4f452f2cf211289dae74ef"}, + {file = "coverage-7.10.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3da794db13cc27ca40e1ec8127945b97fab78ba548040047d54e7bfa6d442dca"}, + {file = "coverage-7.10.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4e27bebbd184ef8d1c1e092b74a2b7109dcbe2618dce6e96b1776d53b14b3fe8"}, + {file = "coverage-7.10.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fd4ee2580b9fefbd301b4f8f85b62ac90d1e848bea54f89a5748cf132782118"}, + {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6999920bdd73259ce11cabfc1307484f071ecc6abdb2ca58d98facbcefc70f16"}, + {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3623f929db885fab100cb88220a5b193321ed37e03af719efdbaf5d10b6e227"}, + {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:25b902c5e15dea056485d782e420bb84621cc08ee75d5131ecb3dbef8bd1365f"}, + {file = "coverage-7.10.3-cp39-cp39-win32.whl", hash = "sha256:f930a4d92b004b643183451fe9c8fe398ccf866ed37d172ebaccfd443a097f61"}, + {file = "coverage-7.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:08e638a93c8acba13c7842953f92a33d52d73e410329acd472280d2a21a6c0e1"}, + {file = "coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1"}, + {file = "coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619"}, ] [package.extras] @@ -984,49 +996,49 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "45.0.4" +version = "45.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main", "dev"] files = [ - {file = "cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1"}, - {file = "cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999"}, - {file = "cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750"}, - {file = "cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2"}, - {file = "cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257"}, - {file = "cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8"}, - {file = "cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad"}, - {file = "cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6"}, - {file = "cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872"}, - {file = "cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4"}, - {file = "cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97"}, - {file = "cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a77c6fb8d76e9c9f99f2f3437c1a4ac287b34eaf40997cfab1e9bd2be175ac39"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58"}, - {file = "cryptography-45.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b97737a3ffbea79eebb062eb0d67d72307195035332501722a9ca86bab9e3ab2"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4828190fb6c4bcb6ebc6331f01fe66ae838bb3bd58e753b59d4b22eb444b996c"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862"}, - {file = "cryptography-45.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bbc505d1dc469ac12a0a064214879eac6294038d6b24ae9f71faae1448a9608d"}, - {file = "cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57"}, + {file = "cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42"}, + {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05"}, + {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453"}, + {file = "cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159"}, + {file = "cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec"}, + {file = "cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016"}, + {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3"}, + {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9"}, + {file = "cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02"}, + {file = "cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043"}, + {file = "cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719"}, ] [package.dependencies] @@ -1039,19 +1051,19 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8 pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==45.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==45.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] name = "distlib" -version = "0.3.9" +version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, - {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] [[package]] @@ -1111,20 +1123,20 @@ typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "flake8" -version = "7.2.0" +version = "7.3.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "flake8-7.2.0-py2.py3-none-any.whl", hash = "sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343"}, - {file = "flake8-7.2.0.tar.gz", hash = "sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"}, + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.13.0,<2.14.0" -pyflakes = ">=3.3.0,<3.4.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" [[package]] name = "idna" @@ -1214,14 +1226,14 @@ ply = "*" [[package]] name = "jsonschema" -version = "4.24.0" +version = "4.25.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d"}, - {file = "jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196"}, + {file = "jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716"}, + {file = "jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f"}, ] [package.dependencies] @@ -1232,7 +1244,7 @@ rpds-py = ">=0.7.1" [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] name = "jsonschema-path" @@ -1376,14 +1388,14 @@ files = [ [[package]] name = "moto" -version = "5.1.6" +version = "5.1.10" description = "A library that allows you to easily mock out tests based on AWS infrastructure" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "moto-5.1.6-py3-none-any.whl", hash = "sha256:e4a3092bc8fe9139caa77cd34cdcbad804de4d9671e2270ea3b4d53f5c645047"}, - {file = "moto-5.1.6.tar.gz", hash = "sha256:baf7afa9d4a92f07277b29cf466d0738f25db2ed2ee12afcb1dc3f2c540beebd"}, + {file = "moto-5.1.10-py3-none-any.whl", hash = "sha256:9ec1a21a924f97470af225b2bfa854fe46c1ad30fb44655eba458206dedf28b5"}, + {file = "moto-5.1.10.tar.gz", hash = "sha256:d6bdc8f82a1e503502927cc0a3da22014f836094d0bf399bb0f695754ae6c7a6"}, ] [package.dependencies] @@ -1425,44 +1437,50 @@ xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] [[package]] name = "mypy" -version = "1.16.0" +version = "1.17.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, - {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, - {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"}, - {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"}, - {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"}, - {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"}, - {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"}, - {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"}, - {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"}, - {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"}, - {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"}, - {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"}, - {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"}, - {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"}, - {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"}, - {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"}, - {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"}, - {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"}, - {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"}, - {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"}, - {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"}, - {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"}, - {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"}, - {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"}, - {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"}, - {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"}, - {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"}, - {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"}, - {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"}, - {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"}, - {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"}, - {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"}, + {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, + {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, + {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, + {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, + {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, + {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, + {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, + {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, + {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, + {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, + {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, + {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, + {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, + {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, + {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, + {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, ] [package.dependencies] @@ -1479,14 +1497,14 @@ reports = ["lxml"] [[package]] name = "mypy-boto3-cloudformation" -version = "1.38.31" -description = "Type annotations for boto3 CloudFormation 1.38.31 service generated with mypy-boto3-builder 8.11.0" +version = "1.40.0" +description = "Type annotations for boto3 CloudFormation 1.40.0 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_cloudformation-1.38.31-py3-none-any.whl", hash = "sha256:1016508783c1263aba9bb24dd29afbea6f0c8c7cee79e9d073c4ed5524ce53f5"}, - {file = "mypy_boto3_cloudformation-1.38.31.tar.gz", hash = "sha256:f4185231faab97bfb50b25dc1323333c630a092ffa8c15356f21116fc92a7f42"}, + {file = "mypy_boto3_cloudformation-1.40.0-py3-none-any.whl", hash = "sha256:3daa2b10307f4763cb9479e541b1d45742a79a3c598f1a577389c5735fa8ad10"}, + {file = "mypy_boto3_cloudformation-1.40.0.tar.gz", hash = "sha256:a0beaae56355fb3e5eb4439d65a919a9e61f6ea2f69ffbf0a03fd6b45ad895f0"}, ] [package.dependencies] @@ -1494,14 +1512,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-cloudfront" -version = "1.38.12" -description = "Type annotations for boto3 CloudFront 1.38.12 service generated with mypy-boto3-builder 8.11.0" +version = "1.40.5" +description = "Type annotations for boto3 CloudFront 1.40.5 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_cloudfront-1.38.12-py3-none-any.whl", hash = "sha256:e20cbf4ec3e607b7b0fd74e21e903dd51769ce002a00b33e1ab49d347b224743"}, - {file = "mypy_boto3_cloudfront-1.38.12.tar.gz", hash = "sha256:da294f2032b56dd3249faf4f5ecd90e075217c6ebb5d68cf5991cfafd6725efb"}, + {file = "mypy_boto3_cloudfront-1.40.5-py3-none-any.whl", hash = "sha256:7ab2ee3453ece2c8060d563649b4333a8e1b7ec509e58f97862d2b16e27b4ebc"}, + {file = "mypy_boto3_cloudfront-1.40.5.tar.gz", hash = "sha256:bee44131593a810fa67cd2c6fd057f123bf07d7de6337b6d80aff352c8b48210"}, ] [package.dependencies] @@ -1509,14 +1527,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-cloudwatch" -version = "1.38.21" -description = "Type annotations for boto3 CloudWatch 1.38.21 service generated with mypy-boto3-builder 8.11.0" +version = "1.40.0" +description = "Type annotations for boto3 CloudWatch 1.40.0 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_cloudwatch-1.38.21-py3-none-any.whl", hash = "sha256:96a014b3ccbc2cd77915fd832368506f77f63f57a1e528b4b270321df78c911b"}, - {file = "mypy_boto3_cloudwatch-1.38.21.tar.gz", hash = "sha256:d9f273a05a0434d7a5294ce81f3d45df46b3aafec3aee8d0b065a8216a290076"}, + {file = "mypy_boto3_cloudwatch-1.40.0-py3-none-any.whl", hash = "sha256:5be89084cfeed6d5bfc34b27b4312010e60e5d69cd584df57272acb122e5080f"}, + {file = "mypy_boto3_cloudwatch-1.40.0.tar.gz", hash = "sha256:49b10a6c65e392f93e8c85d01d3a138fecb38545f5a1bf15cd3e1ac1b594016b"}, ] [package.dependencies] @@ -1524,14 +1542,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-ec2" -version = "1.38.33" -description = "Type annotations for boto3 EC2 1.38.33 service generated with mypy-boto3-builder 8.11.0" +version = "1.40.8" +description = "Type annotations for boto3 EC2 1.40.8 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_ec2-1.38.33-py3-none-any.whl", hash = "sha256:9750403a6ad135e676ecc280acee9f0d7762f46c5bc8d864ec7c2ad3ef6118b7"}, - {file = "mypy_boto3_ec2-1.38.33.tar.gz", hash = "sha256:5d07fc10d5f682f8570000ba53bdec00ee498171509943877451c2cccbc341a3"}, + {file = "mypy_boto3_ec2-1.40.8-py3-none-any.whl", hash = "sha256:d440ace579ef2f7d43e8255cccbc2936b210d24de1c77525d491c9043c47d609"}, + {file = "mypy_boto3_ec2-1.40.8.tar.gz", hash = "sha256:e2aa0103589a8c4841d28bdee3989450476595c19db3a634f0d8603d44bd4e64"}, ] [package.dependencies] @@ -1539,14 +1557,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-iam" -version = "1.38.14" -description = "Type annotations for boto3 IAM 1.38.14 service generated with mypy-boto3-builder 8.11.0" +version = "1.40.0" +description = "Type annotations for boto3 IAM 1.40.0 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_iam-1.38.14-py3-none-any.whl", hash = "sha256:f03e8f029cb00a0a66389dbf809371c88420fd119395c8464b3f48254b38c17a"}, - {file = "mypy_boto3_iam-1.38.14.tar.gz", hash = "sha256:4692200074bf917da7c9237b2c50bbb9718931c9f99b73e579ecdd100b6582a3"}, + {file = "mypy_boto3_iam-1.40.0-py3-none-any.whl", hash = "sha256:46e354287c93b4f84eef2407076f56cb42e504336eaec114641b1b184808fae8"}, + {file = "mypy_boto3_iam-1.40.0.tar.gz", hash = "sha256:b900ac557375428f0bbc37aa24fdd2901e2db7018ae44e0aaf99ec0f84a28d44"}, ] [package.dependencies] @@ -1554,14 +1572,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-s3" -version = "1.38.26" -description = "Type annotations for boto3 S3 1.38.26 service generated with mypy-boto3-builder 8.11.0" +version = "1.40.0" +description = "Type annotations for boto3 S3 1.40.0 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_s3-1.38.26-py3-none-any.whl", hash = "sha256:1129d64be1aee863e04f0c92ac8d315578f13ccae64fa199b20ad0950d2b9616"}, - {file = "mypy_boto3_s3-1.38.26.tar.gz", hash = "sha256:38a45dee5782d5c07ddea07ea50965c4d2ba7e77617c19f613b4c9f80f961b52"}, + {file = "mypy_boto3_s3-1.40.0-py3-none-any.whl", hash = "sha256:5736b7780d57a156312d8d136462c207671d0236b0355704b5754496bb712bc8"}, + {file = "mypy_boto3_s3-1.40.0.tar.gz", hash = "sha256:99a4a27f04d62fe0b31032f274f2e19889fa66424413617a9416873c48567f1d"}, ] [package.dependencies] @@ -1569,14 +1587,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-sns" -version = "1.38.0" -description = "Type annotations for boto3 SNS 1.38.0 service generated with mypy-boto3-builder 8.10.1" +version = "1.40.1" +description = "Type annotations for boto3 SNS 1.40.1 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_sns-1.38.0-py3-none-any.whl", hash = "sha256:e9da0864fe8463a390a0b83c3731830c7aabe98baac61091f06442fcb796186d"}, - {file = "mypy_boto3_sns-1.38.0.tar.gz", hash = "sha256:0e7cbec9c591db0e3c5acbe3ad3c47dfb0e58ef96c13aad49c27c9fdea3d4628"}, + {file = "mypy_boto3_sns-1.40.1-py3-none-any.whl", hash = "sha256:538920699f461b6f142b6dd36492b6a27c2113410ed427422122c7da9d2c921a"}, + {file = "mypy_boto3_sns-1.40.1.tar.gz", hash = "sha256:e06d89db10c83364096365c630a144d59ca3e0fdb663bbd6b73bd1816d1e53db"}, ] [package.dependencies] @@ -1584,14 +1602,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-ssm" -version = "1.38.5" -description = "Type annotations for boto3 SSM 1.38.5 service generated with mypy-boto3-builder 8.10.1" +version = "1.40.0" +description = "Type annotations for boto3 SSM 1.40.0 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_ssm-1.38.5-py3-none-any.whl", hash = "sha256:1bb0f932bee9038a53ab02781f959fc553a5d7f5e9d7cba56f998d0eb0a5878f"}, - {file = "mypy_boto3_ssm-1.38.5.tar.gz", hash = "sha256:e95bbad7d2f6b4849bc946eb9bbcc1f134cbdaafb172c365bedecdb3104eee0e"}, + {file = "mypy_boto3_ssm-1.40.0-py3-none-any.whl", hash = "sha256:9f7d03feac4d5eb3e551871d49814994a216539845e5a223ea3f6c17945bcf05"}, + {file = "mypy_boto3_ssm-1.40.0.tar.gz", hash = "sha256:4a656240ead29ffcfb28e95ce7c7ab6c9bbad71bbe7ce81f328ff9b214ff114b"}, ] [package.dependencies] @@ -1599,14 +1617,14 @@ typing-extensions = {version = "*", markers = "python_version < \"3.12\""} [[package]] name = "mypy-boto3-sts" -version = "1.38.0" -description = "Type annotations for boto3 STS 1.38.0 service generated with mypy-boto3-builder 8.10.1" +version = "1.40.0" +description = "Type annotations for boto3 STS 1.40.0 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "mypy_boto3_sts-1.38.0-py3-none-any.whl", hash = "sha256:61d7ef65677be52cc0e369e359c41ced12b4cc3919f550905af5f480806b89b4"}, - {file = "mypy_boto3_sts-1.38.0.tar.gz", hash = "sha256:143a96f06bd17ec4bbb120e04b65e646cb4345e2d0d4c3c596f8aa0458d12707"}, + {file = "mypy_boto3_sts-1.40.0-py3-none-any.whl", hash = "sha256:fff731694cab2474bf7e7d3344b049b4ac0272d76b72fc4f7e4ae543ead8a5e6"}, + {file = "mypy_boto3_sts-1.40.0.tar.gz", hash = "sha256:eb55e50960ae6194d09488464c302196392df712a6a5f4308b6a0e24244cfd5c"}, ] [package.dependencies] @@ -1757,14 +1775,14 @@ dev = ["black (==22.6.0)", "flake8", "mypy", "pytest"] [[package]] name = "pycodestyle" -version = "2.13.0" +version = "2.14.0" description = "Python style guide checker" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, - {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, ] [[package]] @@ -1782,14 +1800,14 @@ files = [ [[package]] name = "pydantic" -version = "2.11.5" +version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, - {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] @@ -1914,52 +1932,28 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pydantic-settings" -version = "2.9.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"}, - {file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" -typing-inspection = ">=0.4.0" - -[package.extras] -aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - [[package]] name = "pyflakes" -version = "3.3.2" +version = "3.4.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a"}, - {file = "pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"}, + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -1986,14 +1980,14 @@ testing = ["covdefaults (>=2.3)", "pytest (>=8.3.5)", "pytest-cov (>=6.1.1)", "p [[package]] name = "pytest" -version = "8.4.0" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, - {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] @@ -2076,46 +2070,35 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "python-dotenv" -version = "1.1.0" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, - {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - [[package]] name = "pywin32" -version = "310" +version = "311" description = "Python for Window Extensions" optional = false python-versions = "*" groups = ["dev"] markers = "sys_platform == \"win32\"" files = [ - {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, - {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, - {file = "pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213"}, - {file = "pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd"}, - {file = "pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c"}, - {file = "pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582"}, - {file = "pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d"}, - {file = "pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060"}, - {file = "pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966"}, - {file = "pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab"}, - {file = "pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e"}, - {file = "pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33"}, - {file = "pywin32-310-cp38-cp38-win32.whl", hash = "sha256:0867beb8addefa2e3979d4084352e4ac6e991ca45373390775f7084cc0209b9c"}, - {file = "pywin32-310-cp38-cp38-win_amd64.whl", hash = "sha256:30f0a9b3138fb5e07eb4973b7077e1883f558e40c578c6925acc7a94c34eaa36"}, - {file = "pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a"}, - {file = "pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475"}, + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, ] [[package]] @@ -2222,14 +2205,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" -version = "0.25.7" +version = "0.25.8" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c"}, - {file = "responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb"}, + {file = "responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c"}, + {file = "responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4"}, ] [package.dependencies] @@ -2257,141 +2240,179 @@ six = "*" [[package]] name = "rpds-py" -version = "0.25.1" +version = "0.27.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "rpds_py-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f4ad628b5174d5315761b67f212774a32f5bad5e61396d38108bd801c0a8f5d9"}, - {file = "rpds_py-0.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c742af695f7525e559c16f1562cf2323db0e3f0fbdcabdf6865b095256b2d40"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:605ffe7769e24b1800b4d024d24034405d9404f0bc2f55b6db3362cd34145a6f"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccc6f3ddef93243538be76f8e47045b4aad7a66a212cd3a0f23e34469473d36b"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f70316f760174ca04492b5ab01be631a8ae30cadab1d1081035136ba12738cfa"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1dafef8df605fdb46edcc0bf1573dea0d6d7b01ba87f85cd04dc855b2b4479e"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0701942049095741a8aeb298a31b203e735d1c61f4423511d2b1a41dcd8a16da"}, - {file = "rpds_py-0.25.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e87798852ae0b37c88babb7f7bbbb3e3fecc562a1c340195b44c7e24d403e380"}, - {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3bcce0edc1488906c2d4c75c94c70a0417e83920dd4c88fec1078c94843a6ce9"}, - {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e2f6a2347d3440ae789505693a02836383426249d5293541cd712e07e7aecf54"}, - {file = "rpds_py-0.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4fd52d3455a0aa997734f3835cbc4c9f32571345143960e7d7ebfe7b5fbfa3b2"}, - {file = "rpds_py-0.25.1-cp310-cp310-win32.whl", hash = "sha256:3f0b1798cae2bbbc9b9db44ee068c556d4737911ad53a4e5093d09d04b3bbc24"}, - {file = "rpds_py-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3ebd879ab996537fc510a2be58c59915b5dd63bccb06d1ef514fee787e05984a"}, - {file = "rpds_py-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5f048bbf18b1f9120685c6d6bb70cc1a52c8cc11bdd04e643d28d3be0baf666d"}, - {file = "rpds_py-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fbb0dbba559959fcb5d0735a0f87cdbca9e95dac87982e9b95c0f8f7ad10255"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ca54b9cf9d80b4016a67a0193ebe0bcf29f6b0a96f09db942087e294d3d4c2"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ee3e26eb83d39b886d2cb6e06ea701bba82ef30a0de044d34626ede51ec98b0"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89706d0683c73a26f76a5315d893c051324d771196ae8b13e6ffa1ffaf5e574f"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2013ee878c76269c7b557a9a9c042335d732e89d482606990b70a839635feb7"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45e484db65e5380804afbec784522de84fa95e6bb92ef1bd3325d33d13efaebd"}, - {file = "rpds_py-0.25.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48d64155d02127c249695abb87d39f0faf410733428d499867606be138161d65"}, - {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:048893e902132fd6548a2e661fb38bf4896a89eea95ac5816cf443524a85556f"}, - {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d"}, - {file = "rpds_py-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffcf57826d77a4151962bf1701374e0fc87f536e56ec46f1abdd6a903354042"}, - {file = "rpds_py-0.25.1-cp311-cp311-win32.whl", hash = "sha256:cda776f1967cb304816173b30994faaf2fd5bcb37e73118a47964a02c348e1bc"}, - {file = "rpds_py-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc3c1ff0abc91444cd20ec643d0f805df9a3661fcacf9c95000329f3ddf268a4"}, - {file = "rpds_py-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:5a3ddb74b0985c4387719fc536faced33cadf2172769540c62e2a94b7b9be1c4"}, - {file = "rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c"}, - {file = "rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb"}, - {file = "rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe"}, - {file = "rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192"}, - {file = "rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728"}, - {file = "rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559"}, - {file = "rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd"}, - {file = "rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31"}, - {file = "rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500"}, - {file = "rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5"}, - {file = "rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129"}, - {file = "rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66"}, - {file = "rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523"}, - {file = "rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763"}, - {file = "rpds_py-0.25.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ce4c8e485a3c59593f1a6f683cf0ea5ab1c1dc94d11eea5619e4fb5228b40fbd"}, - {file = "rpds_py-0.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8222acdb51a22929c3b2ddb236b69c59c72af4019d2cba961e2f9add9b6e634"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4593c4eae9b27d22df41cde518b4b9e4464d139e4322e2127daa9b5b981b76be"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd035756830c712b64725a76327ce80e82ed12ebab361d3a1cdc0f51ea21acb0"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:114a07e85f32b125404f28f2ed0ba431685151c037a26032b213c882f26eb908"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dec21e02e6cc932538b5203d3a8bd6aa1480c98c4914cb88eea064ecdbc6396a"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09eab132f41bf792c7a0ea1578e55df3f3e7f61888e340779b06050a9a3f16e9"}, - {file = "rpds_py-0.25.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c98f126c4fc697b84c423e387337d5b07e4a61e9feac494362a59fd7a2d9ed80"}, - {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0e6a327af8ebf6baba1c10fadd04964c1965d375d318f4435d5f3f9651550f4a"}, - {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc120d1132cff853ff617754196d0ac0ae63befe7c8498bd67731ba368abe451"}, - {file = "rpds_py-0.25.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:140f61d9bed7839446bdd44852e30195c8e520f81329b4201ceead4d64eb3a9f"}, - {file = "rpds_py-0.25.1-cp39-cp39-win32.whl", hash = "sha256:9c006f3aadeda131b438c3092124bd196b66312f0caa5823ef09585a669cf449"}, - {file = "rpds_py-0.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:a61d0b2c7c9a0ae45732a77844917b427ff16ad5464b4d4f5e4adb955f582890"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b24bf3cd93d5b6ecfbedec73b15f143596c88ee249fa98cefa9a9dc9d92c6f28"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0eb90e94f43e5085623932b68840b6f379f26db7b5c2e6bcef3179bd83c9330f"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d50e4864498a9ab639d6d8854b25e80642bd362ff104312d9770b05d66e5fb13"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c9409b47ba0650544b0bb3c188243b83654dfe55dcc173a86832314e1a6a35d"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:796ad874c89127c91970652a4ee8b00d56368b7e00d3477f4415fe78164c8000"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85608eb70a659bf4c1142b2781083d4b7c0c4e2c90eff11856a9754e965b2540"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4feb9211d15d9160bc85fa72fed46432cdc143eb9cf6d5ca377335a921ac37b"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccfa689b9246c48947d31dd9d8b16d89a0ecc8e0e26ea5253068efb6c542b76e"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3c5b317ecbd8226887994852e85de562f7177add602514d4ac40f87de3ae45a8"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:454601988aab2c6e8fd49e7634c65476b2b919647626208e376afcd22019eeb8"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1c0c434a53714358532d13539272db75a5ed9df75a4a090a753ac7173ec14e11"}, - {file = "rpds_py-0.25.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f73ce1512e04fbe2bc97836e89830d6b4314c171587a99688082d090f934d20a"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee86d81551ec68a5c25373c5643d343150cc54672b5e9a0cafc93c1870a53954"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89c24300cd4a8e4a51e55c31a8ff3918e6651b241ee8876a42cc2b2a078533ba"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:771c16060ff4e79584dc48902a91ba79fd93eade3aa3a12d6d2a4aadaf7d542b"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785ffacd0ee61c3e60bdfde93baa6d7c10d86f15655bd706c89da08068dc5038"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a40046a529cc15cef88ac5ab589f83f739e2d332cb4d7399072242400ed68c9"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85fc223d9c76cabe5d0bff82214459189720dc135db45f9f66aa7cffbf9ff6c1"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0be9965f93c222fb9b4cc254235b3b2b215796c03ef5ee64f995b1b69af0762"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8378fa4a940f3fb509c081e06cb7f7f2adae8cf46ef258b0e0ed7519facd573e"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:33358883a4490287e67a2c391dfaea4d9359860281db3292b6886bf0be3d8692"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1d1fadd539298e70cac2f2cb36f5b8a65f742b9b9f1014dd4ea1f7785e2470bf"}, - {file = "rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a46c2fb2545e21181445515960006e85d22025bd2fe6db23e76daec6eb689fe"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:50f2c501a89c9a5f4e454b126193c5495b9fb441a75b298c60591d8a2eb92e1b"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d779b325cc8238227c47fbc53964c8cc9a941d5dbae87aa007a1f08f2f77b23"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:036ded36bedb727beeabc16dc1dad7cb154b3fa444e936a03b67a86dc6a5066e"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245550f5a1ac98504147cba96ffec8fabc22b610742e9150138e5d60774686d7"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff7c23ba0a88cb7b104281a99476cccadf29de2a0ef5ce864959a52675b1ca83"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e37caa8cdb3b7cf24786451a0bdb853f6347b8b92005eeb64225ae1db54d1c2b"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2f48ab00181600ee266a095fe815134eb456163f7d6699f525dee471f312cf"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e5fc7484fa7dce57e25063b0ec9638ff02a908304f861d81ea49273e43838c1"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d3c10228d6cf6fe2b63d2e7985e94f6916fa46940df46b70449e9ff9297bd3d1"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:5d9e40f32745db28c1ef7aad23f6fc458dc1e29945bd6781060f0d15628b8ddf"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:35a8d1a24b5936b35c5003313bc177403d8bdef0f8b24f28b1c4a255f94ea992"}, - {file = "rpds_py-0.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6099263f526efff9cf3883dfef505518730f7a7a93049b1d90d42e50a22b4793"}, - {file = "rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3"}, + {file = "rpds_py-0.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:130c1ffa5039a333f5926b09e346ab335f0d4ec393b030a18549a7c7e7c2cea4"}, + {file = "rpds_py-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a4cf32a26fa744101b67bfd28c55d992cd19438aff611a46cac7f066afca8fd4"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64a0fe3f334a40b989812de70160de6b0ec7e3c9e4a04c0bbc48d97c5d3600ae"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a0ff7ee28583ab30a52f371b40f54e7138c52ca67f8ca17ccb7ccf0b383cb5f"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ea4d2e182345dd1b4286593601d766411b43f868924afe297570658c31a62b"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36184b44bf60a480863e51021c26aca3dfe8dd2f5eeabb33622b132b9d8b8b54"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b78430703cfcf5f5e86eb74027a1ed03a93509273d7c705babb547f03e60016"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:dbd749cff1defbde270ca346b69b3baf5f1297213ef322254bf2a28537f0b046"}, + {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bde37765564cd22a676dd8101b657839a1854cfaa9c382c5abf6ff7accfd4ae"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1d66f45b9399036e890fb9c04e9f70c33857fd8f58ac8db9f3278cfa835440c3"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d85d784c619370d9329bbd670f41ff5f2ae62ea4519761b679d0f57f0f0ee267"}, + {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5df559e9e7644d9042f626f2c3997b555f347d7a855a15f170b253f6c5bfe358"}, + {file = "rpds_py-0.27.0-cp310-cp310-win32.whl", hash = "sha256:b8a4131698b6992b2a56015f51646711ec5d893a0b314a4b985477868e240c87"}, + {file = "rpds_py-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbc619e84a5e3ab2d452de831c88bdcad824414e9c2d28cd101f94dbdf26329c"}, + {file = "rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622"}, + {file = "rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85"}, + {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626"}, + {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e"}, + {file = "rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7"}, + {file = "rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261"}, + {file = "rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0"}, + {file = "rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4"}, + {file = "rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3"}, + {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03"}, + {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374"}, + {file = "rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97"}, + {file = "rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5"}, + {file = "rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9"}, + {file = "rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff"}, + {file = "rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295"}, + {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b"}, + {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d"}, + {file = "rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd"}, + {file = "rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2"}, + {file = "rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac"}, + {file = "rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774"}, + {file = "rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858"}, + {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79"}, + {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c"}, + {file = "rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23"}, + {file = "rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1"}, + {file = "rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb"}, + {file = "rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51"}, + {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e"}, + {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e"}, + {file = "rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6"}, + {file = "rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a"}, + {file = "rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d"}, + {file = "rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828"}, + {file = "rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156"}, + {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42"}, + {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae"}, + {file = "rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5"}, + {file = "rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391"}, + {file = "rpds_py-0.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e0d7151a1bd5d0a203a5008fc4ae51a159a610cb82ab0a9b2c4d80241745582e"}, + {file = "rpds_py-0.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42ccc57ff99166a55a59d8c7d14f1a357b7749f9ed3584df74053fd098243451"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e377e4cf8795cdbdff75b8f0223d7b6c68ff4fef36799d88ccf3a995a91c0112"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79af163a4b40bbd8cfd7ca86ec8b54b81121d3b213b4435ea27d6568bcba3e9d"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2eff8ee57c5996b0d2a07c3601fb4ce5fbc37547344a26945dd9e5cbd1ed27a"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7cf9bc4508efb18d8dff6934b602324eb9f8c6644749627ce001d6f38a490889"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05284439ebe7d9f5f5a668d4d8a0a1d851d16f7d47c78e1fab968c8ad30cab04"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:1321bce595ad70e80f97f998db37356b2e22cf98094eba6fe91782e626da2f71"}, + {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:737005088449ddd3b3df5a95476ee1c2c5c669f5c30eed909548a92939c0e12d"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b2a4e17bfd68536c3b801800941c95a1d4a06e3cada11c146093ba939d9638d"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dc6b0d5a1ea0318ef2def2b6a55dccf1dcaf77d605672347271ed7b829860765"}, + {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c3f8a0d4802df34fcdbeb3dfe3a4d8c9a530baea8fafdf80816fcaac5379d83"}, + {file = "rpds_py-0.27.0-cp39-cp39-win32.whl", hash = "sha256:699c346abc73993962cac7bb4f02f58e438840fa5458a048d3a178a7a670ba86"}, + {file = "rpds_py-0.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:be806e2961cd390a89d6c3ce8c2ae34271cfcd05660f716257838bb560f1c3b6"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:46f48482c1a4748ab2773f75fffbdd1951eb59794e32788834b945da857c47a8"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:419dd9c98bcc9fb0242be89e0c6e922df333b975d4268faa90d58499fd9c9ebe"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d42a0ef2bdf6bc81e1cc2d49d12460f63c6ae1423c4f4851b828e454ccf6f1"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e39169ac6aae06dd79c07c8a69d9da867cef6a6d7883a0186b46bb46ccfb0c3"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:935afcdea4751b0ac918047a2df3f720212892347767aea28f5b3bf7be4f27c0"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de567dec6d451649a781633d36f5c7501711adee329d76c095be2178855b042"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:555ed147cbe8c8f76e72a4c6cd3b7b761cbf9987891b9448808148204aed74a5"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:d2cc2b34f9e1d31ce255174da82902ad75bd7c0d88a33df54a77a22f2ef421ee"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb0702c12983be3b2fab98ead349ac63a98216d28dda6f518f52da5498a27a1b"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ba783541be46f27c8faea5a6645e193943c17ea2f0ffe593639d906a327a9bcc"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:2406d034635d1497c596c40c85f86ecf2bf9611c1df73d14078af8444fe48031"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dea0808153f1fbbad772669d906cddd92100277533a03845de6893cadeffc8be"}, + {file = "rpds_py-0.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2a81bdcfde4245468f7030a75a37d50400ac2455c3a4819d9d550c937f90ab5"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114"}, + {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9ad08547995a57e74fea6abaf5940d399447935faebbd2612b3b0ca6f987946b"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:61490d57e82e23b45c66f96184237994bfafa914433b8cd1a9bb57fecfced59d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7cf5e726b6fa977e428a61880fb108a62f28b6d0c7ef675b117eaff7076df49"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc662bc9375a6a394b62dfd331874c434819f10ee3902123200dbcf116963f89"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:299a245537e697f28a7511d01038c310ac74e8ea213c0019e1fc65f52c0dcb23"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be3964f7312ea05ed283b20f87cb533fdc555b2e428cc7be64612c0b2124f08c"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ba649a6e55ae3808e4c39e01580dc9a9b0d5b02e77b66bb86ef117922b1264"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:81f81bbd7cdb4bdc418c09a73809abeda8f263a6bf8f9c7f93ed98b5597af39d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11e8e28c0ba0373d052818b600474cfee2fafa6c9f36c8587d217b13ee28ca7d"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e3acb9c16530362aeaef4e84d57db357002dc5cbfac9a23414c3e73c08301ab2"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2e307cb5f66c59ede95c00e93cd84190a5b7f3533d7953690b2036780622ba81"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f09c9d4c26fa79c1bad927efb05aca2391350b8e61c38cbc0d7d3c814e463124"}, + {file = "rpds_py-0.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af22763a0a1eff106426a6e1f13c4582e0d0ad89c1493ab6c058236174cd6c6a"}, + {file = "rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f"}, ] [[package]] name = "s3transfer" -version = "0.13.0" +version = "0.14.0" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be"}, - {file = "s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177"}, + {file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"}, + {file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"}, ] [package.dependencies] @@ -2414,40 +2435,37 @@ files = [ [[package]] name = "tox" -version = "4.26.0" +version = "4.28.4" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "tox-4.26.0-py3-none-any.whl", hash = "sha256:75f17aaf09face9b97bd41645028d9f722301e912be8b4c65a3f938024560224"}, - {file = "tox-4.26.0.tar.gz", hash = "sha256:a83b3b67b0159fa58e44e646505079e35a43317a62d2ae94725e0586266faeca"}, + {file = "tox-4.28.4-py3-none-any.whl", hash = "sha256:8d4ad9ee916ebbb59272bb045e154a10fa12e3bbdcf94cc5185cbdaf9b241f99"}, + {file = "tox-4.28.4.tar.gz", hash = "sha256:b5b14c6307bd8994ff1eba5074275826620325ee1a4f61316959d562bfd70b9d"}, ] [package.dependencies] -cachetools = ">=5.5.1" +cachetools = ">=6.1" chardet = ">=5.2" colorama = ">=0.4.6" -filelock = ">=3.16.1" -packaging = ">=24.2" -platformdirs = ">=4.3.6" -pluggy = ">=1.5" -pyproject-api = ">=1.8" -virtualenv = ">=20.31" - -[package.extras] -test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.4)", "pytest-mock (>=3.14)"] +filelock = ">=3.18" +packaging = ">=25" +platformdirs = ">=4.3.8" +pluggy = ">=1.6" +pyproject-api = ">=1.9.1" +virtualenv = ">=20.31.2" [[package]] name = "types-awscrt" -version = "0.27.2" +version = "0.27.6" description = "Type annotations and code completion for awscrt" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "types_awscrt-0.27.2-py3-none-any.whl", hash = "sha256:49a045f25bbd5ad2865f314512afced933aed35ddbafc252e2268efa8a787e4e"}, - {file = "types_awscrt-0.27.2.tar.gz", hash = "sha256:acd04f57119eb15626ab0ba9157fc24672421de56e7bd7b9f61681fedee44e91"}, + {file = "types_awscrt-0.27.6-py3-none-any.whl", hash = "sha256:18aced46da00a57f02eb97637a32e5894dc5aa3dc6a905ba3e5ed85b9f3c526b"}, + {file = "types_awscrt-0.27.6.tar.gz", hash = "sha256:9d3f1865a93b8b2c32f137514ac88cb048b5bc438739945ba19d972698995bfb"}, ] [[package]] @@ -2476,14 +2494,14 @@ files = [ [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.14.1" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, - {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] [[package]] @@ -2521,14 +2539,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.31.2" +version = "20.33.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, - {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, + {file = "virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67"}, + {file = "virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8"}, ] [package.dependencies] @@ -2560,91 +2578,93 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "wrapt" -version = "1.17.2" +version = "1.17.3" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, - {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, - {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, - {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, - {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, - {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, - {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, - {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, - {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, - {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, - {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, - {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, - {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, - {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, - {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, - {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, - {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, + {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, + {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, + {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, + {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, + {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, + {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, + {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, + {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, + {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, + {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, + {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, + {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, + {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, + {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, + {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, + {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, + {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, + {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, + {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, + {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, + {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, ] [[package]] @@ -2662,4 +2682,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "07f97dc681eaed6facb669244dde2b165d6eefb3b924e9c8f37ead86e399bdad" +content-hash = "58e175d052afab613c1393dbc3c32dc1696a312fa591229e8e08104b071b202a" diff --git a/deployment/pyproject.toml b/deployment/pyproject.toml index a4fac7f6..f2bdaa1c 100644 --- a/deployment/pyproject.toml +++ b/deployment/pyproject.toml @@ -3,29 +3,30 @@ name = "automated_security_response_on_aws" package-mode = false [tool.poetry.dependencies] -aws-lambda-powertools = {extras = ["all"], version = "^3.1.0"} +aws-lambda-powertools = {extras = ["all"], version = "3.1.0"} python = "^3.11" +boto3 = "1.40.39" [tool.poetry.group.dev.dependencies] python = "^3.11" -black = "^24.10.0" -flake8 = "^7.1.1" -isort = "^5.13.2" -mypy = "^1.11.2" -pytest = "^8.3.3" -pytest-cov = "^5.0.0" -pytest-env = "^1.1.5" -pytest-mock = "^3.14.0" -werkzeug = "^3.0.6" -tox = "^4.21.2" -boto3-stubs-lite = { extras = ["cloudfront", "cloudformation", "cloudwatch", "ec2", "iam", "s3", "sns", "ssm", "sts"], version = "^1.35.35" } -moto = { extras = ["cloudfront", "dynamodb", "s3"], version = "^5.1.6" } -types-urllib3 = "^1.26.25.14" -urllib3 = "^2.5.0" -aws-lambda-powertools = {extras = ["all"], version = "^3.1.0"} -aws_lambda_context = "^1.1.0" -openapi_spec_validator = "^0.7.1" -jinja2="^3.1.5" +black = "24.10.0" +flake8 = "7.3.0" +isort = "5.13.2" +mypy = "1.17.1" +pytest = "8.4.1" +pytest-cov = "5.0.0" +pytest-env = "1.1.5" +pytest-mock = "3.14.1" +werkzeug = "3.1.3" +tox = "4.28.4" +boto3-stubs-lite = { extras = ["cloudfront", "cloudformation", "cloudwatch", "ec2", "iam", "s3", "sns", "ssm", "sts"], version = "1.40.8" } +moto = { extras = ["cloudfront", "dynamodb", "s3"], version = "5.1.10" } +types-urllib3 = "1.26.25.14" +urllib3 = "2.5.0" +aws-lambda-powertools = {extras = ["all"], version = "3.1.0"} +aws_lambda_context = "1.1.0" +openapi_spec_validator = "0.7.2" +jinja2="3.1.6" [build-system] requires = ["poetry-core"] diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh index 78a87dd3..098fb551 100755 --- a/deployment/run-unit-tests.sh +++ b/deployment/run-unit-tests.sh @@ -130,16 +130,109 @@ for playbook in `ls ${source_dir}/playbooks`; do fi done +echo "------------------------------------------------------------------------------" +echo "[Build] Data Models Package" +echo "------------------------------------------------------------------------------" +cd "$source_dir"/data-models +npm run build +rc=$? +if [ "$rc" -ne "0" ]; then + echo "** DATA MODELS BUILD FAILED **" + exit $rc +fi echo "------------------------------------------------------------------------------" -echo "[Lint] Code Style and Lint" +echo "[Setup] Starting DynamoDB Local" echo "------------------------------------------------------------------------------" -cd $source_dir -npx prettier --check '**/*.ts' -npx eslint --ext .ts --max-warnings=0 . -cd .. -tox -e format -tox -e lint + +# Check if DynamoDB Local is already running via Docker +if curl -s http://localhost:8000 >/dev/null 2>&1; then + echo "DynamoDB Local is already running (likely via Docker)" + DDB_PID="" +else + # Fall back to tar-based installation + if [[ -z "$DDB_LOCAL_HOME" ]]; then + echo "ERROR: DDB_LOCAL_HOME environment variable is not set and DynamoDB Local is not running via Docker" + exit 1 + fi + + # Verify DynamoDB Local files exist + if [[ ! -f "$DDB_LOCAL_HOME/DynamoDBLocal.jar" ]]; then + echo "ERROR: DynamoDBLocal.jar not found at $DDB_LOCAL_HOME/DynamoDBLocal.jar" + exit 1 + fi + + if [[ ! -d "$DDB_LOCAL_HOME/DynamoDBLocal_lib" ]]; then + echo "ERROR: DynamoDBLocal_lib directory not found at $DDB_LOCAL_HOME/DynamoDBLocal_lib" + exit 1 + fi + + java -Djava.library.path="$DDB_LOCAL_HOME"/DynamoDBLocal_lib -jar "$DDB_LOCAL_HOME"/DynamoDBLocal.jar -sharedDb -inMemory >/dev/null 2>&1 & + DDB_PID=$! + + # Wait for DynamoDB Local to be ready + echo "Waiting for DynamoDB Local to be ready..." + for i in {1..30}; do + if curl -s http://localhost:8000 >/dev/null 2>&1; then + echo "DynamoDB Local is ready (attempt $i)" + break + fi + if [ $i -eq 30 ]; then + echo "ERROR: DynamoDB Local failed to become ready after 30 seconds" + kill $DDB_PID 2>/dev/null || true + exit 1 + fi + sleep 1 + done + + if ! kill -0 $DDB_PID 2>/dev/null; then + echo "ERROR: DynamoDB Local failed to start" + exit 1 + fi + echo "DynamoDB Local started successfully (PID: $DDB_PID)" + + # Ensure DynamoDB process is killed on script exit + trap 'kill $DDB_PID 2>/dev/null || true' EXIT +fi + +echo "------------------------------------------------------------------------------" +echo "[Test] Preprocessor Unit Tests" +echo "------------------------------------------------------------------------------" +cd "$source_dir"/lambdas +npm run test:sequential:preprocessor + +echo "------------------------------------------------------------------------------" +echo "[Test] Lambdas/common Unit Tests" +echo "------------------------------------------------------------------------------" +cd "$source_dir"/lambdas +npm run test:sequential:common + +echo "------------------------------------------------------------------------------" +echo "[Test] Findings synchronization Unit Tests" +echo "------------------------------------------------------------------------------" +cd "$source_dir"/lambdas +npm run test:sequential:synchronization + +echo "------------------------------------------------------------------------------" +echo "[Test] API Unit Tests" +echo "------------------------------------------------------------------------------" +cd "$source_dir"/lambdas +npm run test:sequential:api + +echo "------------------------------------------------------------------------------" +echo "[Cleanup] Stopping DynamoDB Local" +echo "------------------------------------------------------------------------------" +if [[ -n "$DDB_PID" ]]; then + kill $DDB_PID 2>/dev/null || true +else + echo "DynamoDB Local was running via Docker (not stopped by this script)" +fi + +echo "------------------------------------------------------------------------------" +echo "[Test] Deployment Utils Unit Tests" +echo "------------------------------------------------------------------------------" +cd "$template_dir"/utils +npm run test echo "------------------------------------------------------------------------------" echo "[Test] CDK Unit Tests" @@ -161,6 +254,32 @@ cd "$source_dir" } +echo "------------------------------------------------------------------------------" +echo "[Test] WebUI Unit Tests" +echo "------------------------------------------------------------------------------" +cd $source_dir/webui +npm install +npm run test +rc=$? +if [ "$rc" -ne "0" ]; then + echo "** WEBUI UNIT TESTS FAILED **" +else + echo "WebUI Unit Tests Successful" +fi +if [ "$rc" -gt "$maxrc" ]; then + maxrc=$rc +fi + +echo "------------------------------------------------------------------------------" +echo "[Lint] Code Style and Lint" +echo "------------------------------------------------------------------------------" +cd $source_dir +npx eslint --ext .ts --max-warnings=0 --ignore-pattern "*.d.ts" . +cd .. +tox -e format +tox -e lint + + # The pytest --cov with its parameters and .coveragerc generates a xml cov-report with `coverage/sources` list # with absolute path for the source directories. To avoid dependencies of tools (such as SonarQube) on different # absolute paths for source directories, this substitution is used to convert each absolute source directory diff --git a/deployment/utils/generate-controls-list.js b/deployment/utils/generate-controls-list.js new file mode 100755 index 00000000..41550597 --- /dev/null +++ b/deployment/utils/generate-controls-list.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Generate Supported Controls List + * + * This script extracts the list of supported security controls from the SC playbook's + * remediations file and generates a JSON file containing all supported controls. + * + * The generated JSON file is used to document which security controls are supported + * by the solution. + * + * Usage: + * node generate-controls-list.js + * + * Arguments: + * solution-version - The version of the solution (e.g., "v1.0.0") + * + * Output: + * Creates a JSON file at ./global-s3-assets/supported-controls.json containing + * the solution version and an array of all supported control IDs. + */ + +const fs = require('fs'); +const path = require('path'); + +const solutionVersion = process.argv[2]; +if (!solutionVersion) { + console.error('Error: Solution version argument is required'); + console.error('Usage: node generate-controls-list.js '); + process.exit(1); +} + +const remediationsFilePath = path.join(__dirname, '../../source/playbooks/SC/lib/sc_remediations.ts'); +const outputFilePath = path.join(__dirname, '../global-s3-assets/supported-controls.json'); + +const fileContent = fs.readFileSync(remediationsFilePath, 'utf8'); + +// Look for entries like { control: 'ControlID', ... } +const controlRegex = /{\s*control:\s*'([^']+)/g; +const controls = {solutionVersion: solutionVersion, supportedControls: []}; +let match; + +while ((match = controlRegex.exec(fileContent)) !== null) { + controls.supportedControls.push(match[1]); +} + +fs.writeFileSync(outputFilePath, JSON.stringify(controls, null, 2)); + +console.log(`Generated controls list with ${controls.supportedControls.length} controls at ${outputFilePath}`); \ No newline at end of file diff --git a/deployment/utils/generate-controls-list.test.js b/deployment/utils/generate-controls-list.test.js new file mode 100644 index 00000000..49f876a5 --- /dev/null +++ b/deployment/utils/generate-controls-list.test.js @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + writeFileSync: jest.fn(), + mkdirSync: jest.fn(), + existsSync: jest.fn() +})); + +jest.mock('path', () => ({ + ...jest.requireActual('path'), + join: jest.fn(), + dirname: jest.fn() +})); + +jest.mock('process', () => ({ + ...jest.requireActual('process'), + exit: jest.fn(), +})); + +const fs = require('fs'); +const path = require('path'); + +const actualFs = jest.requireActual('fs'); +const actualPath = jest.requireActual('path'); + +// Get the actual file content +const scRemediationsPath = actualPath.join(__dirname, '../../source/playbooks/SC/lib/sc_remediations.ts'); +const actualFileContent = actualFs.readFileSync(scRemediationsPath, 'utf8'); + +// Count the actual number of controls in the file +const controlRegex = /\{\s*control:\s*'([^']+)/g; +let match; +const expectedControls = []; +while ((match = controlRegex.exec(actualFileContent)) !== null) { + expectedControls.push(match[1]); +} + +describe('generate-controls-list', () => { + const originalConsoleError = console.error; + const originalConsoleLog = console.log; + + + + beforeEach(() => { + jest.resetAllMocks(); + + console.error = jest.fn(); + console.log = jest.fn(); + + path.join.mockImplementation((dir, relativePath) => { + if (relativePath && relativePath.includes('sc_remediations.ts')) { + return scRemediationsPath; + } + return '/mock/path/output.json'; + }); + + path.dirname.mockReturnValue('/mock/path'); + + fs.existsSync.mockReturnValue(true); + fs.writeFileSync = jest.fn(); + fs.mkdirSync = jest.fn(); + }); + + afterEach(() => { + console.error = originalConsoleError; + console.log = originalConsoleLog; + + jest.resetModules(); + }); + + test('should extract controls and write to output file', () => { + const originalArgv = process.argv; + process.argv = ['node', 'generate-controls-list.js', 'v1.0.0']; + + require('./generate-controls-list'); + + expect(fs.writeFileSync).toHaveBeenCalled(); + + const writeCall = fs.writeFileSync.mock.calls[0]; + expect(writeCall[0]).toBe('/mock/path/output.json'); + + const writtenData = JSON.parse(writeCall[1]); + expect(writtenData.solutionVersion).toBe('v1.0.0'); + expect(Array.isArray(writtenData.supportedControls)).toBe(true); + expect(writtenData.supportedControls.length).toBe(expectedControls.length); + expect(writtenData.supportedControls.sort()).toEqual(expectedControls.sort()); + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining(`Generated controls list with ${writtenData.supportedControls.length} controls at`) + ); + + process.argv = originalArgv; + }); +}); \ No newline at end of file diff --git a/deployment/utils/package-lock.json b/deployment/utils/package-lock.json new file mode 100644 index 00000000..47525616 --- /dev/null +++ b/deployment/utils/package-lock.json @@ -0,0 +1,3641 @@ +{ + "name": "@amzn/asr-deployment-utils", + "version": "3.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@amzn/asr-deployment-utils", + "version": "3.0.0", + "license": "Apache-2.0", + "devDependencies": { + "jest": "29.7.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, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "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, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "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/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.190", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", + "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "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, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "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/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, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/deployment/utils/package.json b/deployment/utils/package.json new file mode 100644 index 00000000..c3df4e7f --- /dev/null +++ b/deployment/utils/package.json @@ -0,0 +1,17 @@ +{ + "name": "@amzn/asr-deployment-utils", + "version": "3.0.0", + "description": "Deployment scripts for Automated Security Response on AWS", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com/solutions", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "test": "npm ci && jest --coverage" + }, + "devDependencies": { + "jest": "29.7.0" + } +} \ No newline at end of file diff --git a/docs/architecture_diagram.png b/docs/architecture_diagram.png deleted file mode 100644 index b5acecb892f275f1ab2bf03f3f136f3842791e24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 275168 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 z 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

@LO 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�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 z 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 diff --git a/docs/automated-security-response-on-aws-architecture-diagram.png b/docs/automated-security-response-on-aws-architecture-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..cf914e625cf909a97e4ac9f8862f49030aa3a7b2 GIT binary patch literal 500072 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�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

=@o64XBEW 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%0An_><5!-nFG6UFk$^`c*34#%8BT_7^vsn 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 RodW;t+M%{RPT}zH{}1!ZrUn22 literal 0 HcmV?d00001 diff --git a/solution-manifest.yaml b/solution-manifest.yaml index ae9147cf..f75508c7 100644 --- a/solution-manifest.yaml +++ b/solution-manifest.yaml @@ -1,6 +1,6 @@ id: SO0111 name: automated-security-response-on-aws -version: v2.3.2 +version: v3.0.0 cloudformation_templates: - template: automated-security-response-admin.template main_template: true @@ -22,5 +22,6 @@ cloudformation_templates: - template: playbooks/NIST80053Stack.template - template: blueprints/JiraBlueprintStack.template - template: blueprints/ServiceNowBlueprintStack.template + - template: automated-security-response-webui-nested-stack.template build_environment: build_image: aws/codebuild/standard:7.0 \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index 57d4945a..16866806 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,8 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# + # scan templates in cdk.out even though they are in .gitignore sonar.scm.exclusions.disabled = true @@ -5,11 +10,18 @@ sonar.sources = source/,simtest/ sonar.exclusions = \ **/test/**/*, \ + **/__tests__/**/*, \ test-stack/**/*, \ - source/jest.config.ts, \ + **/jest.config.*, \ source/**/*.test.ts, \ - source/coverage/**/*, \ - source/**/cdk.out/* + **/coverage/**/*, \ + source/**/cdk.out/*, \ + source/pre_processor/coverage/**/*, \ + deployment/utils/coverage/**/*, \ + source/webui/src/__tests__/**, \ + source/webui/src/public/*, \ + source/data-models/cjs/**, \ + source/data-models/esm/** sonar.tests = \ source/layer/test/, \ @@ -26,15 +38,32 @@ sonar.tests = \ source/playbooks/SC/test/, \ source/remediation_runbooks/scripts/test/, \ source/solution_deploy/source/test/, \ - source/test/ - -sonar.coverage.exclusions = simtest/**/* -sonar.python.version = 3.8, 3.9, 3.10, 3.11 + source/test/, \ + source/lambdas/pre-processor/__tests__/, \ + source/lambdas/api/__tests__/, \ + source/lambdas/common/__tests__/, \ + deployment/utils/, \ + source/webui/src/__tests__/ +sonar.coverage.exclusions=\ + simtest/**/*, \ + source/webui/public/mockServiceWorker.js, \ + source/webui/src/mocks/**, \ + source/webui/src/main.tsx, \ + source/webui/src/setupTests.ts, \ + **/repositories/abstractRepository.ts +sonar.python.version = 3.11 sonar.python.coverage.reportPaths = deployment/test/coverage-reports/*.coverage.xml -sonar.javascript.lcov.reportPaths = source/coverage/lcov.info +sonar.javascript.lcov.reportPaths = \ + source/coverage/lcov.info, \ + source/webui/coverage/lcov.info, \ + source/lambdas/api/coverage/lcov.info, \ + source/lambdas/common/coverage/lcov.info, \ + source/lambdas/pre-processor/coverage/lcov.info, \ + source/lambdas/synchronization/coverage/lcov.info sonar.cpd.exclusions= \ + source/lib/administrator-stack.ts, \ source/playbooks/**/lib/*_remediations.ts, \ source/playbooks/**/lib/*construct.ts, \ source/playbooks/**/ssmdocs/**, \ @@ -43,4 +72,3 @@ sonar.cpd.exclusions= \ sonar.issue.ignore.multicriteria = ts1 sonar.issue.ignore.multicriteria.ts1.ruleKey = typescript:S1848 sonar.issue.ignore.multicriteria.ts1.resourceKey = **/*.ts - diff --git a/source/.eslintrc.js b/source/.eslintrc.js index e45d3e11..afa47f27 100644 --- a/source/.eslintrc.js +++ b/source/.eslintrc.js @@ -1,43 +1,33 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 module.exports = { - "env": { - "jest": true, - "node": true + env: { + jest: true, + node: true, }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" - ], - "ignorePatterns": [ - "node_modules", - "**/*.d.ts", - "**/*.js" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "project": "**/tsconfig.json", - "sourceType": "module" + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], + ignorePatterns: ['node_modules', '**/*.d.ts', '**/*.js', '**/vite.config.ts'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + project: '**/tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint', 'header', 'import'], + root: true, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + // treat prettier as warning rather than error. prettier can be used as formatter, but should not fail the build + 'prettier/prettier': 'warn', + 'header/header': [ + 'error', + 'line', + [' Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.', ' SPDX-License-Identifier: Apache-2.0'], + 1, + ], }, - "plugins": [ - "@typescript-eslint", - "header", - "import" - ], - "root": true, - "rules": { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "off", - "header/header": [ - "error", - "line", - [ - " Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.", - " SPDX-License-Identifier: Apache-2.0" - ], - 1 - ] - } }; diff --git a/source/.prettierignore b/source/.prettierignore index 849ddff3..77738287 100644 --- a/source/.prettierignore +++ b/source/.prettierignore @@ -1 +1 @@ -dist/ +dist/ \ No newline at end of file diff --git a/source/Orchestrator/check_ssm_doc_state.py b/source/Orchestrator/check_ssm_doc_state.py index e221d1fc..ada1d763 100644 --- a/source/Orchestrator/check_ssm_doc_state.py +++ b/source/Orchestrator/check_ssm_doc_state.py @@ -87,6 +87,10 @@ def _add_doc_state_to_answer(doc: str, account: str, region: str, answer: Any) - cloudwatch_metrics.send_metric(cloudwatch_metric) except Exception: logger.debug("Did not send Cloudwatch metric") + elif exception_type == "ThrottlingException": + # Re-raise throttling exceptions so Step Functions can retry with backoff + logger.warning(f"SSM API throttled for document {doc}, will retry") + raise else: answer.update( { diff --git a/source/Orchestrator/check_ssm_execution.py b/source/Orchestrator/check_ssm_execution.py index 17f5636e..47a9371b 100644 --- a/source/Orchestrator/check_ssm_execution.py +++ b/source/Orchestrator/check_ssm_execution.py @@ -199,7 +199,7 @@ def lambda_handler(event: Dict[str, Any], _: Any) -> Dict[str, Any]: logger.error(answer.message) return answer.json() # type: ignore[no-any-return] - SSM_EXEC_ID = event["SSMExecution"]["ExecId"] + SSM_EXEC_ID = event["SSMExecution"]["SSMExecutionId"] SSM_ACCOUNT = event["SSMExecution"].get("Account") SSM_REGION = event["SSMExecution"].get("Region") diff --git a/source/Orchestrator/schedule_remediation.py b/source/Orchestrator/schedule_remediation.py index 728358ba..97280015 100644 --- a/source/Orchestrator/schedule_remediation.py +++ b/source/Orchestrator/schedule_remediation.py @@ -64,11 +64,14 @@ def lambda_handler(event: Dict[str, Any], _: Any) -> str: is_within_threshold = found_time_is_within_wait_threshold(found_timestamp) - new_timestamp = ( + calculated_timestamp = ( found_timestamp + wait_threshold if is_within_threshold else current_timestamp ) + + # If calculated timestamp is in the past, use current time + new_timestamp = max(calculated_timestamp, current_timestamp) new_timestamp_ttl = new_timestamp + wait_threshold dynamodb_client.put_item( diff --git a/source/Orchestrator/send_notifications.py b/source/Orchestrator/send_notifications.py index b496ee6e..30b694a3 100644 --- a/source/Orchestrator/send_notifications.py +++ b/source/Orchestrator/send_notifications.py @@ -2,31 +2,150 @@ # SPDX-License-Identifier: Apache-2.0 import json import os +import re +from dataclasses import dataclass +from datetime import datetime, timedelta from json.decoder import JSONDecodeError -from typing import Any, NotRequired, TypedDict, Union +from typing import Any, NotRequired, Optional, TypedDict, Union, cast +from urllib.parse import quote_plus +from botocore.exceptions import ClientError from layer import sechub_findings +from layer.awsapi_cached_client import AWSCachedClient from layer.cloudwatch_metrics import CloudWatchMetrics -from layer.metrics import Metrics +from layer.metrics import NORMALIZED_STATUS_REASON_MAPPING, Metrics from layer.powertools_logger import get_logger from layer.tracer_utils import init_tracer from layer.utils import get_account_alias # Get AWS region from Lambda environment. If not present then we're not # running under lambda, so defaulting to us-east-1 -AWS_REGION = os.getenv( - "AWS_DEFAULT_REGION", "us-east-1" -) # MUST BE SET in global variables +AWS_REGION = os.getenv("AWS_REGION", "us-east-1") # MUST BE SET in global variables AWS_PARTITION = os.getenv("AWS_PARTITION", "aws") # MUST BE SET in global variables -WEB_PARTITION = { - "aws-cn": "amazonaws.cn", - "aws-us-gov": "amazonaws-us-gov", - "aws": "aws.amazon", -} + + +def get_console_host(partition: str) -> str: + console_hosts = { + "aws": "console.aws.amazon.com", + "aws-us-gov": "console.amazonaws-us-gov.com", + "aws-cn": "console.amazonaws.cn", + } + return console_hosts.get(partition, console_hosts["aws"]) + + +def get_security_hub_console_url( + finding_id: str, region: Optional[str] = None, partition: Optional[str] = None +) -> str: + """Generates Security Hub finding console URL. + + If Security Hub V2 is enabled in the current account, this finding links to + the Security Hub console. Otherwise, it links to Security Hub CSPM. + + Args: + finding_id: The Security Hub finding ID + region: AWS region (optional, defaults to AWS_REGION env var). Since the solution + must be deployed in the Security Hub aggregation region, all findings should be + available in the region where this Lambda function exists, meaning you likely do + not want to pass a value for this parameter unless you require a region-specific + console link. + partition: AWS partition (optional, defaults to AWS_PARTITION env var) + + Returns: + Console URL for the Security Hub finding + """ + securityhub_v2_enabled = ( + os.getenv("SECURITY_HUB_V2_ENABLED", "false").lower() == "true" + ) + aws_region = region or os.getenv("AWS_REGION", "us-east-1") + aws_partition = partition or cast(str, os.getenv("AWS_PARTITION", "aws")) + + host = get_console_host(aws_partition) + + if securityhub_v2_enabled: + default_url = f"/securityhub/v2/home?region={aws_region}#/findings?search=finding_info.uid%3D%255Coperator%255C%253AEQUALS%255C%253A{quote_plus(finding_id)}" + else: + default_url = f"/securityhub/home?region={aws_region}#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253A{quote_plus(finding_id)}" + + url_pattern = os.getenv("CONSOLE_URL_PATTERN", default_url) + + return f"https://{host}{url_pattern}" + logger = get_logger("send_notifications") tracer = init_tracer() +FINDINGS_TABLE_NAME = os.getenv("FINDINGS_TABLE_NAME", "") +HISTORY_TABLE_NAME = os.getenv("HISTORY_TABLE_NAME", "") + +FINDING_ID_EXECUTION_ID_KEY = "findingId#executionId" +SORT_KEY_ATTRIBUTE_NAME = "#sortKey" + + +class FindingData(TypedDict, total=False): + accountId: str + resourceId: str + resourceType: str + resourceTypeNormalized: str + severity: str + region: str + lastUpdatedBy: str + + +class FindingInfo(TypedDict): + finding_id: str + finding_description: str + standard_name: str + standard_version: str + standard_control: str + title: str + region: str + account: str + finding_arn: str + + +class TransactWriteItem(TypedDict, total=False): + Put: dict[str, Any] + Update: dict[str, Any] + Delete: dict[str, Any] + ConditionCheck: dict[str, Any] + + +def calculate_history_ttl_timestamp(timestamp: str) -> int: + ttl_days = int(os.getenv("HISTORY_TTL_DAYS", "365")) + + dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) + ttl_dt = dt + timedelta(days=ttl_days) + return int(ttl_dt.timestamp()) + + +@dataclass +class RemediationUpdateRequest: + finding_id: str + execution_id: str + remediation_status: str + finding_type: str + error: Optional[str] = None + resource_id: Optional[str] = None + resource_type: Optional[str] = None + account_id: Optional[str] = None + severity: Optional[str] = None + region: Optional[str] = None + lastUpdatedBy: Optional[str] = "Automated" + + def validate(self) -> bool: + if not self.finding_id or not self.execution_id or not self.finding_type: + logger.error( + "Missing required parameters", + extra={ + "findingId": self.finding_id, + "executionId": self.execution_id, + "findingType": self.finding_type, + }, + ) + return False + + return True + def format_details_for_output(details: Any) -> list[str]: """Handle various possible formats in the details""" @@ -52,7 +171,7 @@ def format_details_for_output(details: Any) -> list[str]: def set_message_prefix_and_suffix(event): - message_prefix = event["Notification"].get("ExecId", "") + message_prefix = event["Notification"].get("SSMExecutionId", "") message_suffix = event["Notification"].get("AffectedObject", "") if message_prefix: message_prefix += ": " @@ -61,11 +180,35 @@ def set_message_prefix_and_suffix(event): return message_prefix, message_suffix +def map_remediation_status(status: Optional[str]) -> str: + if not status: + return "NOT_STARTED" + + status_upper = status.upper() + + if status_upper in ("SUCCESS", "NOT_STARTED"): + return status_upper + + if status_upper in ("QUEUED", "RUNNING", "IN_PROGRESS"): + return "IN_PROGRESS" + + if status_upper in list(NORMALIZED_STATUS_REASON_MAPPING.keys()): + logger.debug( + f"Mapping original failed remediation status {status_upper} to 'FAILED'" + ) + return "FAILED" + + logger.warning(f"Unknown remediation status '{status}', mapping to FAILED") + return "FAILED" + + class Notification(TypedDict): Message: str State: str Details: NotRequired[str] RemediationOutput: NotRequired[str] + StepFunctionsExecutionId: NotRequired[str] + SSMExecutionId: NotRequired[str] class GenerateTicket(TypedDict): @@ -83,92 +226,894 @@ class Event(TypedDict): CustomActionName: NotRequired[str] SecurityStandard: NotRequired[str] ControlId: NotRequired[str] + AccountId: NotRequired[str] + Region: NotRequired[str] + Resources: NotRequired[Union[list[dict[str, Any]], dict[str, Any]]] + Severity: NotRequired[dict[str, Any]] -@tracer.capture_lambda_handler # type: ignore[misc] -def lambda_handler(event: Event, _: Any) -> None: - message_prefix, message_suffix = set_message_prefix_and_suffix(event) +def update_remediation_status_and_history(request: RemediationUpdateRequest) -> None: - status_from_event = event.get("Notification", {}).get("State", "").upper() + if not request.validate(): + return + + try: + aws_client = AWSCachedClient(AWS_REGION) + dynamodb = aws_client.get_connection("dynamodb") + + logger.debug( + "Processing remediation status update", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "remediationStatus": request.remediation_status, + "error": request.error, + }, + ) + + # First, try to update both finding and history + success = _try_update_with_existing_history(dynamodb, request) + + if not success: + logger.info( + "History item not found, creating new history record via fallback", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "remediationStatus": request.remediation_status, + }, + ) + _create_history_with_finding_update(dynamodb, request) + + except ClientError as e: + logger.error( + "Failed to update remediation status", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "error": str(e), + }, + ) + raise + except Exception as e: + logger.error( + "Unexpected error updating remediation status", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "error": str(e), + }, + ) + raise + + +def _try_update_with_existing_history( + dynamodb: Any, + request: RemediationUpdateRequest, +) -> bool: + try: + logger.debug( + "Attempting to update existing history item", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "remediationStatus": request.remediation_status, + }, + ) + + transact_items = [] + + finding_update_item = _build_finding_update_item( + request.finding_type, + request.finding_id, + request.remediation_status, + request.execution_id, + request.error, + ) + transact_items.append(finding_update_item) + + remediation_history_item = _build_history_update_item(request) + transact_items.append(remediation_history_item) + + dynamodb.transact_write_items(TransactItems=transact_items) + + logger.debug( + "Successfully updated existing history item via transaction", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "remediationStatus": request.remediation_status, + }, + ) + return True + + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code", "") + + logger.warning( + "Transaction failed while trying to update existing history", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "remediationStatus": request.remediation_status, + "errorCode": error_code, + "errorMessage": str(e), + }, + ) + + # Check if the error is due to conditional check failure (history item doesn't exist) + if error_code == "TransactionCanceledException": + cancellation_reasons = e.response.get("CancellationReasons", []) + for i, reason in enumerate(cancellation_reasons): + if reason.get("Code") == "ConditionalCheckFailed": + logger.warning( + "History item not found due to conditional check failure, will attempt fallback creation", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "remediationStatus": request.remediation_status, + "cancellationReason": reason, + "transactionItemIndex": i, + }, + ) + return False + + raise - finding = None - finding_info: Union[str, dict[str, Any]] = "" - if "Finding" in event: - finding = sechub_findings.Finding(event["Finding"]) - finding_info = { - "finding_id": finding.uuid, - "finding_description": finding.description, - "standard_name": finding.standard_name, - "standard_version": finding.standard_version, - "standard_control": finding.standard_control, - "title": finding.title, - "region": finding.region, - "account": finding.account_id, - "finding_arn": finding.arn, + +def _create_history_with_finding_update( + dynamodb: Any, + request: RemediationUpdateRequest, +) -> None: + finding_data = None + + try: + finding_data = _get_finding_data( + dynamodb, request.finding_type, request.finding_id + ) + except Exception as e: + logger.warning( + "Could not retrieve finding data for history creation, proceeding with minimal data", + extra={ + "findingId": request.finding_id, + "error": str(e), + }, + ) + + try: + transact_items = [] + + if finding_data: + finding_update_item = _build_finding_update_item( + request.finding_type, + request.finding_id, + request.remediation_status, + request.execution_id, + request.error, + ) + transact_items.append(finding_update_item) + + history_create_item = _build_history_create_item(request, finding_data) + transact_items.append(history_create_item) + + dynamodb.transact_write_items(TransactItems=transact_items) + + logger.info( + "Successfully created remediation history via fallback", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + }, + ) + + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code", "") + + if error_code == "TransactionCanceledException": + _update_finding_only(dynamodb, request) + else: + raise + + +def _update_finding_only( + dynamodb: Any, + request: RemediationUpdateRequest, +) -> None: + try: + finding_update_item = _build_finding_update_item( + request.finding_type, + request.finding_id, + request.remediation_status, + request.execution_id, + request.error, + ) + + dynamodb.transact_write_items(TransactItems=[finding_update_item]) + + logger.debug( + "Successfully updated finding only after history operation failure", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + }, + ) + except Exception as e: + logger.error( + "Failed to update finding after history operation failure", + extra={ + "findingId": request.finding_id, + "executionId": request.execution_id, + "findingType": request.finding_type, + "error": str(e), + }, + ) + raise + + +def _extract_finding_fields(item: dict[str, Any]) -> FindingData: + finding_data: FindingData = {} + field_mappings = [ + "accountId", + "resourceId", + "resourceType", + "resourceTypeNormalized", + "severity", + "region", + "lastUpdatedBy", + ] + + for field in field_mappings: + if field in item: + finding_data[field] = item[field]["S"] # type: ignore[literal-required] + + return finding_data + + +def _get_finding_data( + dynamodb: Any, + finding_type: str, + finding_id: str, +) -> Optional[FindingData]: + try: + response = dynamodb.get_item( + TableName=FINDINGS_TABLE_NAME, + Key={ + "findingType": {"S": finding_type}, + "findingId": {"S": finding_id}, + }, + ) + + if "Item" not in response: + return None + + return _extract_finding_fields(response["Item"]) + + except Exception as e: + logger.warning( + "Error retrieving finding data", + extra={ + "findingType": finding_type, + "findingId": finding_id, + "error": str(e), + }, + ) + return None + + +def _build_finding_update_item( + finding_type: str, + finding_id: str, + remediation_status: str, + execution_id: str, + error: Optional[str] = None, +) -> TransactWriteItem: + update_expression = "SET remediationStatus = :rs" + expression_values = { + ":rs": {"S": remediation_status}, + } + expression_names = {} + + if execution_id: + update_expression += ", executionId = :eid" + expression_values[":eid"] = {"S": execution_id} + + if error: + update_expression += ", #err = :err" + expression_names["#err"] = "error" + expression_values[":err"] = {"S": error} + + finding_update_item: TransactWriteItem = { + "Update": { + "TableName": FINDINGS_TABLE_NAME, + "Key": {"findingType": {"S": finding_type}, "findingId": {"S": finding_id}}, + "UpdateExpression": update_expression, + "ExpressionAttributeValues": expression_values, } + } - control_id = ( - event["Finding"].get("Compliance", {}).get("SecurityControlId", "") - if "Finding" in event - else "" + if expression_names: + finding_update_item["Update"]["ExpressionAttributeNames"] = expression_names + + return finding_update_item + + +def _merge_finding_data_into_item( + item: dict[str, Any], finding_data: FindingData +) -> None: + field_mappings = [ + "accountId", + "resourceId", + "resourceType", + "resourceTypeNormalized", + "severity", + "region", + "lastUpdatedBy", + ] + + for field in field_mappings: + if field in finding_data: + item[field] = {"S": finding_data[field]} # type: ignore[literal-required] + + +def _build_history_create_item( + request: RemediationUpdateRequest, finding_data: Optional[FindingData] = None +) -> TransactWriteItem: + timestamp = datetime.utcnow().isoformat() + "Z" + sort_key = f"{request.finding_id}#{request.execution_id}" + + item = { + "findingType": {"S": request.finding_type}, + "findingId": {"S": request.finding_id}, + FINDING_ID_EXECUTION_ID_KEY: {"S": sort_key}, + "executionId": {"S": request.execution_id}, + "remediationStatus": {"S": request.remediation_status}, + "lastUpdatedTime": {"S": timestamp}, + "lastUpdatedTime#findingId": {"S": f"{timestamp}#{request.finding_id}"}, + "REMEDIATION_CONSTANT": {"S": "remediation"}, + "lastUpdatedBy": {"S": request.lastUpdatedBy}, + "expireAt": {"N": str(calculate_history_ttl_timestamp(timestamp))}, + "accountId": {"S": request.account_id}, + "resourceId": {"S": request.resource_id}, + "resourceType": {"S": request.resource_type}, + "severity": {"S": request.severity}, + "region": {"S": request.region}, + } + + if request.error: + item["error"] = {"S": request.error} + + if finding_data: + _merge_finding_data_into_item(item, finding_data) + + history_create_item: TransactWriteItem = { + "Put": { + "TableName": HISTORY_TABLE_NAME, + "Item": item, + "ConditionExpression": "attribute_not_exists(findingType) AND attribute_not_exists(#sortKey)", + "ExpressionAttributeNames": { + SORT_KEY_ATTRIBUTE_NAME: FINDING_ID_EXECUTION_ID_KEY + }, + } + } + + return history_create_item + + +def _build_history_update_item(request: RemediationUpdateRequest) -> TransactWriteItem: + update_expression = "SET remediationStatus = :rs" + + expression_values = { + ":rs": {"S": request.remediation_status}, + } + + expression_names = {} + + if request.error: + update_expression += ", #err = :err" + expression_names["#err"] = "error" + expression_values[":err"] = {"S": request.error} + + sort_key = f"{request.finding_id}#{request.execution_id}" + + history_update_item: TransactWriteItem = { + "Update": { + "TableName": HISTORY_TABLE_NAME, + "Key": { + "findingType": {"S": request.finding_type}, + FINDING_ID_EXECUTION_ID_KEY: {"S": sort_key}, + }, + "UpdateExpression": update_expression, + "ExpressionAttributeValues": expression_values, + "ConditionExpression": "attribute_exists(findingType) AND attribute_exists(#sortKey)", + } + } + + if expression_names: + expression_names[SORT_KEY_ATTRIBUTE_NAME] = FINDING_ID_EXECUTION_ID_KEY + else: + expression_names = {SORT_KEY_ATTRIBUTE_NAME: FINDING_ID_EXECUTION_ID_KEY} + + history_update_item["Update"]["ExpressionAttributeNames"] = expression_names + + return history_update_item + + +def _extract_stepfunctions_execution_id(event: Event) -> str: + execution_id = event.get("Notification", {}).get("StepFunctionsExecutionId") + + if not execution_id: + logger.error("StepFunctionsExecutionId not found in event") + return "unknown" + + return str(execution_id) + + +def _is_notified_workflow(event: Event) -> bool: + if "Finding" not in event: + return False + + finding = event["Finding"] + workflow = finding.get("Workflow", {}) + + if not isinstance(workflow, dict): + return False + + workflow_status = workflow.get("Status", "") + if workflow_status != "NOTIFIED": + return False + + event_type = event.get("EventType", "") + if event_type in ( + "Security Hub Findings - Custom Action", + "Security Hub Findings - API Action", + ): + logger.debug( + "NOTIFIED workflow detected but EventType indicates custom/API action - not skipping database updates", + extra={"findingId": _extract_id(event), "eventType": event_type}, + ) + return False + + logger.debug( + "NOTIFIED workflow detected - skipping database updates", + extra={"findingId": _extract_id(event), "eventType": event_type}, ) - custom_action_name = ( - event["CustomActionName"] if "CustomActionName" in event else "" + return True + + +def get_control_id_from_finding_id(finding_id: str) -> Optional[str]: + # Finding ID structure depends on consolidation settings + # https://aws.amazon.com/blogs/security/consolidating-controls-in-security-hub-the-new-controls-view-and-consolidated-findings/ + + # Unconsolidated finding ID pattern + unconsolidated_pattern = r"^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\d:\d{12}:subscription\/(.+)\/finding\/.+$" + unconsolidated_match = re.match(unconsolidated_pattern, finding_id) + if unconsolidated_match: + return unconsolidated_match.group( + 1 + ) # example: 'aws-foundational-security-best-practices/v/1.0.0/S3.1' + + # Consolidated finding ID pattern + consolidated_pattern = r"^arn:(?:aws|aws-cn|aws-us-gov):securityhub:[a-z]{2}(?:-gov)?-[a-z]+-\d:\d{12}:(.+)\/finding\/.+$" + consolidated_match = re.match(consolidated_pattern, finding_id) + if consolidated_match: + return consolidated_match.group(1) # example: 'security-control/Lambda.3' + + return None + + +def sanitize_control_id(control_id: str) -> str: + non_alphanumeric_or_allowed = re.compile(r"[^a-zA-Z0-9/.-]") + return non_alphanumeric_or_allowed.sub("", control_id) + + +def get_finding_type(event: Event) -> str: + if "Finding" not in event: + return "" + + finding_id = _extract_id(event) + if finding_id: + control_id_from_finding_id = get_control_id_from_finding_id(finding_id) + if control_id_from_finding_id: + return sanitize_control_id(control_id_from_finding_id) + + control_id = _extract_security_control_id(event) + if control_id: + return sanitize_control_id(control_id) + + return "" + + +def _extract_security_control_id(event: Event) -> str: + if "Finding" not in event: + return "" + + # Try to get SecurityControlId from Compliance first + compliance = event["Finding"].get("Compliance", {}) + control_id = ( + compliance.get("SecurityControlId", "") if isinstance(compliance, dict) else "" ) - if "EventType" in event: - metrics = Metrics(event["EventType"]) - metrics_data = metrics.get_metrics_from_event(event) - metrics_data["status"], metrics_data["status_reason"] = ( - Metrics.get_status_for_anonymized_metrics(status_from_event) + # If empty, fallback to ProductFields.ControlId + if not control_id: + product_fields = event["Finding"].get("ProductFields", {}) + control_id = ( + product_fields.get("ControlId", "") + if isinstance(product_fields, dict) + else "" ) - # Send anonymized metrics - metrics.send_metrics(metrics_data) - # Send CloudWatch metrics for ASR's custom dashboard - create_and_send_cloudwatch_metrics( - status_from_event, control_id, custom_action_name + return str(control_id) + + +def _extract_id(event: Event) -> str: + if "Finding" not in event: + return "" + + finding_id = event["Finding"].get("Id", "") + + if not finding_id: + product_fields = event["Finding"].get("ProductFields", {}) + finding_id = ( + product_fields.get("aws/securityhub/FindingId", "") + if isinstance(product_fields, dict) + else "" ) + return str(finding_id) + + +def _extract_resource_id(event: Event, resources: dict[str, Any]) -> str: + resource_id = resources.get("Id", "") if resources else "" + + if not resource_id: + product_fields = event.get("Finding", {}).get("ProductFields", {}) + if isinstance(product_fields, dict): + resources_field = product_fields.get("Resources:0/Id", "") + resource_id = str(resources_field) if resources_field else "" + + return resource_id + + +def _extract_finding_info( + event: Event, +) -> tuple[Optional[sechub_findings.Finding], Union[str, FindingInfo]]: + if "Finding" not in event: + return None, "" + + finding = sechub_findings.Finding(event["Finding"]) + finding_info: FindingInfo = { + "finding_id": finding.uuid or "", + "finding_description": finding.description or "", + "standard_name": finding.standard_name or "", + "standard_version": finding.standard_version or "", + "standard_control": finding.standard_control or "", + "title": finding.title or "", + "region": finding.region or "", + "account": finding.account_id or "", + "finding_arn": finding.arn or "", + } + return finding, finding_info + + +def _process_metrics( + event: Event, status_from_event: str, control_id: str, custom_action_name: str +) -> None: + metrics = Metrics() + metrics_data = metrics.get_metrics_from_event(event) + metrics_data["status"], metrics_data["status_reason"] = ( + Metrics.get_status_for_metrics(status_from_event) + ) + metrics.send_metrics(metrics_data) + + create_and_send_cloudwatch_metrics( + status_from_event, control_id, custom_action_name + ) + + +def _create_notification( + event: Event, + status_from_event: str, + stepfunctions_execution_id: str, + finding: Optional[sechub_findings.Finding], +) -> sechub_findings.ASRNotification: + notification = sechub_findings.ASRNotification( + event.get("SecurityStandard", "ASR"), + AWS_REGION, + stepfunctions_execution_id, + event.get("ControlId", None), + ) + if status_from_event in ("SUCCESS", "QUEUED"): - notification = sechub_findings.ASRNotification( - event.get("SecurityStandard", "ASR"), - AWS_REGION, - event.get("ControlId", None), - ) notification.severity = "INFO" - notification.send_to_sns = True - elif status_from_event == "FAILED": - notification = sechub_findings.ASRNotification( - event.get("SecurityStandard", "ASR"), - AWS_REGION, - event.get("ControlId", None), - ) - notification.severity = "ERROR" - notification.send_to_sns = True else: - notification = sechub_findings.ASRNotification( - event.get("SecurityStandard", "ASR"), - AWS_REGION, - event.get("ControlId", None), - ) notification.severity = "ERROR" if finding: finding.flag(event["Notification"]["Message"]) - notification.send_to_sns = True + + notification.send_to_sns = True + return notification + + +# Check if Notification State is explicitly "NOT_NEW" and workflow status is "RESOLVED" +def _is_resolved_item(event: Event) -> bool: + if "Finding" not in event: + return False + + notification_state = event.get("Notification", {}).get("State", "") + if notification_state != "NOT_NEW": + return False + + finding = event["Finding"] + workflow = finding.get("Workflow", {}) + + if not isinstance(workflow, dict): + return False + + workflow_status = workflow.get("Status", "") + if workflow_status != "RESOLVED": + return False + + return True + + +def _update_finding_remediation_status( + execution_id: str, + status_from_event: str, + event: Event, +) -> None: + remediation_status = map_remediation_status(status_from_event) + error_message = None + + if remediation_status == "FAILED": + error_message = event["Notification"].get("Details") or event[ + "Notification" + ].get("Message", None) + + if _is_resolved_item(event): + logger.warning( + "Overriding remediation status to SUCCESS for resolved workflow with NOT_NEW state", + extra={ + "findingId": _extract_id(event), + "originalStatus": status_from_event, + "overriddenStatus": "SUCCESS", + }, + ) + remediation_status = "SUCCESS" + error_message = None + + finding_id = _extract_id(event) + finding_type = get_finding_type(event) + + logger.debug( + "Finding processing", + extra={ + "finding id": finding_id, + "finding type": finding_type, + }, + ) + + try: + resources = event.get("Resources", {}) + if isinstance(resources, list) and len(resources) > 0: + resources = resources[0] + elif not isinstance(resources, dict): + resources = {} + + remediation_request = RemediationUpdateRequest( + finding_id=finding_id, + execution_id=execution_id, + remediation_status=remediation_status, + finding_type=finding_type, + error=error_message, + resource_id=_extract_resource_id(event, resources), + resource_type=resources.get("Type", "") if resources else "", + account_id=event.get("AccountId", ""), + severity=( + event.get("Severity", {}).get("Label", "") + if event.get("Severity") + else "" + ), + region=event.get("Region", ""), + lastUpdatedBy="Automated", + ) + update_remediation_status_and_history(remediation_request) + except Exception as e: + logger.error( + "Failed to update remediation status and history", + extra={ + "finding_id": finding_id, + "executionId": execution_id, + "finding_type": finding_type, + "error": str(e), + }, + ) + + +def _parse_orchestrator_input(input_str: str) -> dict[str, Any]: + try: + result = json.loads(input_str) + return cast(dict[str, Any], result) + except (JSONDecodeError, TypeError) as e: + logger.warning( + "Failed to parse Step Functions input", + extra={"input": input_str[:500], "error": str(e)}, + ) + return {} + + +def _add_optional_finding_fields( + transformed_event: Event, finding_data: dict[str, Any] +) -> None: + simple_field_mappings = { + "AwsAccountId": "AccountId", + "Region": "Region", + "Resources": "Resources", + "Severity": "Severity", + } + + for source_field, target_field in simple_field_mappings.items(): + if source_field in finding_data: + transformed_event[target_field] = finding_data[source_field] # type: ignore[literal-required] + + # Handle nested ProductFields + product_fields = finding_data.get("ProductFields", {}) + if isinstance(product_fields, dict) and "StandardsGuideArn" in product_fields: + transformed_event["SecurityStandard"] = product_fields["StandardsGuideArn"] + + # Handle nested Compliance + compliance = finding_data.get("Compliance", {}) + if isinstance(compliance, dict) and "SecurityControlId" in compliance: + transformed_event["ControlId"] = compliance["SecurityControlId"] + + +def _transform_stepfunctions_failure_event(raw_event: dict[str, Any]) -> Event: + try: + detail = raw_event.get("detail", {}) + input_str = detail.get("input", "{}") + orchestrator_input = _parse_orchestrator_input(input_str) + + findings_list = orchestrator_input.get("detail", {}).get("findings", []) + finding_data = findings_list[0] if findings_list else {} + + finding_id = finding_data.get("Id", "unknown") + execution_arn = detail.get("executionArn", "unknown") + execution_name = detail.get("name", "unknown") + status = detail.get("status", "FAILED") + cause = detail.get("cause", status) + error = detail.get("error", "") + + logger.info( + "Transforming Step Functions failure event", + extra={ + "findingId": finding_id, + "executionArn": execution_arn, + "executionName": execution_name, + "status": status, + "hasFindingData": bool(finding_data), + }, + ) + + error_details = ( + f"Error: {error}, Cause: {cause}" if error else f"Cause: {cause}" + ) + + transformed_event: Event = { + "Notification": { + "Message": f"Orchestrator execution {status.lower()}: {execution_arn}", + "State": status, + "Details": error_details, + "StepFunctionsExecutionId": execution_arn, + }, + "Finding": ( + finding_data + if finding_data + else {"Id": "unknown", "Title": "Step Functions Execution Failure"} + ), + "EventType": orchestrator_input.get( + "detail-type", "Step Functions Failure" + ), + } + + # Add custom action name if present + orchestrator_detail = orchestrator_input.get("detail", {}) + if "actionName" in orchestrator_detail: + transformed_event["CustomActionName"] = orchestrator_detail["actionName"] + + # Add optional finding fields + if finding_data: + _add_optional_finding_fields(transformed_event, finding_data) + + return transformed_event + except Exception as e: + logger.error( + "Critical error transforming Step Functions event", + extra={"error": str(e), "rawEvent": str(raw_event)[:1000]}, + exc_info=True, + ) + # Return a minimal valid event to prevent Lambda failure + return { + "Notification": { + "Message": f"Failed to transform Step Functions event: {str(e)}", + "State": "FAILED", + "Details": str(raw_event)[:500], + "StepFunctionsExecutionId": "unknown", + }, + "Finding": {"Id": "unknown", "Title": "Transformation Error"}, + "EventType": "Error", + } + + +@tracer.capture_lambda_handler # type: ignore[misc] +def lambda_handler(event: Union[Event, dict[str, Any]], context: Any) -> None: + try: + # Type narrowing: check if this is a Step Functions event (raw dict) + if ( + isinstance(event, dict) + and event.get("detail-type") == "Step Functions Execution Status Change" + ): + raw_event = cast(dict[str, Any], event) + logger.info( + "Processing Step Functions failure event", + extra={ + "executionArn": raw_event.get("detail", {}).get("executionArn", ""), + "status": raw_event.get("detail", {}).get("status", ""), + }, + ) + event = _transform_stepfunctions_failure_event(raw_event) + except Exception as e: + logger.error( + "Failed to transform event - continuing with original", + extra={"error": str(e)}, + exc_info=True, + ) + # Don't raise - try to process with original event structure + + # Type assertion: at this point, event should be of type Event + event = cast(Event, event) + + message_prefix, message_suffix = set_message_prefix_and_suffix(event) + stepfunctions_execution_id = _extract_stepfunctions_execution_id(event) + status_from_event = event.get("Notification", {}).get("State", "").upper() + + finding, finding_info = _extract_finding_info(event) + + control_id = _extract_security_control_id(event) + custom_action_name = event.get("CustomActionName", "") + + _process_metrics(event, status_from_event, control_id, custom_action_name) + + notification = _create_notification( + event, status_from_event, stepfunctions_execution_id, finding + ) build_and_send_notification( - event, notification, message_prefix, message_suffix, control_id, finding_info + event, notification, message_prefix, message_suffix, finding_info ) + is_notified_workflow = _is_notified_workflow(event) + + if "Finding" in event and not is_notified_workflow: + _update_finding_remediation_status( + stepfunctions_execution_id, status_from_event, event + ) + def build_and_send_notification( event: Event, notification: sechub_findings.ASRNotification, message_prefix: str, message_suffix: str, - control_id: str, - finding_info: Union[str, dict[str, Any]], + finding_info: Union[str, FindingInfo], ) -> None: notification.message = ( message_prefix + event["Notification"]["Message"] + message_suffix @@ -176,20 +1121,27 @@ def build_and_send_notification( notification.remediation_output = event["Notification"].get("RemediationOutput", "") - notification.finding_link = ( - f"https://{AWS_REGION}.console.{WEB_PARTITION[AWS_PARTITION]}.com/securityhub/home" - f"?region={AWS_REGION}#/controls/{control_id}" - ) - - notification.remediation_status = event["Notification"]["State"].upper() + notification.remediation_status = event["Notification"]["State"] remediation_account_id = "" if isinstance(finding_info, dict): remediation_account_id = ( finding_info["account"] if "account" in finding_info else "" ) + notification.finding_link = get_security_hub_console_url( + finding_info["finding_arn"] + ) - notification.remediation_account_alias = get_account_alias(remediation_account_id) + try: + notification.remediation_account_alias = get_account_alias( + remediation_account_id + ) + except Exception as e: + logger.warning( + f"Unexpected error getting account alias for {remediation_account_id}, using account ID", + extra={"accountId": remediation_account_id, "error": str(e)}, + ) + notification.remediation_account_alias = remediation_account_id or "Unknown" if ( "Details" in event["Notification"] @@ -208,7 +1160,7 @@ def build_and_send_notification( else f"Error generating ticket: {response_reason} - check ticket_generator lambda logs for details" ) - notification.finding_info = finding_info + notification.finding_info = finding_info # type: ignore[assignment] notification.notify() @@ -217,6 +1169,9 @@ def create_and_send_cloudwatch_metrics( ) -> None: try: cloudwatch_metrics = CloudWatchMetrics() + + control_id = control_id or "Unknown" + dimensions = [ { "Name": "Outcome", diff --git a/source/Orchestrator/test/test_check_ssm_doc_state.py b/source/Orchestrator/test/test_check_ssm_doc_state.py index b3d5ae7f..e1adae4e 100644 --- a/source/Orchestrator/test/test_check_ssm_doc_state.py +++ b/source/Orchestrator/test/test_check_ssm_doc_state.py @@ -393,6 +393,90 @@ def test_client_error(mocker): ssmc_stub.deactivate() +def test_throttling_exception(mocker): + """Test that ThrottlingException is re-raised for Step Functions retry""" + test_input = { + "EventType": "Security Hub Findings - Custom Action", + "Finding": { + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1/finding/test-throttle", + "ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub", + "GeneratorId": "aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1", + "AwsAccountId": "111111111111", + "ProductFields": { + "StandardsArn": "arn:aws:securityhub:::standards/aws-foundational-security-best-practices/v/1.0.0", + "StandardsSubscriptionArn": "arn:aws:securityhub:us-east-1:111111111111:subscription/aws-foundational-security-best-practices/v/1.0.0", + "ControlId": "AutoScaling.1", + "StandardsControlArn": "arn:aws:securityhub:us-east-1:111111111111:control/aws-foundational-security-best-practices/v/1.0.0/AutoScaling.1", + "aws/securityhub/ProductName": "Security Hub", + }, + "Resources": [ + { + "Type": "AwsAccount", + "Id": "arn:aws:autoscaling:us-east-1:111111111111:autoScalingGroup:test", + "Partition": "aws", + "Region": "us-east-1", + } + ], + "WorkflowState": "NEW", + "Workflow": {"Status": "NEW"}, + "RecordState": "ACTIVE", + }, + } + + # Use AWSCachedClient to ensure stub is used for all SSM calls + AWS = AWSCachedClient(get_region()) + ssm_c = AWS.get_connection("ssm") + ssmc_stub = Stubber(ssm_c) + + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "Type": "String", + "Value": "AFSBP", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:43.794000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname", + "DataType": "text", + } + }, + { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0/shortname" + }, + ) + ssmc_stub.add_client_error("get_parameter", "ParameterNotFound") + ssmc_stub.add_response( + "get_parameter", + { + "Parameter": { + "Name": "/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "Type": "String", + "Value": "enabled", + "Version": 1, + "LastModifiedDate": "2021-05-11T08:21:44.632000-04:00", + "ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/Solutions/SO0111/aws-foundational-security-best-practices/1.0.0", + "DataType": "text", + } + }, + ) + ssmc_stub.add_client_error("describe_document", "ThrottlingException") + + ssmc_stub.activate() + mocker.patch("check_ssm_doc_state._get_ssm_client", return_value=ssm_c) + + # Verify that ThrottlingException is raised + import pytest + from botocore.exceptions import ClientError + + with pytest.raises(ClientError) as exc_info: + lambda_handler(test_input, create_lambda_context()) + + assert exc_info.value.response["Error"]["Code"] == "ThrottlingException" + + ssmc_stub.deactivate() + + def test_control_remap(mocker): test_input = { "EventType": "Security Hub Findings - Custom Action", diff --git a/source/Orchestrator/test/test_check_ssm_execution.py b/source/Orchestrator/test/test_check_ssm_execution.py index 320af96c..a2afb7a5 100644 --- a/source/Orchestrator/test/test_check_ssm_execution.py +++ b/source/Orchestrator/test/test_check_ssm_execution.py @@ -92,7 +92,7 @@ def get_region(): }, "SSMExecution": { "Message": "AutoScaling.1remediation was successfully invoked via AWS Systems Manager in account 111111111111: 43374019-a309-4627-b8a2-c641e0140262", - "ExecId": "43374019-a309-4627-b8a2-c641e0140262", + "SSMExecutionId": "43374019-a309-4627-b8a2-c641e0140262", "ExecState": "SUCCESS", "Account": "111111111111", "Region": "us-east-1", @@ -100,7 +100,7 @@ def get_region(): "Remediation": { "LogData": [], "RemediationState": "running", - "ExecId": "43374019-a309-4627-b8a2-c641e0140262", + "SSMExecutionId": "43374019-a309-4627-b8a2-c641e0140262", "Message": "Waiting for completion", "AffectedObject": "", "ExecState": "InProgress", @@ -207,7 +207,9 @@ def test_successful_remediation(mocker): ssm_c = boto3.client("ssm") account = "111111111111" test_event["AutomationDocument"]["AccountId"] = account - test_event["SSMExecution"]["ExecId"] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" + test_event["SSMExecution"][ + "SSMExecutionId" + ] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" expected_result = { "affected_object": "UNKNOWN", @@ -250,7 +252,9 @@ def test_execid_parsing_nonsharr(mocker): ssm_c = boto3.client("ssm") account = "111111111111" test_event["AutomationDocument"]["AccountId"] = account - test_event["SSMExecution"]["ExecId"] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" + test_event["SSMExecution"][ + "SSMExecutionId" + ] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" ssmc_stub = Stubber(ssm_c) @@ -271,7 +275,10 @@ def test_execid_parsing_nonsharr(mocker): mocker.patch("check_ssm_execution._get_ssm_client", return_value=ssm_c) automation_exec_info = AutomationExecution( - test_event["SSMExecution"]["ExecId"], account, "foo-bar-baz", "us-east-1" + test_event["SSMExecution"]["SSMExecutionId"], + account, + "foo-bar-baz", + "us-east-1", ) assert automation_exec_info.status == "Success" assert ( @@ -290,7 +297,9 @@ def test_execid_parsing_sharr(mocker): ssm_c = boto3.client("ssm") account = "111111111111" test_event["AutomationDocument"]["AccountId"] = account - test_event["SSMExecution"]["ExecId"] = "795cf453-c41a-48df-aace-fd68fdace188" + test_event["SSMExecution"][ + "SSMExecutionId" + ] = "795cf453-c41a-48df-aace-fd68fdace188" ssmc_stub = Stubber(ssm_c) @@ -311,7 +320,10 @@ def test_execid_parsing_sharr(mocker): mocker.patch("check_ssm_execution._get_ssm_client", return_value=ssm_c) automation_exec_info = AutomationExecution( - test_event["SSMExecution"]["ExecId"], account, "foo-bar-baz", "us-east-1" + test_event["SSMExecution"]["SSMExecutionId"], + account, + "foo-bar-baz", + "us-east-1", ) assert automation_exec_info.status == "Success" assert ( @@ -331,7 +343,9 @@ def test_missing_account_id(mocker): Verifies that system exit occurs when an account ID is missing from event """ ssm_c = boto3.client("ssm") - test_event["SSMExecution"]["ExecId"] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" + test_event["SSMExecution"][ + "SSMExecutionId" + ] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" test_event["SSMExecution"]["Account"] = None ssmc_stub = Stubber(ssm_c) @@ -368,7 +382,9 @@ def test_missing_region(mocker): Verifies that system exit occurs when region is missing """ ssm_c = boto3.client("ssm") - test_event["SSMExecution"]["ExecId"] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" + test_event["SSMExecution"][ + "SSMExecutionId" + ] = "5f12697a-70a5-4a64-83e6-b7d429ec2b17" test_event["SSMExecution"]["Region"] = None ssmc_stub = Stubber(ssm_c) diff --git a/source/Orchestrator/test/test_schedule_remediation.py b/source/Orchestrator/test/test_schedule_remediation.py index a3650dd9..2a5ee715 100644 --- a/source/Orchestrator/test/test_schedule_remediation.py +++ b/source/Orchestrator/test/test_schedule_remediation.py @@ -117,12 +117,14 @@ def test_no_recent_remediation(mocker): current_timestamp = int(datetime.now(timezone.utc).timestamp()) found_timestamp = current_timestamp - 10 + wait_threshold = int(os.environ.get("RemediationWaitTime", "3")) dynamodb_client.put_item( TableName=table_name, Item={ "AccountID-Region": {"S": table_key}, "LastExecutedTimestamp": {"S": str(found_timestamp)}, + "TTL": {"N": str(found_timestamp + wait_threshold)}, }, ) @@ -166,6 +168,7 @@ def test_recent_remediation(mocker): clients = {"dynamodb": dynamodb_client, "stepfunctions": sfn_client} current_timestamp = int(datetime.now(timezone.utc).timestamp()) found_timestamp = current_timestamp + 100 + wait_threshold = int(os.environ.get("RemediationWaitTime", "3")) create_table() dynamodb_client.put_item( @@ -173,6 +176,7 @@ def test_recent_remediation(mocker): Item={ "AccountID-Region": {"S": table_key}, "LastExecutedTimestamp": {"S": str(found_timestamp)}, + "TTL": {"N": str(found_timestamp + wait_threshold)}, }, ) @@ -251,6 +255,171 @@ def test_account_missing_last_executed(mocker): sfn_stub.deactivate() +@mock_aws +def test_past_timestamp_uses_current_time(mocker): + dynamodb_client = boto3.client("dynamodb", config=BOTO_CONFIG) + sfn_client = boto3.client("stepfunctions", config=BOTO_CONFIG) + sfn_stub = Stubber(sfn_client) + clients = {"dynamodb": dynamodb_client, "stepfunctions": sfn_client} + create_table() + + current_timestamp = int(datetime.now(timezone.utc).timestamp()) + found_timestamp = current_timestamp - 2 + wait_threshold = int(os.environ.get("RemediationWaitTime", "3")) + + dynamodb_client.put_item( + TableName=table_name, + Item={ + "AccountID-Region": {"S": table_key}, + "LastExecutedTimestamp": {"S": str(found_timestamp)}, + "TTL": {"N": str(found_timestamp + wait_threshold)}, + }, + ) + + calculated_timestamp = found_timestamp + 3 + + expected_timestamp = max(calculated_timestamp, current_timestamp) + + expected_timestamp_string = datetime.fromtimestamp( + expected_timestamp, timezone.utc + ).strftime(timestampFormat) + + output = {"PlannedTimestamp": expected_timestamp_string} + output.update(remediation_details) + + sfn_stub.add_response( + "send_task_success", + {}, + {"taskToken": body["TaskToken"], "output": json.dumps(output)}, + ) + + sfn_stub.activate() + + with patch(client, side_effect=lambda service, **_: clients[service]): + response = lambda_handler(event, create_lambda_context()) + final_item = dynamodb_client.get_item( + TableName=table_name, Key={"AccountID-Region": {"S": table_key}} + ) + assert final_item["Item"]["LastExecutedTimestamp"]["S"] == str( + expected_timestamp + ) + assert ( + response + == f"Remediation scheduled to execute at {expected_timestamp_string}" + ) + + sfn_stub.deactivate() + + +@mock_aws +def test_expired_ttl_treated_as_new(mocker): + """Test that items with expired TTL are treated as new items.""" + dynamodb_client = boto3.client("dynamodb", config=BOTO_CONFIG) + sfn_client = boto3.client("stepfunctions", config=BOTO_CONFIG) + sfn_stub = Stubber(sfn_client) + clients = {"dynamodb": dynamodb_client, "stepfunctions": sfn_client} + create_table() + + current_timestamp = int(datetime.now(timezone.utc).timestamp()) + # Old timestamp with expired TTL + found_timestamp = current_timestamp - 100 + expired_ttl = current_timestamp - 50 # TTL expired 50 seconds ago + + dynamodb_client.put_item( + TableName=table_name, + Item={ + "AccountID-Region": {"S": table_key}, + "LastExecutedTimestamp": {"S": str(found_timestamp)}, + "TTL": {"N": str(expired_ttl)}, + }, + ) + + current_timestamp_string = datetime.fromtimestamp( + current_timestamp, timezone.utc + ).strftime(timestampFormat) + + output = {"PlannedTimestamp": current_timestamp_string} + output.update(remediation_details) + + sfn_stub.add_response( + "send_task_success", + {}, + {"taskToken": body["TaskToken"], "output": json.dumps(output)}, + ) + + sfn_stub.activate() + + with patch(client, side_effect=lambda service, **_: clients[service]): + response = lambda_handler(event, create_lambda_context()) + final_item = dynamodb_client.get_item( + TableName=table_name, Key={"AccountID-Region": {"S": table_key}} + ) + # Should be treated as new item with current timestamp + assert final_item["Item"]["LastExecutedTimestamp"]["S"] == str( + current_timestamp + ) + assert ( + response + == f"Remediation scheduled to execute at {current_timestamp_string}" + ) + + sfn_stub.deactivate() + + +@mock_aws +def test_missing_ttl_treated_as_new(mocker): + """Test that items without TTL are treated as new items.""" + dynamodb_client = boto3.client("dynamodb", config=BOTO_CONFIG) + sfn_client = boto3.client("stepfunctions", config=BOTO_CONFIG) + sfn_stub = Stubber(sfn_client) + clients = {"dynamodb": dynamodb_client, "stepfunctions": sfn_client} + create_table() + + current_timestamp = int(datetime.now(timezone.utc).timestamp()) + found_timestamp = current_timestamp - 100 + + # Item without TTL (legacy item or TTL not set) + dynamodb_client.put_item( + TableName=table_name, + Item={ + "AccountID-Region": {"S": table_key}, + "LastExecutedTimestamp": {"S": str(found_timestamp)}, + }, + ) + + current_timestamp_string = datetime.fromtimestamp( + current_timestamp, timezone.utc + ).strftime(timestampFormat) + + output = {"PlannedTimestamp": current_timestamp_string} + output.update(remediation_details) + + sfn_stub.add_response( + "send_task_success", + {}, + {"taskToken": body["TaskToken"], "output": json.dumps(output)}, + ) + + sfn_stub.activate() + + with patch(client, side_effect=lambda service, **_: clients[service]): + response = lambda_handler(event, create_lambda_context()) + final_item = dynamodb_client.get_item( + TableName=table_name, Key={"AccountID-Region": {"S": table_key}} + ) + # Should be treated as new item with current timestamp and TTL set + assert final_item["Item"]["LastExecutedTimestamp"]["S"] == str( + current_timestamp + ) + assert "TTL" in final_item["Item"] + assert ( + response + == f"Remediation scheduled to execute at {current_timestamp_string}" + ) + + sfn_stub.deactivate() + + def test_failure(mocker): sfn_client = boto3.client("stepfunctions", config=BOTO_CONFIG) sfn_stub = Stubber(sfn_client) diff --git a/source/Orchestrator/test/test_send_notifications.py b/source/Orchestrator/test/test_send_notifications.py index d7213d47..15e566af 100644 --- a/source/Orchestrator/test/test_send_notifications.py +++ b/source/Orchestrator/test/test_send_notifications.py @@ -1,17 +1,30 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import copy import os +from datetime import datetime, timedelta +from typing import cast import boto3 +import pytest from moto import mock_aws from send_notifications import ( + Event, + RemediationUpdateRequest, + _extract_security_control_id, + _is_notified_workflow, + _is_resolved_item, + calculate_history_ttl_timestamp, create_and_send_cloudwatch_metrics, + get_control_id_from_finding_id, + get_finding_type, lambda_handler, + map_remediation_status, + sanitize_control_id, set_message_prefix_and_suffix, + update_remediation_status_and_history, ) -AWS_REGION = os.getenv("AWS_DEFAULT_REGION", "us-east-1") - default_event = { "Notification": { "State": "SUCCESS", @@ -48,6 +61,67 @@ } +@pytest.fixture(scope="module", autouse=True) +def setup_aws_region(): + original_region = os.environ.get("AWS_REGION") + os.environ["AWS_REGION"] = "us-east-1" + yield + if original_region: + os.environ["AWS_REGION"] = original_region + else: + os.environ.pop("AWS_REGION", None) + + +def setup_ssm_parameters(): + ssm_client = boto3.client("ssm", region_name="us-east-1") + ssm_client.put_parameter( + Name="/Solutions/SO0111/version", + Value="v1.0.0", + Type="String", + ) + ssm_client.put_parameter( + Name="/Solutions/SO0111/sendCloudwatchMetrics", + Value="yes", + Type="String", + ) + return ssm_client + + +def setup_dynamodb_tables(): + dynamodb = boto3.client("dynamodb", region_name="us-east-1") + + # Create findings table + dynamodb.create_table( + TableName="test-findings-table", + KeySchema=[ + {"AttributeName": "findingType", "KeyType": "HASH"}, + {"AttributeName": "findingId", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "findingType", "AttributeType": "S"}, + {"AttributeName": "findingId", "AttributeType": "S"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + + # Create history table + dynamodb.create_table( + TableName="test-history-table", + KeySchema=[ + {"AttributeName": "findingType", "KeyType": "HASH"}, + {"AttributeName": "findingId#executionId", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "findingType", "AttributeType": "S"}, + {"AttributeName": "findingId#executionId", "AttributeType": "S"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + + os.environ["FINDINGS_TABLE_NAME"] = "test-findings-table" + os.environ["HISTORY_TABLE_NAME"] = "test-history-table" + + def setup(mocker): sharr_notification_stub = mocker.stub() sharr_notification_stub.notify = mocker.Mock() @@ -57,6 +131,21 @@ def setup(mocker): ) mocker.patch("send_notifications.CloudWatchMetrics.send_metric", return_value=None) mocker.patch("send_notifications.get_account_alias", return_value="myAccount") + + mock_finding = mocker.Mock() + mock_finding.uuid = "test-uuid" + mock_finding.description = "test description" + mock_finding.standard_name = "test standard" + mock_finding.standard_version = "1.0" + mock_finding.standard_control = "test control" + mock_finding.title = "test title" + mock_finding.region = "us-east-1" + mock_finding.account_id = "123456789012" + mock_finding.arn = "arn:aws:securityhub:us-east-1:111111111111:subscription/cis-aws-foundations-benchmark/v/3.0.0/foobar.1/finding/c605d623-ee6b-460d-9deb-0e8c0551d155" + mocker.patch( + "send_notifications.sechub_findings.Finding", return_value=mock_finding + ) + return sharr_notification_stub @@ -71,7 +160,7 @@ def test_resolved(mocker): assert sharr_notification_stub.remediation_output == "remediation output." assert ( sharr_notification_stub.finding_link - == f"https://{AWS_REGION}.console.aws.amazon.com/securityhub/home?region={AWS_REGION}#/controls/S3.1" + == "https://console.aws.amazon.com/securityhub/home?region=us-east-1#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253Aarn%3Aaws%3Asecurityhub%3Aus-east-1%3A111111111111%3Asubscription%2Fcis-aws-foundations-benchmark%2Fv%2F3.0.0%2Ffoobar.1%2Ffinding%2Fc605d623-ee6b-460d-9deb-0e8c0551d155" ) assert sharr_notification_stub.remediation_account_alias == "myAccount" assert sharr_notification_stub.severity == "INFO" @@ -93,7 +182,7 @@ def test_notification_with_ticketing(mocker): assert sharr_notification_stub.remediation_output == "remediation output." assert ( sharr_notification_stub.finding_link - == f"https://{AWS_REGION}.console.aws.amazon.com/securityhub/home?region={AWS_REGION}#/controls/S3.1" + == "https://console.aws.amazon.com/securityhub/home?region=us-east-1#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253Aarn%3Aaws%3Asecurityhub%3Aus-east-1%3A111111111111%3Asubscription%2Fcis-aws-foundations-benchmark%2Fv%2F3.0.0%2Ffoobar.1%2Ffinding%2Fc605d623-ee6b-460d-9deb-0e8c0551d155" ) assert sharr_notification_stub.remediation_account_alias == "myAccount" assert sharr_notification_stub.severity == "INFO" @@ -114,7 +203,7 @@ def test_notification_with_ticketing_error(mocker): assert sharr_notification_stub.remediation_output == "remediation output." assert ( sharr_notification_stub.finding_link - == f"https://{AWS_REGION}.console.aws.amazon.com/securityhub/home?region={AWS_REGION}#/controls/S3.1" + == "https://console.aws.amazon.com/securityhub/home?region=us-east-1#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253Aarn%3Aaws%3Asecurityhub%3Aus-east-1%3A111111111111%3Asubscription%2Fcis-aws-foundations-benchmark%2Fv%2F3.0.0%2Ffoobar.1%2Ffinding%2Fc605d623-ee6b-460d-9deb-0e8c0551d155" ) assert sharr_notification_stub.remediation_account_alias == "myAccount" assert sharr_notification_stub.severity == "INFO" @@ -144,7 +233,7 @@ def test_wrong_standard(mocker): def test_message_prefix_and_suffix(): event = { "Notification": { - "ExecId": "Test Prefix", + "SSMExecutionId": "Test Prefix", "AffectedObject": "Test Suffix", "RemediationOutput": "remediation output.", }, @@ -159,14 +248,8 @@ def test_message_prefix_and_suffix(): @mock_aws def test_create_and_send_cloudwatch_metrics(): cloudwatch_client = boto3.client("cloudwatch", region_name="us-east-1") - ssm_client = boto3.client("ssm", region_name="us-east-1") + setup_ssm_parameters() os.environ["ENHANCED_METRICS"] = "no" - os.environ["AWS_DEFAULT_REGION"] = "us-east-1" - ssm_client.put_parameter( - Name="/Solutions/SO0111/sendCloudwatchMetrics", - Value="yes", - Type="String", - ) create_and_send_cloudwatch_metrics("Success", "FooBar.1", "myCustomAction") @@ -186,14 +269,8 @@ def test_create_and_send_cloudwatch_metrics(): @mock_aws def test_create_and_send_enhanced_cloudwatch_metrics(): cloudwatch_client = boto3.client("cloudwatch", region_name="us-east-1") - ssm_client = boto3.client("ssm", region_name="us-east-1") + setup_ssm_parameters() os.environ["ENHANCED_METRICS"] = "yes" - os.environ["AWS_DEFAULT_REGION"] = "us-east-1" - ssm_client.put_parameter( - Name="/Solutions/SO0111/sendCloudwatchMetrics", - Value="yes", - Type="String", - ) create_and_send_cloudwatch_metrics("Success", "FooBar.1", "myCustomAction") @@ -208,3 +285,997 @@ def test_create_and_send_enhanced_cloudwatch_metrics(): assert len(dimensions) == 2 assert {"Name": "Outcome", "Value": "Success"} in dimensions assert {"Name": "ControlId", "Value": "FooBar.1"} in dimensions + + +@mock_aws +def test_send_operational_metrics_with_event_type(mocker): + # ARRANGE + setup_ssm_parameters() + setup_dynamodb_tables() + + mock_urlopen = mocker.patch("layer.metrics.urlopen") + sharr_notification_stub = setup(mocker) + + event = cast(Event, copy.deepcopy(default_event)) + event["EventType"] = "CustomAction" + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + + # ACT + lambda_handler(event, {}) + + # ASSERT + mock_urlopen.assert_called_once() + assert sharr_notification_stub.notify.call_count == 1 + + +@mock_aws +def test_send_operational_metrics_without_event_type(mocker): + # ARRANGE + setup_ssm_parameters() + setup_dynamodb_tables() + mock_urlopen = mocker.patch("layer.metrics.urlopen") + sharr_notification_stub = setup(mocker) + + event = cast(Event, copy.deepcopy(default_event)) + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + if "EventType" in event: + del event["EventType"] + + # ACT + lambda_handler(event, {}) + + # ASSERT + mock_urlopen.assert_called_once() + assert sharr_notification_stub.notify.call_count == 1 + + +def test_calculate_history_ttl_timestamp(): + + timestamp = "2024-01-01T00:00:00Z" + ttl = calculate_history_ttl_timestamp(timestamp) + + expected_ttl = int( + ( + datetime.fromisoformat("2024-01-01T00:00:00+00:00") + timedelta(days=365) + ).timestamp() + ) + assert ttl == expected_ttl + + +def test_map_remediation_status(): + + assert map_remediation_status("SUCCESS") == "SUCCESS" + assert map_remediation_status("success") == "SUCCESS" + + assert map_remediation_status("QUEUED") == "IN_PROGRESS" + assert map_remediation_status("RUNNING") == "IN_PROGRESS" + assert map_remediation_status("IN_PROGRESS") == "IN_PROGRESS" + + assert map_remediation_status("FAILED") == "FAILED" + assert map_remediation_status("LAMBDA_ERROR") == "FAILED" + assert map_remediation_status("TIMEOUT") == "FAILED" + assert map_remediation_status("CANCELLED") == "FAILED" + + assert map_remediation_status("UNKNOWN_STATUS") == "FAILED" + + +def test_remediation_update_request_validation(): + + # Test valid request + valid_request = RemediationUpdateRequest( + finding_id="test-finding-id", + execution_id="arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id", + remediation_status="SUCCESS", + finding_type="test-finding-type", + ) + assert valid_request.validate() is True + + # Test invalid request - missing finding_id + invalid_request = RemediationUpdateRequest( + finding_id="", + execution_id="arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id", + remediation_status="SUCCESS", + finding_type="test-finding-type", + ) + assert invalid_request.validate() is False + + # Test invalid request - missing execution_id + invalid_request2 = RemediationUpdateRequest( + finding_id="test-finding-id", + execution_id="", + remediation_status="SUCCESS", + finding_type="test-finding-type", + ) + assert invalid_request2.validate() is False + + +@mock_aws +def test_update_remediation_status_and_history_success(mocker): + + setup_dynamodb_tables() + + mocker.patch( + "send_notifications._try_update_with_existing_history", return_value=True + ) + + request = RemediationUpdateRequest( + finding_id="test-finding-id", + execution_id="arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id", + remediation_status="SUCCESS", + finding_type="test-finding-type", + ) + + update_remediation_status_and_history(request) + + +@mock_aws +def test_update_remediation_status_and_history_fallback(mocker): + + setup_dynamodb_tables() + + mocker.patch( + "send_notifications._try_update_with_existing_history", return_value=False + ) + mocker.patch("send_notifications._create_history_with_finding_update") + + request = RemediationUpdateRequest( + finding_id="test-finding-id", + execution_id="arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id", + remediation_status="SUCCESS", + finding_type="test-finding-type", + ) + + update_remediation_status_and_history(request) + + +@mock_aws +def test_lambda_handler_with_remediation_update(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + sharr_notification_stub = setup(mocker) + mocker.patch("send_notifications.update_remediation_status_and_history") + + event = cast(Event, copy.deepcopy(default_event)) + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + + lambda_handler(event, {}) + + assert sharr_notification_stub.notify.call_count == 1 + + +@mock_aws +def test_update_finding_remediation_status_with_finding_type(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + os.environ["ENHANCED_METRICS"] = "no" + + mock_update = mocker.patch( + "send_notifications.update_remediation_status_and_history" + ) + setup(mocker) + + event = cast(Event, copy.deepcopy(default_event)) + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + event["Finding"]["Compliance"]["SecurityControlId"] = "EC2.1" + event["Finding"][ + "Id" + ] = "arn:aws:securityhub:us-east-1:123456789012:finding/custom-format/test-id" + event["Resources"] = [{"Id": "i-1234567890abcdef0", "Type": "AwsEc2Instance"}] + event["AccountId"] = "123456789012" + event["Region"] = "us-east-1" + event["Severity"] = {"Label": "HIGH"} + + lambda_handler(event, {}) + + mock_update.assert_called_once() + + call_args = mock_update.call_args.args[0] + + assert call_args.finding_type == "EC2.1" + assert ( + call_args.finding_id + == "arn:aws:securityhub:us-east-1:123456789012:finding/custom-format/test-id" + ) + assert ( + call_args.execution_id + == "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + ) + assert call_args.remediation_status == "SUCCESS" + assert call_args.resource_id == "i-1234567890abcdef0" + assert call_args.resource_type == "AwsEc2Instance" + assert call_args.account_id == "123456789012" + assert call_args.region == "us-east-1" + assert call_args.severity == "HIGH" + + +@mock_aws +def test_update_finding_remediation_status_missing_finding_type(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + mock_update = mocker.patch( + "send_notifications.update_remediation_status_and_history" + ) + setup(mocker) + + event = cast(Event, copy.deepcopy(default_event)) + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + + event["Finding"][ + "Id" + ] = "arn:aws:securityhub:us-east-1:123456789012:finding/custom-format/test-id" + if "Compliance" in event["Finding"]: + del event["Finding"]["Compliance"]["SecurityControlId"] + + lambda_handler(event, {}) + + mock_update.assert_called_once() + + call_args = mock_update.call_args.args[0] + + assert call_args.finding_type == "" + + +@mock_aws +def test_update_finding_remediation_status_no_finding_in_event(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + mock_update = mocker.patch( + "send_notifications.update_remediation_status_and_history" + ) + setup(mocker) + + event = { + "Notification": { + "State": "SUCCESS", + "Message": "A Door is Ajar", + "StepFunctionsExecutionId": "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id", + }, + "SecurityStandard": "AFSBP", + "ControlId": "foobar.1", + } + + lambda_handler(event, {}) + + mock_update.assert_not_called() + + +def test_get_control_id_from_finding_id(): + unconsolidated_id = "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0/S3.13/finding/abc123" + result = get_control_id_from_finding_id(unconsolidated_id) + assert result == "aws-foundational-security-best-practices/v/1.0.0/S3.13" + + consolidated_id = "arn:aws:securityhub:us-east-1:123456789012:security-control/S3.13/finding/abc123" + result = get_control_id_from_finding_id(consolidated_id) + assert result == "security-control/S3.13" + + invalid_id = "invalid-finding-id" + result = get_control_id_from_finding_id(invalid_id) + assert result is None + + +def test_sanitize_control_id(): + assert sanitize_control_id("S3.13") == "S3.13" + + assert sanitize_control_id("S3@13#test!") == "S313test" + + assert ( + sanitize_control_id("aws-foundational/v1.0.0/S3.13") + == "aws-foundational/v1.0.0/S3.13" + ) + + +def test_extract_security_control_id_fallback(): + event_with_compliance = cast( + Event, + { + "Finding": { + "Compliance": {"SecurityControlId": "S3.13"}, + "ProductFields": {"ControlId": "S3.14"}, + } + }, + ) + result = _extract_security_control_id(event_with_compliance) + assert result == "S3.13" + + event_with_fallback = cast( + Event, + { + "Finding": { + "Compliance": {"SecurityControlId": ""}, + "ProductFields": {"ControlId": "S3.14"}, + } + }, + ) + result = _extract_security_control_id(event_with_fallback) + assert result == "S3.14" + + event_no_finding: Event = cast(Event, {}) + result = _extract_security_control_id(event_no_finding) + assert result == "" + + +def test_get_finding_type_comprehensive(): + event_with_finding_id = cast( + Event, + { + "Finding": { + "Id": "arn:aws:securityhub:us-east-1:123456789012:security-control/S3.15/finding/abc123", + "Compliance": {"SecurityControlId": "S3.13"}, + "ProductFields": {"ControlId": "S3.14"}, + } + }, + ) + result = get_finding_type(event_with_finding_id) + assert result == "security-control/S3.15" + + event_compliance_fallback = cast( + Event, + { + "Finding": { + "Id": "invalid-finding-id", + "Compliance": {"SecurityControlId": "S3.13"}, + "ProductFields": {"ControlId": "S3.14"}, + } + }, + ) + result = get_finding_type(event_compliance_fallback) + assert result == "S3.13" + + event_product_fields_fallback = cast( + Event, + { + "Finding": { + "Id": "invalid-finding-id", + "Compliance": {"SecurityControlId": ""}, + "ProductFields": {"ControlId": "S3.14"}, + } + }, + ) + result = get_finding_type(event_product_fields_fallback) + assert result == "S3.14" + + event_no_control_id = cast( + Event, + { + "Finding": { + "Id": "invalid-finding-id", + "Compliance": {}, + "ProductFields": {}, + } + }, + ) + result = get_finding_type(event_no_control_id) + assert result == "" + + +def test_is_notified_workflow(): + # Test NOTIFIED workflow with regular event type + notified_event = cast( + Event, + { + "Finding": { + "Workflow": {"Status": "NOTIFIED"}, + "Id": "test-finding-id", + }, + "EventType": "Security Hub Findings - Imported", + }, + ) + assert _is_notified_workflow(notified_event) is True + + # Test NOTIFIED workflow with Custom Action event type - should return False + notified_custom_action_event = cast( + Event, + { + "Finding": { + "Workflow": {"Status": "NOTIFIED"}, + "Id": "test-finding-id", + }, + "EventType": "Security Hub Findings - Custom Action", + }, + ) + assert _is_notified_workflow(notified_custom_action_event) is False + + # Test NOTIFIED workflow with API Action event type - should return False + notified_api_action_event = cast( + Event, + { + "Finding": { + "Workflow": {"Status": "NOTIFIED"}, + "Id": "test-finding-id", + }, + "EventType": "Security Hub Findings - API Action", + }, + ) + assert _is_notified_workflow(notified_api_action_event) is False + + # Test NOTIFIED workflow without EventType - should return True + notified_no_event_type = cast( + Event, + { + "Finding": { + "Workflow": {"Status": "NOTIFIED"}, + "Id": "test-finding-id", + } + }, + ) + assert _is_notified_workflow(notified_no_event_type) is True + + # Test non-NOTIFIED workflow + new_event = cast( + Event, + { + "Finding": { + "Workflow": {"Status": "NEW"}, + "Id": "test-finding-id", + } + }, + ) + assert _is_notified_workflow(new_event) is False + + # Test missing workflow + no_workflow_event = cast( + Event, + { + "Finding": { + "Id": "test-finding-id", + } + }, + ) + assert _is_notified_workflow(no_workflow_event) is False + + # Test empty workflow + empty_workflow_event = cast( + Event, + { + "Finding": { + "Workflow": {}, + "Id": "test-finding-id", + } + }, + ) + assert _is_notified_workflow(empty_workflow_event) is False + + # Test no finding + no_finding_event = cast(Event, {}) + assert _is_notified_workflow(no_finding_event) is False + + # Test workflow is not a dict + invalid_workflow_event = cast( + Event, + { + "Finding": { + "Workflow": "NOTIFIED", # String instead of dict + "Id": "test-finding-id", + } + }, + ) + assert _is_notified_workflow(invalid_workflow_event) is False + + +@mock_aws +def test_lambda_handler_with_product_fields_fallback(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + sharr_notification_stub = setup(mocker) + mock_update = mocker.patch( + "send_notifications.update_remediation_status_and_history" + ) + + event = cast(Event, copy.deepcopy(default_event)) + event["Finding"][ + "Id" + ] = "arn:aws:securityhub:us-east-1:123456789012:finding/custom-format/test-id" + event["Finding"]["Compliance"]["SecurityControlId"] = "" # Empty + event["Finding"]["ProductFields"]["ControlId"] = "S3.13" # Fallback value + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + + lambda_handler(event, {}) + + assert sharr_notification_stub.notify.call_count == 1 + assert ( + sharr_notification_stub.finding_link + == "https://console.aws.amazon.com/securityhub/home?region=us-east-1#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253Aarn%3Aaws%3Asecurityhub%3Aus-east-1%3A111111111111%3Asubscription%2Fcis-aws-foundations-benchmark%2Fv%2F3.0.0%2Ffoobar.1%2Ffinding%2Fc605d623-ee6b-460d-9deb-0e8c0551d155" + ) + + mock_update.assert_called_once() + call_args = mock_update.call_args.args[0] + assert call_args.finding_type == "S3.13" + + +@mock_aws +def test_lambda_handler_notified_workflow_skips_database_updates(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + sharr_notification_stub = setup(mocker) + mock_update = mocker.patch( + "send_notifications.update_remediation_status_and_history" + ) + + event = cast(Event, copy.deepcopy(default_event)) + event["Finding"]["Workflow"] = { + "Status": "NOTIFIED" + } # Set NOTIFIED workflow status + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + + lambda_handler(event, {}) + + assert sharr_notification_stub.notify.call_count == 1 + + mock_update.assert_not_called() + + +@mock_aws +def test_lambda_handler_non_notified_workflow_updates_database(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + sharr_notification_stub = setup(mocker) + mock_update = mocker.patch( + "send_notifications.update_remediation_status_and_history" + ) + + event = cast(Event, copy.deepcopy(default_event)) + event["Finding"]["Workflow"] = {"Status": "NEW"} # Set non-NOTIFIED workflow status + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + + lambda_handler(event, {}) + + assert sharr_notification_stub.notify.call_count == 1 + + mock_update.assert_called_once() + + +def test_security_hub_v2_enabled_finding_link(mocker): + os.environ["SECURITY_HUB_V2_ENABLED"] = "true" + event = default_event + sharr_notification_stub = setup(mocker) + + lambda_handler(event, {}) + + assert sharr_notification_stub.notify.call_count == 1 + assert ( + sharr_notification_stub.finding_link + == "https://console.aws.amazon.com/securityhub/v2/home?region=us-east-1#/findings?search=finding_info.uid%3D%255Coperator%255C%253AEQUALS%255C%253Aarn%3Aaws%3Asecurityhub%3Aus-east-1%3A111111111111%3Asubscription%2Fcis-aws-foundations-benchmark%2Fv%2F3.0.0%2Ffoobar.1%2Ffinding%2Fc605d623-ee6b-460d-9deb-0e8c0551d155" + ) + + # Clean up + del os.environ["SECURITY_HUB_V2_ENABLED"] + + +def test_should_override_to_success_with_not_new_and_resolved(): + event = cast( + Event, + { + "Notification": { + "State": "NOT_NEW", + "Message": "Finding Workflow State is not NEW (RESOLVED).", + }, + "Finding": { + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/test/finding/123", + "Workflow": {"Status": "RESOLVED"}, + }, + }, + ) + + result = _is_resolved_item(event) + + assert result is True + + +def test_should_override_to_success_with_different_state(): + event = cast( + Event, + { + "Notification": { + "State": "QUEUED", + "Message": "Remediation queued", + }, + "Finding": { + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/test/finding/123", + "Workflow": {"Status": "RESOLVED"}, + }, + }, + ) + + result = _is_resolved_item(event) + + assert result is False + + +def test_should_override_to_success_with_different_workflow_status(): + event = cast( + Event, + { + "Notification": { + "State": "NOT_NEW", + "Message": "Finding Workflow State is not NEW (NOTIFIED).", + }, + "Finding": { + "Id": "arn:aws:securityhub:us-east-1:111111111111:subscription/test/finding/123", + "Workflow": {"Status": "NOTIFIED"}, + }, + }, + ) + + result = _is_resolved_item(event) + + assert result is False + + +def test_should_override_to_success_without_finding(): + event = cast( + Event, + { + "Notification": { + "State": "NOT_NEW", + "Message": "Test message", + }, + }, + ) + + result = _is_resolved_item(event) + + assert result is False + + +@mock_aws +def test_lambda_handler_overrides_status_for_resolved_workflow(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + sharr_notification_stub = setup(mocker) + mock_update = mocker.patch( + "send_notifications.update_remediation_status_and_history" + ) + + event = cast(Event, copy.deepcopy(default_event)) + event["Notification"]["State"] = "NOT_NEW" + event["Notification"]["Message"] = "Finding Workflow State is not NEW (RESOLVED)." + event["Finding"]["Workflow"] = {"Status": "RESOLVED"} + event["Notification"][ + "StepFunctionsExecutionId" + ] = "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution-id" + + lambda_handler(event, {}) + + assert sharr_notification_stub.notify.call_count == 1 + + mock_update.assert_called_once() + + call_args = mock_update.call_args[0][0] + assert call_args.remediation_status == "SUCCESS" + assert call_args.error is None + + +def test_extract_finding_fields(): + from send_notifications import _extract_finding_fields + + item = { + "accountId": {"S": "123456789012"}, + "resourceId": {"S": "i-1234567890abcdef0"}, + "resourceType": {"S": "AwsEc2Instance"}, + "resourceTypeNormalized": {"S": "EC2Instance"}, + "severity": {"S": "HIGH"}, + "region": {"S": "us-east-1"}, + "lastUpdatedBy": {"S": "Automated"}, + } + + result = _extract_finding_fields(item) + + assert result["accountId"] == "123456789012" + assert result["resourceId"] == "i-1234567890abcdef0" + assert result["resourceType"] == "AwsEc2Instance" + assert result["resourceTypeNormalized"] == "EC2Instance" + assert result["severity"] == "HIGH" + assert result["region"] == "us-east-1" + assert result["lastUpdatedBy"] == "Automated" + + +def test_extract_finding_fields_partial(): + from send_notifications import _extract_finding_fields + + item = { + "accountId": {"S": "123456789012"}, + "severity": {"S": "MEDIUM"}, + } + + result = _extract_finding_fields(item) + + assert result["accountId"] == "123456789012" + assert result["severity"] == "MEDIUM" + assert "resourceId" not in result + assert "resourceType" not in result + + +def test_extract_finding_fields_empty(): + from typing import Any + + from send_notifications import _extract_finding_fields + + item: dict[str, Any] = {} + result = _extract_finding_fields(item) + + assert result == {} + + +def test_merge_finding_data_into_item(): + from send_notifications import FindingData, _merge_finding_data_into_item + + item = { + "findingId": {"S": "test-finding-id"}, + "accountId": {"S": "original-account"}, + } + + finding_data: FindingData = { + "accountId": "123456789012", + "resourceId": "i-1234567890abcdef0", + "resourceType": "AwsEc2Instance", + "severity": "HIGH", + "region": "us-west-2", + } + + _merge_finding_data_into_item(item, finding_data) + + # Should override existing accountId + assert item["accountId"] == {"S": "123456789012"} + # Should add new fields + assert item["resourceId"] == {"S": "i-1234567890abcdef0"} + assert item["resourceType"] == {"S": "AwsEc2Instance"} + assert item["severity"] == {"S": "HIGH"} + assert item["region"] == {"S": "us-west-2"} + # Should preserve original fields + assert item["findingId"] == {"S": "test-finding-id"} + + +def test_merge_finding_data_into_item_partial(): + from send_notifications import FindingData, _merge_finding_data_into_item + + item = {"findingId": {"S": "test-finding-id"}} + + finding_data: FindingData = { + "accountId": "123456789012", + "severity": "LOW", + } + + _merge_finding_data_into_item(item, finding_data) + + assert item["accountId"] == {"S": "123456789012"} + assert item["severity"] == {"S": "LOW"} + assert "resourceId" not in item + + +def test_parse_orchestrator_input_valid_json(): + """Test _parse_orchestrator_input with valid JSON.""" + from send_notifications import _parse_orchestrator_input + + input_str = '{"detail": {"findings": [{"Id": "test-id"}]}}' + result = _parse_orchestrator_input(input_str) + + assert result == {"detail": {"findings": [{"Id": "test-id"}]}} + + +def test_parse_orchestrator_input_invalid_json(): + from send_notifications import _parse_orchestrator_input + + input_str = "invalid json {{" + result = _parse_orchestrator_input(input_str) + + assert result == {} + + +def test_parse_orchestrator_input_empty_string(): + from send_notifications import _parse_orchestrator_input + + input_str = "" + result = _parse_orchestrator_input(input_str) + + assert result == {} + + +def test_add_optional_finding_fields(): + from send_notifications import Event, _add_optional_finding_fields + + transformed_event: Event = { + "Notification": { + "Message": "test", + "State": "SUCCESS", + }, + "Finding": {"Id": "test-id"}, + } + + finding_data = { + "AwsAccountId": "123456789012", + "Region": "us-east-1", + "Resources": [{"Id": "i-123", "Type": "AwsEc2Instance"}], + "Severity": {"Label": "HIGH"}, + "ProductFields": {"StandardsGuideArn": "arn:aws:securityhub:::ruleset/cis"}, + "Compliance": {"SecurityControlId": "EC2.1"}, + } + + _add_optional_finding_fields(transformed_event, finding_data) + + assert transformed_event["AccountId"] == "123456789012" + assert transformed_event["Region"] == "us-east-1" + assert transformed_event["Resources"] == [{"Id": "i-123", "Type": "AwsEc2Instance"}] + assert transformed_event["Severity"] == {"Label": "HIGH"} + assert transformed_event["SecurityStandard"] == "arn:aws:securityhub:::ruleset/cis" + assert transformed_event["ControlId"] == "EC2.1" + + +def test_add_optional_finding_fields_partial(): + """Test _add_optional_finding_fields with partial data.""" + from send_notifications import Event, _add_optional_finding_fields + + transformed_event: Event = { + "Notification": { + "Message": "test", + "State": "SUCCESS", + }, + "Finding": {"Id": "test-id"}, + } + + finding_data = { + "AwsAccountId": "123456789012", + "Region": "us-west-2", + } + + _add_optional_finding_fields(transformed_event, finding_data) + + assert transformed_event["AccountId"] == "123456789012" + assert transformed_event["Region"] == "us-west-2" + assert "Resources" not in transformed_event + assert "Severity" not in transformed_event + assert "SecurityStandard" not in transformed_event + assert "ControlId" not in transformed_event + + +def test_add_optional_finding_fields_nested_missing(): + """Test _add_optional_finding_fields when nested fields are missing.""" + from send_notifications import Event, _add_optional_finding_fields + + transformed_event: Event = { + "Notification": { + "Message": "test", + "State": "SUCCESS", + }, + "Finding": {"Id": "test-id"}, + } + + finding_data = { + "AwsAccountId": "123456789012", + "ProductFields": {}, # Empty ProductFields + "Compliance": {}, # Empty Compliance + } + + _add_optional_finding_fields(transformed_event, finding_data) + + assert transformed_event["AccountId"] == "123456789012" + assert "SecurityStandard" not in transformed_event + assert "ControlId" not in transformed_event + + +def test_transform_stepfunctions_failure_event_complete(): + from send_notifications import _transform_stepfunctions_failure_event + + raw_event = { + "detail": { + "executionArn": "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution", + "name": "test-execution", + "status": "FAILED", + "cause": "Lambda function failed", + "error": "LambdaError", + "input": '{"detail": {"findings": [{"Id": "test-finding-id", "AwsAccountId": "123456789012", "Region": "us-east-1"}], "actionName": "CustomAction"}, "detail-type": "Custom Action"}', + } + } + + result = _transform_stepfunctions_failure_event(raw_event) + + assert result["Notification"]["State"] == "FAILED" + assert "test-execution" in result["Notification"]["Message"] + assert "LambdaError" in result["Notification"]["Details"] + assert "Lambda function failed" in result["Notification"]["Details"] + assert ( + result["Notification"]["StepFunctionsExecutionId"] + == "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution" + ) + assert result["Finding"]["Id"] == "test-finding-id" + assert result["AccountId"] == "123456789012" + assert result["Region"] == "us-east-1" + assert result["CustomActionName"] == "CustomAction" + assert result["EventType"] == "Custom Action" + + +def test_transform_stepfunctions_failure_event_minimal(): + from send_notifications import _transform_stepfunctions_failure_event + + raw_event = { + "detail": { + "executionArn": "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution", + "status": "TIMEOUT", + } + } + + result = _transform_stepfunctions_failure_event(raw_event) + + assert result["Notification"]["State"] == "TIMEOUT" + assert result["Finding"]["Id"] == "unknown" + assert result["Finding"]["Title"] == "Step Functions Execution Failure" + + +def test_transform_stepfunctions_failure_event_invalid_json(): + from send_notifications import _transform_stepfunctions_failure_event + + raw_event = { + "detail": { + "executionArn": "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution", + "status": "FAILED", + "input": "invalid json {{", + } + } + + result = _transform_stepfunctions_failure_event(raw_event) + + assert result["Notification"]["State"] == "FAILED" + assert result["Finding"]["Id"] == "unknown" + + +def test_transform_stepfunctions_failure_event_exception(mocker): + from send_notifications import _transform_stepfunctions_failure_event + + # Mock logger to avoid exc_info conflict + mocker.patch("send_notifications.logger.error") + + # Pass None to trigger exception + raw_event = None + + result = _transform_stepfunctions_failure_event(raw_event) # type: ignore[arg-type] + + assert result["Notification"]["State"] == "FAILED" + assert "Failed to transform" in result["Notification"]["Message"] + assert result["Finding"]["Id"] == "unknown" + assert result["EventType"] == "Error" + + +@mock_aws +def test_lambda_handler_with_stepfunctions_failure_event(mocker): + setup_ssm_parameters() + setup_dynamodb_tables() + + sharr_notification_stub = setup(mocker) + + raw_event = { + "detail-type": "Step Functions Execution Status Change", + "detail": { + "executionArn": "arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:test-execution", + "status": "FAILED", + "cause": "Lambda function failed", + "input": '{"detail": {"findings": [{"Id": "test-finding-id", "Compliance": {"SecurityControlId": "EC2.1"}}]}}', + }, + } + + lambda_handler(raw_event, {}) + + assert sharr_notification_stub.notify.call_count == 1 + assert sharr_notification_stub.severity == "ERROR" diff --git a/source/Orchestrator/test/test_stepfunctions_event_transformation.py b/source/Orchestrator/test/test_stepfunctions_event_transformation.py new file mode 100644 index 00000000..936f410f --- /dev/null +++ b/source/Orchestrator/test/test_stepfunctions_event_transformation.py @@ -0,0 +1,135 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json + +from send_notifications import _transform_stepfunctions_failure_event + + +def test_transform_stepfunctions_failure_event(): + stepfunctions_event = { + "version": "0", + "id": "12345678-1234-1234-1234-123456789012", + "detail-type": "Step Functions Execution Status Change", + "source": "aws.states", + "account": "123456789012", + "time": "2024-01-01T12:00:00Z", + "region": "us-east-1", + "detail": { + "executionArn": "arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:my-execution", + "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:MyStateMachine", + "name": "my-execution", + "status": "FAILED", + "startDate": 1704110400000, + "stopDate": 1704110460000, + "input": json.dumps( + { + "detail-type": "Security Hub Findings - Imported", + "detail": { + "findings": [ + { + "Id": "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0/S3.1/finding/test-finding", + "AwsAccountId": "123456789012", + "Region": "us-east-1", + "Resources": [ + { + "Type": "AwsS3Bucket", + "Id": "arn:aws:s3:::test-bucket", + } + ], + "Severity": {"Label": "HIGH"}, + "Compliance": {"SecurityControlId": "S3.1"}, + "ProductFields": { + "StandardsGuideArn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.2.0" + }, + } + ], + "actionName": "CustomAction", + }, + } + ), + "cause": "Lambda function failed", + "error": "LambdaError", + }, + } + + result = _transform_stepfunctions_failure_event(stepfunctions_event) + + assert ( + result["Notification"]["Message"] + == "Orchestrator execution failed: arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:my-execution" + ) + assert result["Notification"]["State"] == "FAILED" + assert "Error: LambdaError" in result["Notification"]["Details"] + assert "Cause: Lambda function failed" in result["Notification"]["Details"] + assert ( + result["Notification"]["StepFunctionsExecutionId"] + == "arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:my-execution" + ) + + assert ( + result["Finding"]["Id"] + == "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0/S3.1/finding/test-finding" + ) + assert result["Finding"]["AwsAccountId"] == "123456789012" + assert result["Finding"]["Region"] == "us-east-1" + + assert result["EventType"] == "Security Hub Findings - Imported" + assert result["CustomActionName"] == "CustomAction" + assert result["AccountId"] == "123456789012" + assert result["Region"] == "us-east-1" + assert result["ControlId"] == "S3.1" + + +def test_transform_stepfunctions_timeout_event(): + stepfunctions_event = { + "detail-type": "Step Functions Execution Status Change", + "source": "aws.states", + "detail": { + "executionArn": "arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:timeout-execution", + "name": "timeout-execution", + "status": "TIMED_OUT", + "input": json.dumps( + { + "detail-type": "Security Hub Findings - Imported", + "detail": { + "findings": [ + { + "Id": "test-finding-id", + "AwsAccountId": "123456789012", + } + ] + }, + } + ), + "cause": "Execution timed out", + "error": "", + }, + } + + result = _transform_stepfunctions_failure_event(stepfunctions_event) + + assert result["Notification"]["State"] == "TIMED_OUT" + assert "Cause: Execution timed out" in result["Notification"]["Details"] + + +def test_transform_with_invalid_input(): + stepfunctions_event = { + "detail-type": "Step Functions Execution Status Change", + "source": "aws.states", + "detail": { + "executionArn": "arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:bad-input", + "name": "bad-input", + "status": "FAILED", + "input": "invalid json {{{", + "cause": "Parse error", + "error": "ParseError", + }, + } + + result = _transform_stepfunctions_failure_event(stepfunctions_event) + + # With invalid input, we now return a minimal valid finding instead of empty dict + assert result["Finding"]["Id"] == "unknown" + assert result["Notification"]["State"] == "FAILED" + assert "Parse error" in result["Notification"]["Details"] diff --git a/source/blueprints/cdk/blueprint-stack.ts b/source/blueprints/cdk/blueprint-stack.ts index f1b6e0e7..ff39ff07 100644 --- a/source/blueprints/cdk/blueprint-stack.ts +++ b/source/blueprints/cdk/blueprint-stack.ts @@ -70,7 +70,7 @@ export class BlueprintStack extends Stack { resources: [`arn:${this.partition}:logs:*:${this.account}:log-group:*`], }), new PolicyStatement({ - actions: ['organizations:ListAccounts'], + actions: ['organizations:DescribeAccount'], resources: ['*'], }), ], diff --git a/source/blueprints/jira/cdk/jira-blueprint-stack.ts b/source/blueprints/jira/cdk/jira-blueprint-stack.ts index ada27a5e..3b697135 100644 --- a/source/blueprints/jira/cdk/jira-blueprint-stack.ts +++ b/source/blueprints/jira/cdk/jira-blueprint-stack.ts @@ -9,6 +9,7 @@ import { BlueprintProps, BlueprintStack } from '../../cdk/blueprint-stack'; export class JiraBlueprintStack extends BlueprintStack { constructor(scope: App, id: string, props: BlueprintProps) { super(scope, id, props); + const stack = Stack.of(this); const solutionsBucket = super.getSolutionsBucket(); @@ -58,6 +59,9 @@ export class JiraBlueprintStack extends BlueprintStack { INSTANCE_URI: jiraInstanceURIParam.valueAsString, PROJECT_NAME: jiraProjectKeyParam.valueAsString, SECRET_ARN: secretArnParam.valueAsString, + AWS_ACCOUNT_ID: stack.account, + STACK_ID: stack.stackId, + DISABLE_ACCOUNT_ALIAS_LOOKUP: 'false', }, memorySize: 256, timeout: cdk.Duration.seconds(15), diff --git a/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap b/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap index 3b3349c5..e340b0a2 100644 --- a/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap +++ b/source/blueprints/jira/cdk/test/__snapshots__/jira-blueprint-stack.test.ts.snap @@ -119,6 +119,10 @@ exports[`JiraBlueprintStack Matches snapshot 1`] = ` "Description": "Creates a ticket in the provided Jira project with remediation details.", "Environment": { "Variables": { + "AWS_ACCOUNT_ID": { + "Ref": "AWS::AccountId", + }, + "DISABLE_ACCOUNT_ALIAS_LOOKUP": "false", "INSTANCE_URI": { "Ref": "InstanceURI", }, @@ -134,6 +138,9 @@ exports[`JiraBlueprintStack Matches snapshot 1`] = ` "Ref": "SecretArn", }, "SOLUTION_ID": "SO9999", + "STACK_ID": { + "Ref": "AWS::StackId", + }, }, }, "FunctionName": "Jira-Function-Name", @@ -240,7 +247,7 @@ exports[`JiraBlueprintStack Matches snapshot 1`] = ` }, }, { - "Action": "organizations:ListAccounts", + "Action": "organizations:DescribeAccount", "Effect": "Allow", "Resource": "*", }, diff --git a/source/blueprints/jira/cdk/test/jira-blueprint-stack.test.ts b/source/blueprints/jira/cdk/test/jira-blueprint-stack.test.ts index 26db9738..f332c0b4 100644 --- a/source/blueprints/jira/cdk/test/jira-blueprint-stack.test.ts +++ b/source/blueprints/jira/cdk/test/jira-blueprint-stack.test.ts @@ -117,7 +117,7 @@ describe('JiraBlueprintStack', () => { }, }, { - Action: 'organizations:ListAccounts', + Action: 'organizations:DescribeAccount', Effect: 'Allow', Resource: '*', }, diff --git a/source/blueprints/jira/ticket_generator/jira_ticket_generator.py b/source/blueprints/jira/ticket_generator/jira_ticket_generator.py index d57a7fc4..9b6f2738 100644 --- a/source/blueprints/jira/ticket_generator/jira_ticket_generator.py +++ b/source/blueprints/jira/ticket_generator/jira_ticket_generator.py @@ -168,18 +168,19 @@ def get_api_credentials(secret_arn: str) -> APICredentials: def get_account_alias(account_id: str) -> str: + if not account_id: + return "Unknown" + default_account_alias = account_id + + if os.getenv("DISABLE_ACCOUNT_ALIAS_LOOKUP", "false").lower() == "true": + logger.debug("Account alias lookup disabled via environment variable") + return default_account_alias + try: organizations_client = connect_to_service("organizations") - accounts = [] - - paginator = organizations_client.get_paginator("list_accounts") - for page in paginator.paginate(): - accounts.extend(page["Accounts"]) - return next( - (account["Name"] for account in accounts if account["Id"] == account_id), - default_account_alias, - ) + response = organizations_client.describe_account(AccountId=account_id) + return str(response["Account"]["Name"]) except Exception as e: logger.error(f"encountered error retrieving account alias: {str(e)}") return default_account_alias diff --git a/source/blueprints/poetry.lock b/source/blueprints/poetry.lock index 6f5ff8f3..e7bae610 100644 --- a/source/blueprints/poetry.lock +++ b/source/blueprints/poetry.lock @@ -2,14 +2,14 @@ [[package]] name = "aws-lambda-powertools" -version = "3.9.0" +version = "3.1.0" description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." optional = false -python-versions = "<4.0.0,>=3.9" +python-versions = "<4.0.0,>=3.8" groups = ["main"] files = [ - {file = "aws_lambda_powertools-3.9.0-py3-none-any.whl", hash = "sha256:759a48bcd570274a19b29a481d68b8331481ae6b0bb37c3e4cb80de1b31abc12"}, - {file = "aws_lambda_powertools-3.9.0.tar.gz", hash = "sha256:58a3800066595a9c5c29a99067d106cc4f2820293164af0e68203005e6c4bd16"}, + {file = "aws_lambda_powertools-3.1.0-py3-none-any.whl", hash = "sha256:fdc834678d131e230052ccd684f969be417ce0165d65ee35c053e1a966e46e4c"}, + {file = "aws_lambda_powertools-3.1.0.tar.gz", hash = "sha256:758a8e5d668ae759051d064d542decff777d9c7a0a5612f0c05ab78fb6f20365"}, ] [package.dependencies] @@ -18,11 +18,11 @@ jmespath = ">=1.0.1,<2.0.0" typing-extensions = ">=4.11.0,<5.0.0" [package.extras] -all = ["aws-encryption-sdk (>=3.1.1,<5.0.0)", "aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)", "pydantic (>=2.4.0,<3.0.0)", "pydantic-settings (>=2.6.1,<3.0.0)"] +all = ["aws-encryption-sdk (>=3.1.1,<4.0.0)", "aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)", "pydantic (>=2.0.3,<3.0.0)"] aws-sdk = ["boto3 (>=1.34.32,<2.0.0)"] -datadog = ["datadog-lambda (>=6.106.0,<7.0.0)"] -datamasking = ["aws-encryption-sdk (>=3.1.1,<5.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)"] -parser = ["pydantic (>=2.4.0,<3.0.0)"] +datadog = ["datadog-lambda (>=4.77,<7.0)"] +datamasking = ["aws-encryption-sdk (>=3.1.1,<4.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)"] +parser = ["pydantic (>=2.0.3,<3.0.0)"] redis = ["redis (>=4.4,<6.0)"] tracer = ["aws-xray-sdk (>=2.8.0,<3.0.0)"] validation = ["fastjsonschema (>=2.14.5,<3.0.0)"] @@ -223,5 +223,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = "^3.10" -content-hash = "a7f68afd50ca66929dc5c61dc79d7f4bbd192f80eba85f6efcf8dcd0210228e4" +python-versions = "^3.11" +content-hash = "1386e69017e6b0f831318a0af6748f604fe30ccd16ffd82dadf1e59b2e4f7f8e" diff --git a/source/blueprints/pyproject.toml b/source/blueprints/pyproject.toml index dd85ae29..11e38a87 100644 --- a/source/blueprints/pyproject.toml +++ b/source/blueprints/pyproject.toml @@ -3,8 +3,8 @@ name = "automated_security_response_on_aws" package-mode = false [tool.poetry.dependencies] -aws-lambda-powertools = {version = "^3.1.0", extras = ["tracer"]} -python = "^3.10" +aws-lambda-powertools = {version = "3.1.0", extras = ["tracer"]} +python = "^3.11" [build-system] requires = ["poetry-core"] diff --git a/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts b/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts index ff1454d3..3b242525 100644 --- a/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts +++ b/source/blueprints/servicenow/cdk/servicenow-blueprint-stack.ts @@ -10,6 +10,7 @@ import { BlueprintProps, BlueprintStack } from '../../cdk/blueprint-stack'; export class ServiceNowBlueprintStack extends BlueprintStack { constructor(scope: App, id: string, props: BlueprintProps) { super(scope, id, props); + const stack = cdk.Stack.of(this); const solutionsBucket = super.getSolutionsBucket(); @@ -59,6 +60,9 @@ export class ServiceNowBlueprintStack extends BlueprintStack { INSTANCE_URI: serviceNowInstanceURIParam.valueAsString, TABLE_NAME: serviceNowTableName.valueAsString, SECRET_ARN: secretArnParam.valueAsString, + AWS_ACCOUNT_ID: stack.account, + STACK_ID: stack.stackId, + DISABLE_ACCOUNT_ALIAS_LOOKUP: 'false', }, memorySize: 256, timeout: cdk.Duration.seconds(15), diff --git a/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap b/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap index 06808e2f..8ac23952 100644 --- a/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap +++ b/source/blueprints/servicenow/cdk/test/__snapshots__/servicenow-blueprint-stack.test.ts.snap @@ -120,6 +120,10 @@ exports[`ServiceNowBlueprintStack Matches snapshot 1`] = ` "Description": "Creates a ticket in the provided ServiceNow table with remediation details.", "Environment": { "Variables": { + "AWS_ACCOUNT_ID": { + "Ref": "AWS::AccountId", + }, + "DISABLE_ACCOUNT_ALIAS_LOOKUP": "false", "INSTANCE_URI": { "Ref": "InstanceURI", }, @@ -132,6 +136,9 @@ exports[`ServiceNowBlueprintStack Matches snapshot 1`] = ` "Ref": "SecretArn", }, "SOLUTION_ID": "SO9999", + "STACK_ID": { + "Ref": "AWS::StackId", + }, "TABLE_NAME": { "Ref": "ServiceNowTableName", }, @@ -241,7 +248,7 @@ exports[`ServiceNowBlueprintStack Matches snapshot 1`] = ` }, }, { - "Action": "organizations:ListAccounts", + "Action": "organizations:DescribeAccount", "Effect": "Allow", "Resource": "*", }, diff --git a/source/blueprints/servicenow/cdk/test/servicenow-blueprint-stack.test.ts b/source/blueprints/servicenow/cdk/test/servicenow-blueprint-stack.test.ts index 66c21e8a..ddc09977 100644 --- a/source/blueprints/servicenow/cdk/test/servicenow-blueprint-stack.test.ts +++ b/source/blueprints/servicenow/cdk/test/servicenow-blueprint-stack.test.ts @@ -117,7 +117,7 @@ describe('ServiceNowBlueprintStack', () => { }, }, { - Action: 'organizations:ListAccounts', + Action: 'organizations:DescribeAccount', Effect: 'Allow', Resource: '*', }, diff --git a/source/blueprints/servicenow/ticket_generator/servicenow_ticket_generator.py b/source/blueprints/servicenow/ticket_generator/servicenow_ticket_generator.py index e66897e4..9116ada9 100644 --- a/source/blueprints/servicenow/ticket_generator/servicenow_ticket_generator.py +++ b/source/blueprints/servicenow/ticket_generator/servicenow_ticket_generator.py @@ -165,18 +165,19 @@ def get_api_credentials(secret_arn: str) -> str: def get_account_alias(account_id: str) -> str: + if not account_id: + return "Unknown" + default_account_alias = account_id + + if os.getenv("DISABLE_ACCOUNT_ALIAS_LOOKUP", "false").lower() == "true": + logger.debug("Account alias lookup disabled via environment variable") + return default_account_alias + try: organizations_client = connect_to_service("organizations") - accounts = [] - - paginator = organizations_client.get_paginator("list_accounts") - for page in paginator.paginate(): - accounts.extend(page["Accounts"]) - return next( - (account["Name"] for account in accounts if account["Id"] == account_id), - default_account_alias, - ) + response = organizations_client.describe_account(AccountId=account_id) + return str(response["Account"]["Name"]) except Exception as e: logger.error(f"encountered error retrieving account alias: {str(e)}") return default_account_alias diff --git a/source/data-models/apiActions.ts b/source/data-models/apiActions.ts new file mode 100644 index 00000000..3f5bfccd --- /dev/null +++ b/source/data-models/apiActions.ts @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export type SuppressionResult = { + suppressed: boolean; +}; + +export type RemediationResult = { + remediationStatus: 'IN_PROGRESS' | 'FAILED'; + executionIdsByFindingId?: Map; + error?: string; +}; + +export type ActionResult = SuppressionResult | RemediationResult; diff --git a/source/data-models/finding.ts b/source/data-models/finding.ts new file mode 100644 index 00000000..baa38253 --- /dev/null +++ b/source/data-models/finding.ts @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { z } from 'zod'; + +export const ComparisonOperatorSchema = z.enum([ + 'EQUALS', + 'NOT_EQUALS', + 'CONTAINS', + 'NOT_CONTAINS', + 'GREATER_THAN_OR_EQUAL', + 'LESS_THAN_OR_EQUAL', +]); + +export const StringFilterSchema = z.object({ + FieldName: z.string(), + Filter: z.object({ + Value: z.string(), + Comparison: ComparisonOperatorSchema, + }), +}); + +export const CompositeFilterSchema = z.object({ + Operator: z.enum(['AND', 'OR']), + StringFilters: z.array(StringFilterSchema), +}); + +export const SortCriteriaSchema = z.object({ + Field: z.string(), + SortOrder: z.enum(['asc', 'desc']), +}); + +export const FindingsRequestSchema = z.object({ + Filters: z + .object({ + CompositeFilters: z.array(CompositeFilterSchema).optional(), + CompositeOperator: z.enum(['AND', 'OR']).optional(), + }) + .optional(), + SortCriteria: z.array(SortCriteriaSchema).optional(), + NextToken: z.string().optional(), +}); + +export const FindingsActionRequestSchema = z.object({ + actionType: z.enum(['Suppress', 'Unsuppress', 'Remediate', 'RemediateAndGenerateTicket']), + findingIds: z.array(z.string()).min(1, 'At least one finding ID is required'), +}); + +export type FindingsRequest = z.infer; +export type FindingsActionRequest = z.infer; +export type ComparisonOperator = z.infer; diff --git a/source/data-models/index.ts b/source/data-models/index.ts new file mode 100644 index 00000000..93362476 --- /dev/null +++ b/source/data-models/index.ts @@ -0,0 +1,9 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export * from './user'; +export * from './apiActions'; +export * from './finding'; +export * from './remediation'; +export * from './searchCriteria'; +export * from './schemaTypes'; diff --git a/source/data-models/package-lock.json b/source/data-models/package-lock.json new file mode 100644 index 00000000..e10dc0eb --- /dev/null +++ b/source/data-models/package-lock.json @@ -0,0 +1,41 @@ +{ + "name": "@asr/data-models", + "version": "3.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@asr/data-models", + "version": "3.0.0", + "dependencies": { + "zod": "3.25.76" + }, + "devDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/source/data-models/package.json b/source/data-models/package.json new file mode 100644 index 00000000..a434c43f --- /dev/null +++ b/source/data-models/package.json @@ -0,0 +1,26 @@ +{ + "name": "@asr/data-models", + "version": "3.0.0", + "private": true, + "description": "Shared data models and schemas for Automated Security Response on AWS solution", + "main": "cjs/index.js", + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./cjs/index.js", + "types": "./esm/index.d.ts" + } + }, + "scripts": { + "build": "npm run clean && npm run build:cjs && npm run build:esm", + "build:cjs": "tsc -p tsconfig.cjs.json", + "build:esm": "tsc -p tsconfig.esm.json", + "clean": "rm -rf cjs esm *.js *.d.ts *.js.map" + }, + "dependencies": { + "zod": "3.25.76" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/source/data-models/remediation.ts b/source/data-models/remediation.ts new file mode 100644 index 00000000..e5c9a194 --- /dev/null +++ b/source/data-models/remediation.ts @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { z } from 'zod'; +import { CompositeFilterSchema, SortCriteriaSchema, StringFilterSchema } from './finding'; + +const FiltersSchema = z + .object({ + StringFilters: z.array(StringFilterSchema).optional(), + CompositeFilters: z.array(CompositeFilterSchema).optional(), + CompositeOperator: z.enum(['AND', 'OR']).optional(), + }) + .optional(); + +const BaseRequestSchema = z.object({ + Filters: FiltersSchema, + SortCriteria: z.array(SortCriteriaSchema).optional(), +}); + +export const RemediationsRequestSchema = BaseRequestSchema.extend({ + NextToken: z.string().optional(), +}); + +export const ExportRequestSchema = RemediationsRequestSchema; + +export type RemediationsRequest = z.infer; +export type ExportRequest = z.infer; diff --git a/source/data-models/schemaTypes.ts b/source/data-models/schemaTypes.ts new file mode 100644 index 00000000..7df05232 --- /dev/null +++ b/source/data-models/schemaTypes.ts @@ -0,0 +1,491 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { z } from 'zod'; + +// Enum types for ASFF +export type ASFFComplianceStatus = 'PASSED' | 'WARNING' | 'FAILED' | 'NOT_AVAILABLE'; +export type ASFFSeverity = 'INFORMATIONAL' | 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; +export type ASFFRecordState = 'ACTIVE' | 'ARCHIVED'; +export type ASFFWorkflowStatus = 'NEW' | 'NOTIFIED' | 'RESOLVED' | 'SUPPRESSED'; + +// Severity mapping for numeric values +export const SEVERITY_MAPPING: Record = { + INFORMATIONAL: 0, + LOW: 1, + MEDIUM: 2, + HIGH: 3, + CRITICAL: 4, +}; + +// Zod schemas for runtime validation +export const ASFFSchema = z + .object({ + SchemaVersion: z.literal('2018-10-08'), + Id: z.string(), + ProductArn: z.string(), + ProductName: z.string().optional(), + CompanyName: z.string().optional(), + Region: z.string().optional(), + GeneratorId: z.string(), + AwsAccountId: z.string(), + Types: z.array(z.string()), + FirstObservedAt: z.string().optional(), + LastObservedAt: z.string().optional(), + CreatedAt: z.string(), + UpdatedAt: z.string(), + Severity: z.object({ + Label: z.enum(['INFORMATIONAL', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional(), + Normalized: z.number().optional(), + Original: z.string().optional(), + Product: z.number().optional(), + }), + Confidence: z.number().optional(), + Criticality: z.number().optional(), + Title: z.string(), + Description: z.string().optional(), + Remediation: z + .object({ + Recommendation: z + .object({ + Text: z.string().optional(), + Url: z.string().optional(), + }) + .optional(), + }) + .optional(), + SourceUrl: z.string().optional(), + ProductFields: z.record(z.string()).optional(), + UserDefinedFields: z.record(z.string()).optional(), + Malware: z.array(z.record(z.any())).optional(), + Network: z.record(z.any()).optional(), + NetworkPath: z.array(z.record(z.any())).optional(), + Process: z.record(z.any()).optional(), + ThreatIntelIndicators: z.array(z.record(z.any())).optional(), + Resources: z.array( + z.object({ + Type: z.string(), + Id: z.string(), + Partition: z.string().optional(), + Region: z.string().optional(), + ResourceRole: z.string().optional(), + Tags: z.record(z.string(), z.string().optional()).optional(), + DataClassification: z + .object({ + DetailedResultsLocation: z.string().optional(), + Result: z.record(z.any()).optional(), + }) + .optional(), + Details: z.record(z.any()).optional(), + }), + ), + Compliance: z.object({ + Status: z.enum(['PASSED', 'WARNING', 'FAILED', 'NOT_AVAILABLE']).optional(), + RelatedRequirements: z.array(z.string()).optional(), + StatusReasons: z + .array( + z.object({ + ReasonCode: z.string(), + Description: z.string().optional(), + }), + ) + .optional(), + SecurityControlId: z.string(), + AssociatedStandards: z + .array( + z.object({ + StandardsId: z.string().optional(), + }), + ) + .optional(), + SecurityControlParameters: z + .array( + z.object({ + Name: z.string().optional(), + Value: z.array(z.string()).optional(), + }), + ) + .optional(), + }), + VerificationState: z.string().optional(), + WorkflowState: z.string().optional(), + Workflow: z + .object({ + Status: z.enum(['NEW', 'NOTIFIED', 'RESOLVED', 'SUPPRESSED']).optional(), + }) + .optional(), + RecordState: z.enum(['ACTIVE', 'ARCHIVED']).optional(), + RelatedFindings: z + .array( + z.object({ + ProductArn: z.string(), + Id: z.string(), + }), + ) + .optional(), + Note: z + .object({ + Text: z.string(), + UpdatedBy: z.string(), + UpdatedAt: z.string(), + }) + .optional(), + Vulnerabilities: z.array(z.record(z.any())).optional(), + PatchSummary: z.record(z.any()).optional(), + Action: z.record(z.any()).optional(), + FindingProviderFields: z + .object({ + Confidence: z.number().optional(), + Criticality: z.number().optional(), + RelatedFindings: z + .array( + z.object({ + ProductArn: z.string(), + Id: z.string(), + }), + ) + .optional(), + Severity: z + .object({ + Label: z.enum(['INFORMATIONAL', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional(), + Original: z.string().optional(), + Normalized: z.number().optional(), + }) + .optional(), + Types: z.array(z.string()).optional(), + }) + .optional(), + Sample: z.boolean().optional(), + GeneratorDetails: z + .object({ + Name: z.string().optional(), + Description: z.string().optional(), + Labels: z.array(z.string()).optional(), + }) + .optional(), + AwsAccountName: z.string().optional(), + }) + .passthrough(); + +export const OCSFComplianceSchema = z + .object({ + activity_id: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(99)]), + category_uid: z.number(), + class_uid: z.literal(2003), + severity_id: z.union([ + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), + z.literal(4), + z.literal(5), + z.literal(6), + z.literal(99), + ]), + type_uid: z.number(), + end_time_dt: z.string().optional(), + start_time_dt: z.string().optional(), + time_dt: z.string().optional(), + activity_name: z.enum(['Close', 'Update', 'Create', 'Unknown', 'Other']).optional(), + category_name: z.string().optional(), + class_name: z.string().optional(), + severity: z.enum(['Unknown', 'Informational', 'Low', 'Medium', 'High', 'Critical', 'Fatal', 'Other']).optional(), + type_name: z.string().optional(), + time: z.number(), + cloud: z.object({ + account: z.object({ + uid: z.string(), + }), + provider: z.string().optional(), + region: z.string().optional(), + }), + finding_info: z.object({ + created_time: z.number().optional(), + created_time_dt: z.string().optional(), + desc: z.string().optional(), + first_seen_time: z.number().optional(), + first_seen_time_dt: z.string().optional(), + last_seen_time: z.number().optional(), + last_seen_time_dt: z.string().optional(), + modified_time: z.number().optional(), + modified_time_dt: z.string().optional(), + product_uid: z.string().optional(), + title: z.string().optional(), + types: z.array(z.string()).optional(), + analytic: z + .object({ + category: z.string().optional(), + name: z.string().optional(), + type: z.string().optional(), + type_id: z.number().optional(), + }) + .optional(), + uid: z.string(), + }), + compliance: z.object({ + requirements: z.array(z.string()).optional(), + status: z.string().optional(), + status_code: z.string().optional(), + status_detail: z.string().optional(), + status_id: z.number().optional(), + control: z.string(), + standards: z.array(z.string()), + }), + resources: z.array( + z + .object({ + cloud_partition: z.string().optional(), + region: z.string().optional(), + type: z.string(), + uid: z.string().optional(), + role_id: z.string().optional(), + uid_alt: z.string().optional(), + account_uid: z.string().optional(), + labels: z.array(z.string()).optional(), + name: z.string().optional(), + namespace: z.string().optional(), + tags: z + .array( + z.object({ + name: z.string(), + value: z.string().optional(), + }), + ) + .optional(), + owner: z.object({ + account: z.object({ + name: z.string().optional(), + type: z.string().optional(), + type_id: z.number().optional(), + uid: z.string().optional(), + }), + credential_uid: z.string().optional(), + domain: z.string().optional(), + email_addr: z.string().optional(), + full_name: z.string().optional(), + name: z.string().optional(), + org: z + .object({ + name: z.string().optional(), + ou_name: z.string().optional(), + ou_uid: z.string().optional(), + uid: z.string().optional(), + }) + .optional(), + type: z.string().optional(), + type_id: z.number().optional(), + uid: z.string().optional(), + }), + data: z.record(z.any()).optional(), + }) + .refine((resource) => resource.uid || resource.uid_alt || resource.name, { + message: "At least one of 'uid', 'uid_alt', or 'name' must be defined", + }), + ), + api: z + .object({ + group: z.record(z.any()).optional(), + operation: z.string(), + request: z + .object({ + containers: z.array(z.record(z.any())).optional(), + data: z.record(z.any()).optional(), + flags: z.array(z.string()).optional(), + uid: z.string(), + }) + .optional(), + response: z + .object({ + code: z.number().optional(), + containers: z.array(z.record(z.any())).optional(), + data: z.record(z.any()).optional(), + error: z.string().optional(), + error_message: z.string().optional(), + flags: z.array(z.string()).optional(), + message: z.string().optional(), + }) + .optional(), + service: z.record(z.any()).optional(), + version: z.string().optional(), + }) + .optional(), + remediation: z + .object({ + desc: z.string().optional(), + kb_articles: z.array(z.string()).optional(), + }) + .optional(), + confidence: z.string().optional(), + confidence_id: z.number().optional(), + confidence_score: z.number().optional(), + count: z.number().optional(), + duration: z.number().optional(), + end_time: z.number().optional(), + message: z.string().optional(), + raw_data: z.string().optional(), + start_time: z.number().optional(), + status: z.enum(['Unknown', 'New', 'In Progress', 'Suppressed', 'Resolved', 'Archived', 'Other']).optional(), + status_code: z.string().optional(), + status_detail: z.string().optional(), + status_id: z.number().optional(), + timezone_offset: z.number().optional(), + metadata: z + .object({ + correlation_uid: z.string().optional(), + event_code: z.string().optional(), + extension: z.record(z.any()).optional(), + labels: z.array(z.string()).optional(), + logged_time: z.number().optional(), + modified_time: z.number().optional(), + original_time: z.string().optional(), + processed_time: z.number().optional(), + product: z + .object({ + feature: z + .object({ + name: z.string().optional(), + uid: z.string().optional(), + version: z.string().optional(), + }) + .optional(), + lang: z.string().optional(), + name: z.string().optional(), + path: z.string().optional(), + uid: z.string().optional(), + url_string: z.string().optional(), + vendor_name: z.string().optional(), + version: z.string().optional(), + }) + .optional(), + profiles: z.array(z.string()).optional(), + sequence: z.number().optional(), + uid: z.string().optional(), + version: z.string().optional(), + }) + .optional(), + observables: z + .array( + z.object({ + name: z.string().optional(), + reputation: z + .object({ + base_score: z.number().optional(), + provider: z.string().optional(), + score: z.string().optional(), + score_id: z.number().optional(), + }) + .optional(), + type: z.string().optional(), + type_id: z.number().optional(), + value: z.string().optional(), + }), + ) + .optional(), + enrichments: z + .array( + z.object({ + data: z.record(z.any()).optional(), + name: z.string().optional(), + provider: z.string().optional(), + type: z.string().optional(), + value: z.string().optional(), + }), + ) + .optional(), + vendor_attributes: z + .object({ + severity: z + .enum(['Unknown', 'Informational', 'Low', 'Medium', 'High', 'Critical', 'Fatal', 'Other']) + .optional(), + severity_id: z + .union([ + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), + z.literal(4), + z.literal(5), + z.literal(6), + z.literal(99), + ]) + .optional(), + }) + .optional(), + }) + .passthrough(); + +const RemediationStatusEnum = z.enum(['NOT_STARTED', 'SUCCESS', 'IN_PROGRESS', 'FAILED']); + +// Generate remediation status type from Zod schema +export type remediationStatus = z.infer; + +export interface FindingAbstractData { + findingType: string; + findingId: string; + accountId: string; + resourceId: string; + resourceType: string; + resourceTypeNormalized: string; + severity: string; + region: string; + remediationStatus: remediationStatus; + lastUpdatedTime: string; + error?: string; + executionId?: string; +} +// Base interface for fields that should be in API response +export interface FindingBaseData extends FindingAbstractData { + findingDescription: string; + securityHubUpdatedAtTime: string; + suppressed: boolean; + creationTime: string; +} + +// API response +export interface FindingApiResponse extends FindingBaseData { + consoleLink: string; +} + +export interface FindingTableItem extends FindingBaseData { + // Table-specific fields that shouldn't be exposed in API + 'securityHubUpdatedAtTime#findingId': string; + 'severityNormalized#securityHubUpdatedAtTime#findingId': string; + findingJSON: Uint8Array; + findingIdControl: string; + FINDING_CONSTANT: 'finding'; + lastUpdatedBy?: string; + expireAt: number; + severityNormalized: number; +} + +// Custom error for invalid finding schemas +export class InvalidFindingSchemaError extends Error { + constructor(supportedSchemas: string[]) { + super(`Finding schema is not ${supportedSchemas.join(' or ')}.`); + this.name = 'InvalidFindingSchemaError'; + } +} + +// Remediation History schema and types +export interface RemediationHistoryBaseData extends FindingAbstractData { + lastUpdatedBy: string; + error?: string; +} + +// API response for remediation history +export interface RemediationHistoryApiResponse extends RemediationHistoryBaseData { + consoleLink: string; +} + +// Table item for remediation history +export interface RemediationHistoryTableItem extends RemediationHistoryBaseData { + // Table-specific fields + 'findingId#executionId': string; + 'lastUpdatedTime#findingId': string; + REMEDIATION_CONSTANT: 'remediation'; + expireAt: number; +} + +// Generate TypeScript types from Zod schemas +export type ASFFFinding = z.infer; +export type OCSFComplianceFinding = z.infer; diff --git a/source/data-models/searchCriteria.ts b/source/data-models/searchCriteria.ts new file mode 100644 index 00000000..0b1f40cc --- /dev/null +++ b/source/data-models/searchCriteria.ts @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ComparisonOperator } from './finding'; + +export type PaginationAttributeValue = string | number | boolean | null | Uint8Array; + +export interface SearchFilter { + fieldName: string; + value: string; + comparison: ComparisonOperator; +} + +export interface SearchCriteria { + filters: SearchFilter[]; + sortField?: string; + sortOrder?: 'asc' | 'desc'; + pageSize: number; + nextToken?: string; +} + +export interface SearchResult { + items: T[]; + nextToken?: string; + totalCount?: number; +} + +export interface PaginationToken { + [key: string]: PaginationAttributeValue; +} diff --git a/source/data-models/tsconfig.cjs.json b/source/data-models/tsconfig.cjs.json new file mode 100644 index 00000000..c2cd3439 --- /dev/null +++ b/source/data-models/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./cjs", + "skipLibCheck": true, + }, + "include": ["*.ts"], + "exclude": ["node_modules"] +} diff --git a/source/data-models/tsconfig.esm.json b/source/data-models/tsconfig.esm.json new file mode 100644 index 00000000..4f698fc4 --- /dev/null +++ b/source/data-models/tsconfig.esm.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "outDir": "./esm", + "skipLibCheck": true, + }, + "include": ["*.ts"], + "exclude": ["node_modules"] +} diff --git a/source/data-models/user.ts b/source/data-models/user.ts new file mode 100644 index 00000000..0ab6243d --- /dev/null +++ b/source/data-models/user.ts @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { z } from 'zod'; + +// Account IDs validation schema +export const accountIdsSchema = z.array(z.string().regex(/^\d{12}$/)).min(1); + +// User type constants +export const USER_TYPE_ACCOUNT_OPERATOR = 'account-operator' as const; +export const USER_TYPE_DELEGATED_ADMIN = 'delegated-admin' as const; +export const USER_TYPE_ADMIN = 'admin' as const; + +// Base user schema +export const GeneralUserSchema = z.object({ + email: z.string().email(), + invitedBy: z.union([z.string().email(), z.literal('system')]), + invitationTimestamp: z.string().datetime(), + status: z.enum(['Invited', 'Confirmed']), + type: z.string(), +}); + +// Specific user type schemas +export const AccountOperatorUserSchema = GeneralUserSchema.extend({ + accountIds: accountIdsSchema, + type: z.literal(USER_TYPE_ACCOUNT_OPERATOR), +}); + +export const DelegatedAdminUserSchema = GeneralUserSchema.extend({ + type: z.literal(USER_TYPE_DELEGATED_ADMIN), +}); + +export const AdminUserSchema = GeneralUserSchema.extend({ + type: z.literal(USER_TYPE_ADMIN), +}); + +// User account mapping schema (from lambda) +export const UserAccountMappingSchema = z.object({ + userId: z.string().email(), + accountIds: accountIdsSchema, + invitedBy: z.union([z.string().email(), z.literal('system')]), + invitationTimestamp: z.string().datetime(), + lastModifiedBy: z.string().email().optional(), + lastModifiedTimestamp: z.string().datetime().optional(), +}); + +// Request schemas +export const InviteUserRequest = z + .object({ + accountIds: accountIdsSchema.optional(), + role: z.enum(['AccountOperator', 'DelegatedAdmin']), + email: z.string().email(), + }) + .strict(); + +export const PutUserRequest = z + .object({ + type: z.string(), // Required for business logic validation + accountIds: accountIdsSchema, // Required in API calls + email: z.string().email(), + status: z.enum(['Invited', 'Confirmed']).optional(), + }) + .strict(); + +// Type exports +export type DelegatedAdminUser = z.infer; +export type AccountOperatorUser = z.infer; +export type AdminUser = z.infer; +export type UserAccountMapping = z.infer; +export type userAccountIds = z.infer; +export type User = DelegatedAdminUser | AccountOperatorUser | AdminUser; +export type InviteUserRequest = z.infer; +export type PutUserRequest = z.infer; diff --git a/source/lambdas/api/README.md b/source/lambdas/api/README.md new file mode 100644 index 00000000..ff4242dc --- /dev/null +++ b/source/lambdas/api/README.md @@ -0,0 +1,16 @@ +# API Lambda Functions + +Each Lambda function backing ASR's API is using this code bundle. Each individual lambda function has a separate entry +point in the /handlers directory. + +## Structure + +``` +├── clients/ # Client classes to communicate with external systems, e.g. S3 +│ └── s3.ts +├── handlers/ # Entry points for the different Lambda functiuns that use this code bundle +│ ├── findings.ts # handler for all /findings API endpoints +│ └── deployWebui.ts # CustomResource to deploy the WebUI +├── models/ # Data models. Need to be in sync with the corresponding models in the webio. +│ └── finding.ts +``` diff --git a/source/lambdas/api/__tests__/clients/s3.test.ts b/source/lambdas/api/__tests__/clients/s3.test.ts new file mode 100644 index 00000000..272608c1 --- /dev/null +++ b/source/lambdas/api/__tests__/clients/s3.test.ts @@ -0,0 +1,252 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { mockClient } from 'aws-sdk-client-mock'; +import { CopyObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { ASRS3Client } from '../../clients/ASRS3Client'; +import { MAX_PRESIGNED_URL_EXPIRY_SECONDS } from '../../../common/constants/apiConstant'; + +const s3Mock = mockClient(S3Client); + +describe('S3 Service', () => { + let s3Service: ASRS3Client; + + beforeEach(() => { + s3Mock.reset(); + s3Service = new ASRS3Client(); + process.env.PRESIGNED_URL_TTL_DAYS = '7'; + }); + + afterEach(() => { + delete process.env.PRESIGNED_URL_TTL_DAYS; + }); + + describe('readJsonFile', () => { + it('should read and parse JSON file successfully', async () => { + // ARRANGE + const bucketName = 'test-bucket'; + const fileName = 'test.json'; + const jsonContent = { key: 'value', number: 123 }; + const jsonString = JSON.stringify(jsonContent); + + s3Mock + .on(GetObjectCommand, { + Bucket: bucketName, + Key: fileName, + }) + .resolves({ + Body: { transformToString: async () => jsonString } as any, + }); + + // ACT + const result = await s3Service.readJsonFile(bucketName, fileName); + + // ASSERT + expect(result).toEqual(jsonContent); + expect(s3Mock.commandCalls(GetObjectCommand)).toHaveLength(1); + }); + + it('should throw error when no body in response', async () => { + // ARRANGE + const bucketName = 'test-bucket'; + const fileName = 'test.json'; + + s3Mock + .on(GetObjectCommand, { + Bucket: bucketName, + Key: fileName, + }) + .resolves({ + Body: undefined, + }); + + // ACT & ASSERT + await expect(s3Service.readJsonFile(bucketName, fileName)).rejects.toThrow('No body in S3 response'); + }); + }); + + describe('copyFile', () => { + it('should copy file successfully', async () => { + // ARRANGE + const sourceBucket = 'source-bucket'; + const targetBucket = 'target-bucket'; + const sourcePrefix = 'source/'; + const targetPrefix = 'target/'; + const fileName = 'test.txt'; + + s3Mock + .on(CopyObjectCommand, { + CopySource: `${sourceBucket}/${sourcePrefix}${fileName}`, + Bucket: targetBucket, + Key: `${targetPrefix}${fileName}`, + }) + .resolves({}); + + // ACT + await s3Service.copyFile(sourceBucket, targetBucket, sourcePrefix, targetPrefix, fileName); + + // ASSERT + expect(s3Mock.commandCalls(CopyObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(CopyObjectCommand)[0].args[0].input).toEqual({ + CopySource: `${sourceBucket}/${sourcePrefix}${fileName}`, + Bucket: targetBucket, + Key: `${targetPrefix}${fileName}`, + }); + }); + + it('should handle copy errors', async () => { + // ARRANGE + const sourceBucket = 'source-bucket'; + const targetBucket = 'target-bucket'; + const sourcePrefix = 'source/'; + const targetPrefix = 'target/'; + const fileName = 'test.txt'; + + const error = new Error('Access denied'); + error.name = 'AccessDenied'; + s3Mock.on(CopyObjectCommand).rejects(error); + + // ACT & ASSERT + await expect( + s3Service.copyFile(sourceBucket, targetBucket, sourcePrefix, targetPrefix, fileName), + ).rejects.toThrow('Access denied'); + }); + }); + + describe('writeJsonAsFile', () => { + it('should write JSON object as file successfully', async () => { + // ARRANGE + const bucketName = 'test-bucket'; + const fileName = 'test.json'; + const jsonObject = { key: 'value', number: 123 }; + + s3Mock + .on(PutObjectCommand, { + Bucket: bucketName, + Key: fileName, + Body: JSON.stringify(jsonObject), + ContentType: 'application/json', + Metadata: { + 'Content-Type': 'application/json', + }, + }) + .resolves({}); + + // ACT + await s3Service.writeJsonAsFile(bucketName, fileName, jsonObject); + + // ASSERT + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(PutObjectCommand)[0].args[0].input).toEqual({ + Bucket: bucketName, + Key: fileName, + Body: JSON.stringify(jsonObject), + ContentType: 'application/json', + Metadata: { + 'Content-Type': 'application/json', + }, + }); + }); + + it('should handle write errors', async () => { + // ARRANGE + const bucketName = 'test-bucket'; + const fileName = 'test.json'; + const jsonObject = { key: 'value' }; + + const error = new Error('Access denied'); + s3Mock.on(PutObjectCommand).rejects(error); + + // ACT & ASSERT + await expect(s3Service.writeJsonAsFile(bucketName, fileName, jsonObject)).rejects.toThrow('Access denied'); + }); + }); + + describe('uploadCsvAndGeneratePresignedUrl', () => { + it('should upload CSV and generate presigned URL successfully', async () => { + // ARRANGE + const bucketName = 'test-bucket'; + const fileName = 'test-export.csv'; + const csvContent = 'header1,header2\nvalue1,value2'; + + s3Mock + .on(PutObjectCommand, { + Bucket: bucketName, + Key: fileName, + Body: csvContent, + ContentType: 'text/csv', + ContentDisposition: `attachment; filename="${fileName}"`, + }) + .resolves({}); + + // ACT + const result = await s3Service.uploadCsvAndGeneratePresignedUrl(bucketName, fileName, csvContent); + + // ASSERT + expect(result).toMatch(/^https:\/\/.*\.s3\..*\.amazonaws\.com\/.*\?.*$/); // Real presigned URL format + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(PutObjectCommand)[0].args[0].input).toEqual({ + Bucket: bucketName, + Key: fileName, + Body: csvContent, + ContentType: 'text/csv', + ContentDisposition: `attachment; filename="${fileName}"`, + }); + }); + + it('should use custom TTL from environment variable', async () => { + // ARRANGE + process.env.PRESIGNED_URL_TTL_DAYS = '3'; + const bucketName = 'test-bucket'; + const fileName = 'test-export.csv'; + const csvContent = 'header1,header2\nvalue1,value2'; + + s3Mock.on(PutObjectCommand).resolves({}); + + // ACT + const result = await s3Service.uploadCsvAndGeneratePresignedUrl(bucketName, fileName, csvContent); + + // ASSERT + expect(result).toMatch(/^https:\/\/.*\.s3\..*\.amazonaws\.com\/.*\?.*$/); // Real presigned URL format + // The TTL is embedded in the URL, so we can verify it by checking the Expires parameter + const url = new URL(result); + const expires = url.searchParams.get('X-Amz-Expires'); + expect(expires).toBe('86400'); + }); + + it('should enforce AWS maximum of 7 days', async () => { + // ARRANGE + process.env.PRESIGNED_URL_TTL_DAYS = '10'; // More than AWS maximum + const bucketName = 'test-bucket'; + const fileName = 'test-export.csv'; + const csvContent = 'header1,header2\nvalue1,value2'; + + s3Mock.on(PutObjectCommand).resolves({}); + + // ACT + const result = await s3Service.uploadCsvAndGeneratePresignedUrl(bucketName, fileName, csvContent); + + // ASSERT + expect(result).toMatch(/^https:\/\/.*\.s3\..*\.amazonaws\.com\/.*\?.*$/); // Real presigned URL format + // The TTL should be capped at 1 day + const url = new URL(result); + const expires = url.searchParams.get('X-Amz-Expires'); + expect(expires).toBe(MAX_PRESIGNED_URL_EXPIRY_SECONDS.toString()); + }); + + it('should handle upload errors', async () => { + // ARRANGE + const bucketName = 'test-bucket'; + const fileName = 'test-export.csv'; + const csvContent = 'header1,header2\nvalue1,value2'; + + const error = new Error('Upload failed'); + s3Mock.on(PutObjectCommand).rejects(error); + + // ACT & ASSERT + await expect(s3Service.uploadCsvAndGeneratePresignedUrl(bucketName, fileName, csvContent)).rejects.toThrow( + 'Upload failed', + ); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/handlers/apiHandler.test.ts b/source/lambdas/api/__tests__/handlers/apiHandler.test.ts new file mode 100644 index 00000000..0e0ee3cd --- /dev/null +++ b/source/lambdas/api/__tests__/handlers/apiHandler.test.ts @@ -0,0 +1,1235 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { mockClient } from 'aws-sdk-client-mock'; +import { + CognitoIdentityProviderClient, + ListUsersCommand, + AdminCreateUserCommand, + AdminAddUserToGroupCommand, + AdminGetUserCommand, + AdminListGroupsForUserCommand, + AdminDeleteUserCommand, +} from '@aws-sdk/client-cognito-identity-provider'; +import 'aws-sdk-client-mock-jest'; +import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb'; +import { UserAccountMapping } from '@asr/data-models'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; +import { userAccountMappingTableName } from '../../../common/__tests__/envSetup'; +import { createMockEvent, createMockContext, TEST_REQUEST_CONTEXT } from '../utils'; +import { handler, createResponse } from '../../handlers/apiHandler'; +import { FORBIDDEN_ERROR_MESSAGE } from '../../../common/utils/httpErrors'; +import { setupMetricsMocks, cleanupMetricsMocks } from '../../../common/__tests__/metricsMockSetup'; + +const cognitoMock = mockClient(CognitoIdentityProviderClient); + +const TEST_EVENT_ORIGIN = process.env.WEB_UI_URL; + +const STANDARD_HEADERS = { + 'Content-Type': 'application/json', + Origin: TEST_EVENT_ORIGIN, +}; + +const EXPECTED_CORS_HEADERS = { + 'Access-Control-Allow-Origin': TEST_EVENT_ORIGIN, + 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', +}; + +const expectCorsHeaders = (result: any) => { + expect(result.headers).toEqual(expect.objectContaining(EXPECTED_CORS_HEADERS)); +}; + +describe('Top-level routing', () => { + beforeAll(async () => { + await DynamoDBTestSetup.initialize(); + await DynamoDBTestSetup.createUserAccountMappingTable(userAccountMappingTableName); + }); + + afterAll(async () => { + await DynamoDBTestSetup.deleteTable(userAccountMappingTableName); + cleanupMetricsMocks(); + }); + + beforeEach(async () => { + await DynamoDBTestSetup.clearTable(userAccountMappingTableName, 'userAccountMapping'); + process.env.USER_POOL_ID = 'test-user-pool-id'; + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME = userAccountMappingTableName; + + cognitoMock.reset(); + setupMetricsMocks(); + + cognitoMock.on(AdminGetUserCommand).callsFake((input) => { + const username = input.Username; + return Promise.resolve({ + Username: username, + UserAttributes: [ + { Name: 'email', Value: username }, + { Name: 'custom:invitedBy', Value: 'system@example.com' }, + ], + UserCreateDate: new Date(), + UserStatus: 'CONFIRMED', + }); + }); + + cognitoMock.on(AdminListGroupsForUserCommand).callsFake((input) => { + const username = input.Username; + let groups = []; + + if (username?.includes('admin-user') || username?.includes('admin@') || username?.includes('super@')) { + groups = [{ GroupName: 'AdminGroup' }]; + } else if (username?.includes('delegated@')) { + groups = [{ GroupName: 'DelegatedAdminGroup' }]; + } else if (username?.includes('operator@')) { + groups = [{ GroupName: 'AccountOperatorGroup' }]; + } else { + groups = [{ GroupName: 'AdminGroup' }]; + } + + return Promise.resolve({ Groups: groups }); + }); + cognitoMock.on(ListUsersCommand).resolves({ Users: [] }); + cognitoMock.on(AdminCreateUserCommand).resolves({ User: { Username: 'new-user@example.com' } }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + }); + + describe('general', () => { + it('should reject requests with x-amzn-requestid header', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'GET', + path: '/users', + headers: { + ...STANDARD_HEADERS, + authorization: 'Bearer valid-token', + 'x-amzn-requestid': 'test-request-id', + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe('X-Amzn-Requestid header is not allowed'); + }); + + it('should reject requests with X-Amzn-Requestid header (case insensitive)', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'GET', + path: '/users', + headers: { + ...STANDARD_HEADERS, + authorization: 'Bearer valid-token', + 'X-Amzn-Requestid': 'test-request-id', + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe('X-Amzn-Requestid header is not allowed'); + }); + + it('should reject requests with x-amz-request-id header', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'GET', + path: '/users', + headers: { + ...STANDARD_HEADERS, + authorization: 'Bearer valid-token', + 'x-amz-request-id': 'test-request-id', + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe('X-Amzn-Requestid header is not allowed'); + }); + + it('should handle Unauthorized when authorization claims are missing', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + ...STANDARD_HEADERS, + authorization: 'Bearer valid-token', + }, + body: JSON.stringify({ actionType: 'Suppress', findingIds: ['finding-1'] }), + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(401); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Could not read claims./); + }); + it('should create proper API Gateway response', () => { + // ARRANGE + const statusCode = 200; + const body = { message: 'success' }; + const corsHeaders = { 'Access-Control-Allow-Origin': '*' }; + + // ACT + const response = createResponse(statusCode, body, corsHeaders); + + // ASSERT + expect(response.statusCode).toBe(200); + expect(response.headers).toEqual(corsHeaders); + expect(response.body).toBe(JSON.stringify(body)); + }); + + it('should handle unsupported route', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'POST', + path: '/unsupported', + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'myusername', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(404); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Method .+ not found./); + }); + }); + + describe('users routes', () => { + it('should handle ForbiddenError when claims are missing in GET /users', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'GET', + path: '/users', + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'some-claim': 'some-claim-value', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(401); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Could not read claims./); + }); + + it('should handle ForbiddenError when username is missing from claims in GET /users', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + httpMethod: 'GET', + path: '/users', + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(401); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Could not read claims./); + }); + + it('should handle ForbiddenError when claims are missing in POST /users', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'POST', + path: '/users', + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'some-claim': 'some-claim-value', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(401); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Could not read claims./); + }); + + it('should handle ForbiddenError when claims are missing in PUT /users/{id}', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'PUT', + path: '/users/user@example.com', + pathParameters: { id: 'user@example.com' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'some-claim': 'some-claim-value', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(401); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Could not read claims./); + }); + + it('should handle ForbiddenError when claims are missing in DELETE /users/{id}', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + httpMethod: 'DELETE', + path: '/users/user@example.com', + pathParameters: { id: 'user@example.com' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'some-claim': 'some-claim-value', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(401); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Could not read claims./); + }); + + it('should route GET /users request successfully', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'GET', + path: '/users', + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(ListUsersCommand).resolves({ + Users: [], + }); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + expectCorsHeaders(result); + expect(result.body).toBeDefined(); + }); + + it('should route POST /users request successfully for AdminGroup creating DelegatedAdmin', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'POST', + path: '/users', + body: JSON.stringify({ + email: 'delegated@example.com', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ User: { Username: 'delegated@example.com' } }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + expectCorsHeaders(result); + const body = result.body; + expect(body).toBe(JSON.stringify({ message: 'User invited successfully', email: 'delegated@example.com' })); + }); + + it('should route POST /users request successfully for AdminGroup creating AccountOperator with DynamoDB validation', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'POST', + path: '/users', + body: JSON.stringify({ + email: 'operator@example.com', + role: 'AccountOperator', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ User: { Username: 'operator@example.com' } }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + expectCorsHeaders(result); + const body = result.body; + expect(body).toBe(JSON.stringify({ message: 'User invited successfully', email: 'operator@example.com' })); + + // Verify UserAccountMapping was created in DynamoDB + const dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.userId).toBe('operator@example.com'); + expect(getResponse.Item?.accountIds).toEqual(['123456789012']); + }); + + it('should handle UnauthorizedError with 401 status', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'GET', + path: '/users', + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(401); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Could not read claims./); + }); + + it('should handle ForbiddenError with 403 status', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + httpMethod: 'GET', + path: '/users', + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['RegularUserGroup'], + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(403); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe(FORBIDDEN_ERROR_MESSAGE); + }); + + it('should handle generic errors with 400 status', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + httpMethod: 'GET', + path: '/users', + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(ListUsersCommand).rejects(new Error('Service error')); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe('An unexpected error occurred.'); + }); + + it('should handle DelegatedAdmin access error with proper message', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + queryStringParameters: { type: 'admins' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(403); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe( + 'DelegatedAdminGroup can only fetch Account Operators. You must provide the "type" query parameter with value "accountOperators".', + ); + }); + + it('should handle POST /users validation error for invalid email', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'POST', + path: '/users', + body: JSON.stringify({ + email: 'invalid-email', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Invalid request/); + }); + + it('should handle POST /users authorization error for DelegatedAdminGroup creating DelegatedAdmin', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'POST', + path: '/users', + body: JSON.stringify({ + email: 'delegated@example.com', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(403); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/DelegatedAdminGroup can only create AccountOperator users/); + }); + + it('should handle POST /users validation error when invitedBy is included in request body', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'POST', + path: '/users', + body: JSON.stringify({ + email: 'operator@example.com', + role: 'AccountOperator', + accountIds: ['123456789012'], + invitedBy: 'different@example.com', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Invalid request/); + }); + + it('should successfully create AccountOperator user and verify complete flow', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'POST', + path: '/users', + body: JSON.stringify({ + email: 'newoperator@example.com', + role: 'AccountOperator', + accountIds: ['111111111111', '222222222222'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ User: { Username: 'newoperator@example.com' } }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + expectCorsHeaders(result); + const body = result.body; + expect(body).toBe(JSON.stringify({ message: 'User invited successfully', email: 'newoperator@example.com' })); + + // Verify Cognito calls + expect(cognitoMock).toHaveReceivedCommandWith(AdminCreateUserCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'newoperator@example.com', + UserAttributes: [ + { Name: 'email', Value: 'newoperator@example.com' }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + expect(cognitoMock).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'newoperator@example.com', + GroupName: 'AccountOperatorGroup', + }); + + // Verify DynamoDB record creation + const dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'newoperator@example.com' }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.userId).toBe('newoperator@example.com'); + expect(getResponse.Item?.accountIds).toEqual(['111111111111', '222222222222']); + expect(getResponse.Item?.invitedBy).toBe('admin@example.com'); + }); + + it('should successfully create DelegatedAdmin user with complete verification', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'POST', + path: '/users', + body: JSON.stringify({ + email: 'newdelegated@example.com', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ User: { Username: 'newdelegated@example.com' } }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + expectCorsHeaders(result); + const body = result.body; + expect(body).toBe(JSON.stringify({ message: 'User invited successfully', email: 'newdelegated@example.com' })); + + // Verify Cognito calls + expect(cognitoMock).toHaveReceivedCommandWith(AdminCreateUserCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'newdelegated@example.com', + UserAttributes: [ + { Name: 'email', Value: 'newdelegated@example.com' }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + expect(cognitoMock).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'newdelegated@example.com', + GroupName: 'DelegatedAdminGroup', + }); + + // Verify no DynamoDB record created for DelegatedAdmin + const dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'newdelegated@example.com' }, + }), + ); + expect(getResponse.Item).toBeUndefined(); + }); + + it('should route PUT /users/{id} request successfully for AdminGroup updating AccountOperator', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'PUT', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + body: JSON.stringify({ + type: 'account-operator', + email: userId, + status: 'Confirmed', + accountIds: ['123456789012', '987654321098'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // Mock existing user in Cognito + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: userId, + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AccountOperatorGroup' }], + }); + + // Create existing user account mapping + const dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + const existingMapping: UserAccountMapping = { + userId, + accountIds: ['111111111111'], + invitedBy: 'admin@example.com', + invitationTimestamp: '2023-01-01T00:00:00Z', + }; + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: existingMapping, + }), + ); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + expectCorsHeaders(result); + const body = result.body; + expect(body).toBe(JSON.stringify({ message: 'User updated successfully' })); + + // Verify DynamoDB was updated + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId }, + }), + ); + expect(getResponse.Item?.accountIds).toEqual(['123456789012', '987654321098']); + }); + + it('should handle PUT /users/{id} authorization error for insufficient permissions', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'PUT', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + body: JSON.stringify({ + type: 'account-operator', + email: userId, + status: 'Confirmed', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AccountOperatorGroup'], + username: 'operator@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(403); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe(FORBIDDEN_ERROR_MESSAGE); + }); + + it('should handle PUT /users/{id} validation error for invalid user type', async () => { + // ARRANGE + const userId = 'user@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'PUT', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + body: JSON.stringify({ + type: 'admin', + email: userId, + status: 'Confirmed', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe('Only account-operator users can be updated'); + }); + + it('should handle PUT /users/{id} validation error for empty accountIds array', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'PUT', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + body: JSON.stringify({ + type: 'account-operator', + email: userId, + status: 'Confirmed', + accountIds: [], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Invalid request/); + }); + + it('should handle PUT /users/{id} validation error when invitedBy is included in request body', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', ...STANDARD_HEADERS }, + httpMethod: 'PUT', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + body: JSON.stringify({ + type: 'account-operator', + email: userId, + invitedBy: 'different@example.com', + status: 'Confirmed', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Invalid request/); + }); + + it('should route DELETE /users/{id} request successfully for AdminGroup deleting AccountOperator', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'DELETE', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: userId, + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AccountOperatorGroup' }], + }); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + expectCorsHeaders(result); + const body = result.body; + expect(body).toBe(JSON.stringify({ message: 'User deleted successfully' })); + }); + + it('should handle DELETE /users/{id} authorization error for insufficient permissions', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + httpMethod: 'DELETE', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AccountOperatorGroup'], + username: 'operator@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: userId, + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AccountOperatorGroup' }], + }); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(403); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toBe(FORBIDDEN_ERROR_MESSAGE); + }); + + it('should handle DELETE /users/{id} validation error for invalid email', async () => { + // ARRANGE + const userId = 'invalid-email'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + httpMethod: 'DELETE', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(400); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/Valid email address is required for user ID/); + }); + + it('should handle DELETE /users/{id} not found error', async () => { + // ARRANGE + const userId = 'notfound@example.com'; + const encodedUserId = encodeURIComponent(userId); + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + httpMethod: 'DELETE', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({}); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(404); + expectCorsHeaders(result); + const body = JSON.parse(result.body); + expect(body.message).toMatch(/not found/); + }); + + it('should route DELETE /users/{id} request successfully for DelegatedAdminGroup deleting AccountOperator', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const encodedUserId = 'operator%40example.com'; // testing without encodeURIComponent + const event = createMockEvent({ + headers: { ...STANDARD_HEADERS, authorization: 'Bearer valid-token' }, + httpMethod: 'DELETE', + path: `/users/${encodedUserId}`, + pathParameters: { id: encodedUserId }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: userId, + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AccountOperatorGroup' }], + }); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + + // ACT + const result = await handler(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + expectCorsHeaders(result); + const body = result.body; + expect(body).toBe(JSON.stringify({ message: 'User deleted successfully' })); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/handlers/baseHandler.test.ts b/source/lambdas/api/__tests__/handlers/baseHandler.test.ts new file mode 100644 index 00000000..0e15f089 --- /dev/null +++ b/source/lambdas/api/__tests__/handlers/baseHandler.test.ts @@ -0,0 +1,126 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { z } from 'zod'; +import { BaseHandler } from '../../handlers/baseHandler'; +import { BadRequestError } from '../../../common/utils/httpErrors'; + +describe('BaseHandler', () => { + let baseHandler: BaseHandler; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = new Logger({ serviceName: 'test' }); + jest.spyOn(mockLogger, 'error').mockImplementation(); + jest.spyOn(mockLogger, 'info').mockImplementation(); + jest.spyOn(mockLogger, 'debug').mockImplementation(); + jest.spyOn(mockLogger, 'warn').mockImplementation(); + + baseHandler = new BaseHandler(mockLogger); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('extractValidatedBody', () => { + const TestSchema = z.object({ + name: z.string(), + age: z.number(), + email: z.string().email(), + }); + + it('should successfully extract and validate a valid body', () => { + const event = { + body: { + name: 'John Doe', + age: 30, + email: 'john@example.com', + }, + httpMethod: 'POST', + path: '/test', + headers: {}, + requestContext: {} as any, + } as any; + + const result = baseHandler.extractValidatedBody(event, TestSchema); + + expect(result).toEqual({ + name: 'John Doe', + age: 30, + email: 'john@example.com', + }); + }); + + it('should throw BadRequestError for invalid data', () => { + const event = { + body: { + name: 'John Doe', + age: 'thirty', // Invalid: should be number + email: 'invalid-email', // Invalid: not a valid email + }, + httpMethod: 'POST', + path: '/test', + headers: {}, + requestContext: {} as any, + } as any; + + expect(() => { + baseHandler.extractValidatedBody(event, TestSchema); + }).toThrow(BadRequestError); + }); + + it('should throw BadRequestError with custom error prefix', () => { + const event = { + body: { + name: 'John Doe', + age: 'thirty', + email: 'invalid-email', + }, + httpMethod: 'POST', + path: '/test', + headers: {}, + requestContext: {} as any, + } as any; + + expect(() => { + baseHandler.extractValidatedBody(event, TestSchema, 'Custom validation error'); + }).toThrow('Custom validation error'); + }); + + it('should handle empty body', () => { + const event = { + body: null, + httpMethod: 'POST', + path: '/test', + headers: {}, + requestContext: {} as any, + } as any; + + expect(() => { + baseHandler.extractValidatedBody(event, TestSchema); + }).toThrow(BadRequestError); + }); + + it('should return correct TypeScript type', () => { + const event = { + body: { + name: 'John Doe', + age: 30, + email: 'john@example.com', + }, + httpMethod: 'POST', + path: '/test', + headers: {}, + requestContext: {} as any, + } as any; + + const result = baseHandler.extractValidatedBody(event, TestSchema); + + expect(typeof result.name).toBe('string'); + expect(typeof result.age).toBe('number'); + expect(typeof result.email).toBe('string'); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/handlers/deployWebui.test.ts b/source/lambdas/api/__tests__/handlers/deployWebui.test.ts new file mode 100644 index 00000000..83fa4f5a --- /dev/null +++ b/source/lambdas/api/__tests__/handlers/deployWebui.test.ts @@ -0,0 +1,384 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { mockClient } from 'aws-sdk-client-mock'; +import { CopyObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { CloudFormationCustomResourceEvent, Context } from 'aws-lambda'; +import nock from 'nock'; +import { lambdaHandler, WebUIDeployer } from '../../handlers/deployWebui'; + +const s3Mock = mockClient(S3Client); + +describe('WebUI Deploy', () => { + const webuiSrcPath = 'solution-name/v1.2.3/webui/'; + const config = { + SrcBucket: 'solutionBucket', + SrcPath: webuiSrcPath, + WebUIBucket: 'myConsoleBucket', + awsExports: { + AwsUserPoolsId: 'myUserPoolId', + AwsUserPoolsWebClientId: 'myWebClient', + AwsCognitoIdentityPoolId: 'myCognitoIdp', + AwsAppsyncGraphqlEndpoint: 'myAppSyncEndpoint', + AwsContentDeliveryBucket: 'myCDNBucket', + AwsContentDeliveryUrl: 'muCDNUrl', + AwsCognitoDomainPrefix: '', + }, + ServiceToken: 'myServiceToken', + }; + + beforeEach(() => { + s3Mock.reset(); + jest.clearAllMocks(); + nock.cleanAll(); + process.env.CONFIG = JSON.stringify(config); + }); + + afterEach(() => { + delete process.env.CONFIG; + nock.cleanAll(); + }); + + describe('WebUI files are copied and config is generated', () => { + it('should copy files and create config', async () => { + // ARRANGE + const webUIDeployer = new WebUIDeployer(); + + const filenamesFromManifest = ['index.html', 'static/css/main.b4f55c7e.css', 'static/js/main.c838e191.js']; + + const manifestContent = JSON.stringify({ files: filenamesFromManifest }); + + // Mock the manifest file read + s3Mock + .on(GetObjectCommand, { + Bucket: config.SrcBucket, + Key: webuiSrcPath + 'webui-manifest.json', + }) + .resolves({ + Body: { + transformToString: async () => manifestContent, + } as any, + }); + + // Mock file copies + for (const filename of filenamesFromManifest) { + s3Mock + .on(CopyObjectCommand, { + CopySource: `${config.SrcBucket}/${webuiSrcPath}${filename}`, + Bucket: config.WebUIBucket, + Key: filename, + }) + .resolves({}); + } + + // Mock config file write + s3Mock + .on(PutObjectCommand, { + Bucket: config.WebUIBucket, + Key: 'aws-exports.json', + }) + .resolves({}); + + // ACT + await webUIDeployer.deploy(); + + // ASSERT + // Verify manifest was read + expect(s3Mock.commandCalls(GetObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(GetObjectCommand)[0].args[0].input).toEqual({ + Bucket: config.SrcBucket, + Key: webuiSrcPath + 'webui-manifest.json', + }); + + // Verify files were copied + expect(s3Mock.commandCalls(CopyObjectCommand)).toHaveLength(filenamesFromManifest.length); + + filenamesFromManifest.forEach((filename, index) => { + expect(s3Mock.commandCalls(CopyObjectCommand)[index].args[0].input).toEqual({ + CopySource: `${config.SrcBucket}/${webuiSrcPath}${filename}`, + Bucket: config.WebUIBucket, + Key: filename, + }); + }); + + // Verify config file was written + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(PutObjectCommand)[0].args[0].input).toEqual({ + Bucket: config.WebUIBucket, + Key: 'aws-exports.json', + Body: JSON.stringify(config.awsExports), + ContentType: 'application/json', + Metadata: { + 'Content-Type': 'application/json', + }, + }); + }); + + it('should handle lambda Create event and send success response', async () => { + // ARRANGE + const responseUrl = 'https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/test-response-url'; + const event: CloudFormationCustomResourceEvent = { + RequestType: 'Create', + ResponseURL: responseUrl, + StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/12345678-1234-1234-1234-123456789012', + RequestId: 'test-request-id', + LogicalResourceId: 'WebUIDeployment', + ResourceType: 'Custom::WebUIDeployment', + ServiceToken: 'myServiceToken', + ResourceProperties: { + ServiceToken: 'myServiceToken', + }, + }; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '1', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-aws-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test-stream', + getRemainingTimeInMillis: () => 30000, + done: jest.fn(), + fail: jest.fn(), + succeed: jest.fn(), + }; + + const filenamesFromManifest = ['index.html', 'static/css/main.b4f55c7e.css', 'static/js/main.c838e191.js']; + const manifestContent = JSON.stringify({ files: filenamesFromManifest }); + + // Mock CloudFormation response + const cfnResponseScope = nock('https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com') + .put('/test-response-url') + .reply(200); + + // Mock the manifest file read + s3Mock + .on(GetObjectCommand, { + Bucket: config.SrcBucket, + Key: webuiSrcPath + 'webui-manifest.json', + }) + .resolves({ + Body: { + transformToString: async () => manifestContent, + } as any, + }); + + // Mock file copies + for (const filename of filenamesFromManifest) { + s3Mock + .on(CopyObjectCommand, { + CopySource: `${config.SrcBucket}/${webuiSrcPath}${filename}`, + Bucket: config.WebUIBucket, + Key: filename, + }) + .resolves({}); + } + + // Mock config file write + s3Mock + .on(PutObjectCommand, { + Bucket: config.WebUIBucket, + Key: 'aws-exports.json', + }) + .resolves({}); + + // ACT + await lambdaHandler(event, context); + + // ASSERT + // Verify CloudFormation response was sent + expect(cfnResponseScope.isDone()).toBe(true); + + // Verify S3 operations were performed + expect(s3Mock.commandCalls(GetObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(CopyObjectCommand)).toHaveLength(filenamesFromManifest.length); + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + }); + + it('should handle lambda Update event and send success response', async () => { + // ARRANGE + const responseUrl = 'https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/test-response-url'; + const event: CloudFormationCustomResourceEvent = { + RequestType: 'Update', + ResponseURL: responseUrl, + StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/12345678-1234-1234-1234-123456789012', + RequestId: 'test-request-id', + LogicalResourceId: 'WebUIDeployment', + ResourceType: 'Custom::WebUIDeployment', + ResourceProperties: { + ServiceToken: 'myServiceToken', + }, + OldResourceProperties: { + ServiceToken: 'myServiceToken', + }, + PhysicalResourceId: 'test-physical-id', + ServiceToken: 'myServiceToken', + }; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '1', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-aws-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test-stream', + getRemainingTimeInMillis: () => 30000, + done: jest.fn(), + fail: jest.fn(), + succeed: jest.fn(), + }; + + const filenamesFromManifest = ['index.html', 'static/css/main.b4f55c7e.css', 'static/js/main.c838e191.js']; + const manifestContent = JSON.stringify({ files: filenamesFromManifest }); + + // Mock CloudFormation response + const cfnResponseScope = nock('https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com') + .put('/test-response-url') + .reply(200); + + // Mock the manifest file read + s3Mock + .on(GetObjectCommand, { + Bucket: config.SrcBucket, + Key: webuiSrcPath + 'webui-manifest.json', + }) + .resolves({ + Body: { + transformToString: async () => manifestContent, + } as any, + }); + + // Mock file copies + for (const filename of filenamesFromManifest) { + s3Mock + .on(CopyObjectCommand, { + CopySource: `${config.SrcBucket}/${webuiSrcPath}${filename}`, + Bucket: config.WebUIBucket, + Key: filename, + }) + .resolves({}); + } + + // Mock config file write + s3Mock + .on(PutObjectCommand, { + Bucket: config.WebUIBucket, + Key: 'aws-exports.json', + }) + .resolves({}); + + // ACT + await lambdaHandler(event, context); + + // ASSERT + // Verify CloudFormation response was sent + expect(cfnResponseScope.isDone()).toBe(true); + + // Verify S3 operations were performed + expect(s3Mock.commandCalls(GetObjectCommand)).toHaveLength(1); + expect(s3Mock.commandCalls(CopyObjectCommand)).toHaveLength(filenamesFromManifest.length); + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(1); + }); + + it('should handle lambda Delete event and send success response without deployment', async () => { + // ARRANGE + const responseUrl = 'https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/test-response-url'; + const event: CloudFormationCustomResourceEvent = { + RequestType: 'Delete', + ResponseURL: responseUrl, + StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/12345678-1234-1234-1234-123456789012', + RequestId: 'test-request-id', + LogicalResourceId: 'WebUIDeployment', + ResourceType: 'Custom::WebUIDeployment', + ResourceProperties: { + ServiceToken: 'myServiceToken', + }, + PhysicalResourceId: 'test-physical-id', + ServiceToken: 'myServiceToken', + }; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '1', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-aws-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test-stream', + getRemainingTimeInMillis: () => 30000, + done: jest.fn(), + fail: jest.fn(), + succeed: jest.fn(), + }; + + // Mock CloudFormation response + const cfnResponseScope = nock('https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com') + .put('/test-response-url') + .reply(200); + + // ACT + await lambdaHandler(event, context); + + // ASSERT + // Verify CloudFormation response was sent + expect(cfnResponseScope.isDone()).toBe(true); + + // Verify no S3 operations were performed for Delete + expect(s3Mock.commandCalls(GetObjectCommand)).toHaveLength(0); + expect(s3Mock.commandCalls(CopyObjectCommand)).toHaveLength(0); + expect(s3Mock.commandCalls(PutObjectCommand)).toHaveLength(0); + }); + + it('should handle errors and send failure response', async () => { + // ARRANGE + const responseUrl = 'https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/test-response-url'; + const event: CloudFormationCustomResourceEvent = { + RequestType: 'Create', + ResponseURL: responseUrl, + StackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/12345678-1234-1234-1234-123456789012', + RequestId: 'test-request-id', + LogicalResourceId: 'WebUIDeployment', + ResourceType: 'Custom::WebUIDeployment', + ResourceProperties: { + ServiceToken: 'myServiceToken', + }, + ServiceToken: 'myServiceToken', + }; + + const context: Context = { + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '1', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-aws-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test-stream', + getRemainingTimeInMillis: () => 30000, + done: jest.fn(), + fail: jest.fn(), + succeed: jest.fn(), + }; + + // Mock CloudFormation response + const cfnResponseScope = nock('https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com') + .put('/test-response-url') + .reply(200); + + // Mock S3 error + s3Mock.on(GetObjectCommand).rejects(new Error('S3 operation failed')); + + // ACT + await lambdaHandler(event, context); + + // ASSERT + // Verify CloudFormation response was sent + expect(cfnResponseScope.isDone()).toBe(true); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/handlers/findings.test.ts b/source/lambdas/api/__tests__/handlers/findings.test.ts new file mode 100644 index 00000000..bf08b83f --- /dev/null +++ b/source/lambdas/api/__tests__/handlers/findings.test.ts @@ -0,0 +1,1861 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AdminGetUserCommand, + AdminListGroupsForUserCommand, + CognitoIdentityProviderClient, +} from '@aws-sdk/client-cognito-identity-provider'; +import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn'; +import { BatchWriteCommand, DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; +import { findingsTableName } from '../../../common/__tests__/envSetup'; +import { API_HEADERS } from '../../handlers/apiHandler'; +import { executeFindingAction, searchFindings } from '../../handlers/findings'; +import { createMockContext, createMockEvent, createMockFinding, TEST_REQUEST_CONTEXT } from '../utils'; + +const cognitoMock = mockClient(CognitoIdentityProviderClient); +const sfnMock = mockClient(SFNClient); + +const expectedFindingsHeaders = API_HEADERS.FINDINGS; + +describe('FindingsHandler Integration Tests', () => { + let dynamoDBDocumentClient: DynamoDBDocumentClient; + const remediationHistoryTableName = 'test-remediation-history-table'; + const userAccountMappingTableName = 'test-user-account-mapping-table'; + + beforeAll(async () => { + await DynamoDBTestSetup.initialize(); + dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + await DynamoDBTestSetup.createFindingsTable(findingsTableName); + await DynamoDBTestSetup.createRemediationHistoryTable(remediationHistoryTableName); + await DynamoDBTestSetup.createUserAccountMappingTable(userAccountMappingTableName); + }); + + afterAll(async () => { + await DynamoDBTestSetup.deleteTable(findingsTableName); + await DynamoDBTestSetup.deleteTable(remediationHistoryTableName); + await DynamoDBTestSetup.deleteTable(userAccountMappingTableName); + }); + + beforeEach(async () => { + await DynamoDBTestSetup.clearTable(findingsTableName, 'findings'); + await DynamoDBTestSetup.clearTable(remediationHistoryTableName, 'remediationHistory'); + await DynamoDBTestSetup.clearTable(userAccountMappingTableName, 'userAccountMapping'); + process.env.FINDINGS_TABLE_NAME = findingsTableName; + process.env.REMEDIATION_HISTORY_TABLE_NAME = remediationHistoryTableName; + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME = userAccountMappingTableName; + process.env.USER_POOL_ID = 'test-user-pool-id'; + process.env.ORCHESTRATOR_ARN = 'arn:aws:states:us-east-1:123456789012:stateMachine:test-orchestrator'; + + cognitoMock.reset(); + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: 'admin-user@example.com', + UserAttributes: [ + { Name: 'email', Value: 'admin-user@example.com' }, + { Name: 'custom:invitedBy', Value: 'system@example.com' }, + ], + UserCreateDate: new Date(), + UserStatus: 'CONFIRMED', + }); + + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AdminGroup' }], + }); + + sfnMock.reset(); + sfnMock.on(StartExecutionCommand).resolves({ + executionArn: 'arn:aws:states:us-east-1:123456789012:execution:test-orchestrator:test-execution-id', + }); + }); + + afterEach(() => { + delete process.env.FINDINGS_TABLE_NAME; + delete process.env.REMEDIATION_HISTORY_TABLE_NAME; + delete process.env.ORCHESTRATOR_ARN; + cognitoMock.reset(); + sfnMock.reset(); + }); + + describe('searchFindings', () => { + beforeEach(async () => { + const testFindings = [ + createMockFinding({ + findingId: 'finding-1', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::bucket-1', + severity: 'HIGH', + findingDescription: 'Critical S3 bucket issue', + 'securityHubUpdatedAtTime#findingId': '2023-01-01T00:00:00Z#finding-1', + }), + createMockFinding({ + findingId: 'finding-2', + accountId: '123456789012', + resourceId: 'arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0', + resourceType: 'AWS::EC2::Instance', + severity: 'MEDIUM', + findingDescription: 'EC2 security group issue', + 'securityHubUpdatedAtTime#findingId': '2023-01-02T00:00:00Z#finding-2', + }), + createMockFinding({ + findingId: 'finding-3', + accountId: '987654321098', + resourceId: 'arn:aws:rds:us-west-2:987654321098:db:mydb', + resourceType: 'AWS::RDS::DBInstance', + severity: 'LOW', + findingDescription: 'RDS configuration issue', + 'securityHubUpdatedAtTime#findingId': '2023-01-03T00:00:00Z#finding-3', + }), + ]; + + for (const finding of testFindings) { + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ); + } + }); + + it('should return 200 with all findings when no filters are provided', async () => { + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify({}), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(3); + expect(body.Findings[0]).toHaveProperty('findingId'); + expect(body.Findings[0]).toHaveProperty('accountId'); + expect(body.Findings[0]).toHaveProperty('severity'); + + expect(result.headers).toEqual(expectedFindingsHeaders); + }); + + it('should filter findings by accountId', async () => { + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'accountId', + Filter: { + Value: '123456789012', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(2); + expect(body.Findings.every((f: any) => f.accountId === '123456789012')).toBe(true); + }); + + it('should filter findings by severity', async () => { + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'severity', + Filter: { + Value: 'HIGH', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(1); + expect(body.Findings[0].severity).toBe('HIGH'); + }); + + it('should handle complex filter requests with multiple criteria', async () => { + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'accountId', + Filter: { + Value: '123456789012', + Comparison: 'EQUALS', + }, + }, + { + FieldName: 'severity', + Filter: { + Value: 'HIGH', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + SortCriteria: [ + { + Field: 'securityHubUpdatedAtTime', + SortOrder: 'desc', + }, + ], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(1); + expect(body.Findings[0].accountId).toBe('123456789012'); + expect(body.Findings[0].severity).toBe('HIGH'); + }); + + it('should throw BadRequestError when request validation fails', async () => { + const invalidRequestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'INVALID_OPERATOR', + StringFilters: [ + { + FieldName: 'accountId', + Filter: { + Value: '123456789012', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(invalidRequestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(searchFindings(event, context)).rejects.toThrow( + "Invalid request: Filters.CompositeFilters.0.Operator: Invalid enum value. Expected 'AND' | 'OR', received 'INVALID_OPERATOR'", + ); + }); + + it('should handle sort criteria correctly', async () => { + const requestBody = { + SortCriteria: [ + { + Field: 'securityHubUpdatedAtTime', + SortOrder: 'desc', + }, + ], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(3); + // Should be sorted by lastUpdatedTime descending + expect(body.Findings[0].findingId).toBe('finding-3'); // 2023-01-03 + expect(body.Findings[1].findingId).toBe('finding-2'); // 2023-01-02 + expect(body.Findings[2].findingId).toBe('finding-1'); // 2023-01-01 + }); + + it('should filter findings by resourceType AWS::S3::Bucket using normalized search', async () => { + // Clear existing findings and create specific test data + await DynamoDBTestSetup.clearTable(findingsTableName, 'findings'); + + const testFindings = [ + createMockFinding({ + findingId: 'finding-s3-1', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::bucket-1', + resourceType: 'AWS::S3::Bucket', + resourceTypeNormalized: 'awss3bucket', + severity: 'HIGH', + findingDescription: 'S3 bucket issue 1', + 'securityHubUpdatedAtTime#findingId': '2023-01-01T00:00:00Z#finding-s3-1', + }), + createMockFinding({ + findingId: 'finding-s3-2', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::bucket-2', + resourceType: 'AwsS3Bucket', + resourceTypeNormalized: 'awss3bucket', + severity: 'MEDIUM', + findingDescription: 'S3 bucket issue 2', + 'securityHubUpdatedAtTime#findingId': '2023-01-02T00:00:00Z#finding-s3-2', + }), + createMockFinding({ + findingId: 'finding-ec2-1', + accountId: '123456789012', + resourceId: 'arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0', + resourceType: 'AWS::EC2::Instance', + resourceTypeNormalized: 'awsec2instance', + severity: 'LOW', + findingDescription: 'EC2 instance issue', + 'securityHubUpdatedAtTime#findingId': '2023-01-03T00:00:00Z#finding-ec2-1', + }), + ]; + + for (const finding of testFindings) { + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ); + } + + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'resourceType', + Filter: { + Value: 'AWS::S3::Bucket', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(2); + expect(body.Findings.every((f: any) => f.resourceTypeNormalized === 'awss3bucket')).toBe(true); + }); + + it('should filter findings by resourceType AwsS3Bucket using normalized search', async () => { + // Clear existing findings and create specific test data + await DynamoDBTestSetup.clearTable(findingsTableName, 'findings'); + + const testFindings = [ + createMockFinding({ + findingId: 'finding-s3-3', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::bucket-3', + resourceType: 'AWS::S3::Bucket', + resourceTypeNormalized: 'awss3bucket', + severity: 'HIGH', + findingDescription: 'S3 bucket issue 3', + 'securityHubUpdatedAtTime#findingId': '2023-01-01T00:00:00Z#finding-s3-3', + }), + createMockFinding({ + findingId: 'finding-s3-4', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::bucket-4', + resourceType: 'AwsS3Bucket', + resourceTypeNormalized: 'awss3bucket', + severity: 'MEDIUM', + findingDescription: 'S3 bucket issue 4', + 'securityHubUpdatedAtTime#findingId': '2023-01-02T00:00:00Z#finding-s3-4', + }), + createMockFinding({ + findingId: 'finding-rds-1', + accountId: '123456789012', + resourceId: 'arn:aws:rds:us-west-2:123456789012:db:mydb', + resourceType: 'AWS::RDS::DBInstance', + resourceTypeNormalized: 'awsrdsdbinstance', + severity: 'LOW', + findingDescription: 'RDS configuration issue', + 'securityHubUpdatedAtTime#findingId': '2023-01-03T00:00:00Z#finding-rds-1', + }), + ]; + + for (const finding of testFindings) { + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ); + } + + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'resourceType', + Filter: { + Value: 'AwsS3Bucket', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(2); + expect(body.Findings.every((f: any) => f.resourceTypeNormalized === 'awss3bucket')).toBe(true); + }); + + it('should handle CONTAINS comparison', async () => { + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'findingDescription', + Filter: { + Value: 'S3', + Comparison: 'CONTAINS', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(1); + expect(body.Findings[0].findingDescription).toContain('S3'); + }); + + it('should handle empty request body', async () => { + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: null, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Findings).toHaveLength(3); + }); + }); + + describe('Pagination', () => { + beforeEach(async () => { + // Clear existing data first + await DynamoDBTestSetup.clearTable(findingsTableName, 'findings'); + + // Create 60 findings to test pagination (default page size is 50) + const testFindings = []; + for (let i = 1; i <= 60; i++) { + let severity: string; + if (i % 3 === 0) { + severity = 'HIGH'; + } else if (i % 2 === 0) { + severity = 'MEDIUM'; + } else { + severity = 'LOW'; + } + + testFindings.push( + createMockFinding({ + findingId: `finding-${i.toString().padStart(3, '0')}`, + accountId: '123456789012', + resourceId: `arn:aws:s3:::bucket-${i}`, + severity, + findingDescription: `Test finding ${i}`, + 'securityHubUpdatedAtTime#findingId': `2023-01-${i.toString().padStart(2, '0')}T00:00:00Z#finding-${i.toString().padStart(3, '0')}`, + }), + ); + } + + for (let i = 0; i < testFindings.length; i += 25) { + const batch = testFindings.slice(i, i + 25); + await dynamoDBDocumentClient.send( + new BatchWriteCommand({ + RequestItems: { + [findingsTableName]: batch.map((finding) => ({ + PutRequest: { + Item: finding, + }, + })), + }, + }), + ); + } + }); + + it('should return first page of results with NextToken when there are more than 50 findings', async () => { + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify({}), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + + // Should return exactly 50 findings (default page size) + expect(body.Findings).toHaveLength(50); + + // Should have NextToken since there are more results + expect(body.NextToken).toBeDefined(); + expect(typeof body.NextToken).toBe('string'); + + // Verify findings are properly formatted + expect(body.Findings[0]).toHaveProperty('findingId'); + expect(body.Findings[0]).toHaveProperty('accountId'); + expect(body.Findings[0]).toHaveProperty('severity'); + }); + + it('should return second page of results when NextToken is provided', async () => { + // First request to get NextToken + const firstEvent = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify({}), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const firstResult = await searchFindings(firstEvent, context); + const firstBody = JSON.parse(firstResult.body); + + expect(firstBody.NextToken).toBeDefined(); + + // Second request with NextToken + const secondEvent = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify({ + NextToken: firstBody.NextToken, + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + + const secondResult = await searchFindings(secondEvent, context); + const secondBody = JSON.parse(secondResult.body); + + expect(secondResult.statusCode).toBe(200); + + // Should return remaining 10 findings + expect(secondBody.Findings).toHaveLength(10); + + // Should not have NextToken since this is the last page + expect(secondBody.NextToken).toBeUndefined(); + + // Verify no duplicate findings between pages + const firstPageIds = firstBody.Findings.map((f: any) => f.findingId); + const secondPageIds = secondBody.Findings.map((f: any) => f.findingId); + const intersection = firstPageIds.filter((id: string) => secondPageIds.includes(id)); + expect(intersection).toHaveLength(0); + }); + + it('should handle pagination with filters', async () => { + // Create additional findings with different account IDs + const additionalFindings = []; + for (let i = 61; i <= 80; i++) { + additionalFindings.push( + createMockFinding({ + findingId: `finding-${i.toString().padStart(3, '0')}`, + accountId: '987654321098', // Different account ID + resourceId: `arn:aws:s3:::bucket-${i}`, + severity: 'HIGH', + findingDescription: `Test finding ${i}`, + 'securityHubUpdatedAtTime#findingId': `2023-01-${(i - 60).toString().padStart(2, '0')}T00:00:00Z#finding-${i.toString().padStart(3, '0')}`, + }), + ); + } + + await Promise.all( + additionalFindings.map((finding) => + dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ), + ), + ); + + // Filter by original account ID + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'accountId', + Filter: { + Value: '123456789012', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchFindings(event, context); + const body = JSON.parse(result.body); + + expect(result.statusCode).toBe(200); + + // Should return 50 findings (first page) all with the filtered account ID + expect(body.Findings).toHaveLength(50); + expect(body.Findings.every((f: any) => f.accountId === '123456789012')).toBe(true); + + // Should have NextToken since there are 60 total findings with this account ID + expect(body.NextToken).toBeDefined(); + }); + }); + + describe('Input Validation', () => { + it('should reject invalid comparison operators', async () => { + const invalidRequestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'accountId', + Filter: { + Value: '123456789012', + Comparison: 'INVALID_COMPARISON', // Invalid comparison + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(invalidRequestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(searchFindings(event, context)).rejects.toThrow( + "Invalid request: Filters.CompositeFilters.0.StringFilters.0.Filter.Comparison: Invalid enum value. Expected 'EQUALS' | 'NOT_EQUALS' | 'CONTAINS' | 'NOT_CONTAINS' | 'GREATER_THAN_OR_EQUAL' | 'LESS_THAN_OR_EQUAL', received 'INVALID_COMPARISON'", + ); + }); + + it('should reject invalid sort order', async () => { + const invalidRequestBody = { + SortCriteria: [ + { + Field: 'securityHubUpdatedAtTime', + SortOrder: 'invalid', // Invalid sort order + }, + ], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(invalidRequestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(searchFindings(event, context)).rejects.toThrow( + "Invalid request: SortCriteria.0.SortOrder: Invalid enum value. Expected 'asc' | 'desc', received 'invalid'", + ); + }); + + it('should accept comparison operators GREATER_THAN_OR_EQUAL and LESS_THAN_OR_EQUAL', async () => { + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'securityHubUpdatedAtTime', + Filter: { + Value: '2023-01-01T00:00:00Z', + Comparison: 'GREATER_THAN_OR_EQUAL', + }, + }, + { + FieldName: 'securityHubUpdatedAtTime', + Filter: { + Value: '2023-12-31T23:59:59Z', + Comparison: 'LESS_THAN_OR_EQUAL', + }, + }, + ], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // Should not throw an error - the new operators should be accepted + const response = await searchFindings(event, context); + expect(response.statusCode).toBe(200); + }); + }); + + describe('executeFindingAction', () => { + beforeEach(async () => { + // Create test findings for action testing + const testFindings = [ + createMockFinding({ + findingId: 'finding-1', + findingType: 'cis-aws-foundations-benchmark/v/1.4.0/4.8', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::test-bucket-1', + severity: 'HIGH', + findingDescription: 'Test finding 1', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': '2023-01-01T00:00:00Z#finding-1', + }), + createMockFinding({ + findingId: 'finding-2', + findingType: 'cis-aws-foundations-benchmark/v/1.4.0/4.9', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::test-bucket-2', + severity: 'MEDIUM', + findingDescription: 'Test finding 2', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': '2023-01-02T00:00:00Z#finding-2', + }), + createMockFinding({ + findingId: 'finding-3', + findingType: 'cis-aws-foundations-benchmark/v/1.4.0/4.10', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::test-bucket-3', + severity: 'LOW', + findingDescription: 'Test finding 3', + suppressed: true, + 'securityHubUpdatedAtTime#findingId': '2023-01-03T00:00:00Z#finding-3', + }), + ]; + + for (const finding of testFindings) { + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ); + } + }); + + describe('Suppress Action', () => { + it('should return 200 and suppress single finding', async () => { + const suppressSingleFindingId = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/suppress-single-test'; + const testFinding = createMockFinding({ + findingId: suppressSingleFindingId, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:suppress-single-test', + severity: 'HIGH', + findingDescription: 'Test finding for single suppress test', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${suppressSingleFindingId}`, + }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: testFinding, + }), + ); + + const requestBody = { + actionType: 'Suppress', + findingIds: [suppressSingleFindingId], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(200); + expect(result.headers).toEqual(expectedFindingsHeaders); + }); + + it('should return 200 and suppress multiple findings', async () => { + const suppressFinding1Id = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/suppress-multiple-test-1'; + const suppressFinding2Id = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/suppress-multiple-test-2'; + + const additionalFindings = [ + createMockFinding({ + findingId: suppressFinding1Id, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:suppress-multiple-test-1', + severity: 'HIGH', + findingDescription: 'Test finding for suppress multiple test 1', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${suppressFinding1Id}`, + }), + createMockFinding({ + findingId: suppressFinding2Id, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:suppress-multiple-test-2', + severity: 'MEDIUM', + findingDescription: 'Test finding for suppress multiple test 2', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-02T00:00:00Z#${suppressFinding2Id}`, + }), + ]; + + for (const finding of additionalFindings) { + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ); + } + + const requestBody = { + actionType: 'Suppress', + findingIds: [suppressFinding1Id, suppressFinding2Id], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(200); + expect(result.headers).toEqual(expectedFindingsHeaders); + }); + + it('should return 500 error for non-existent finding', async () => { + const requestBody = { + actionType: 'Suppress', + findingIds: ['non-existent-finding'], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('No findings found for the provided IDs'); + }); + }); + + describe('Unsuppress Action', () => { + it('should return 200 and unsuppress single finding', async () => { + const unsuppressSingleFindingId = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/unsuppress-single-test'; + const testFinding = createMockFinding({ + findingId: unsuppressSingleFindingId, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:unsuppress-single-test', + severity: 'LOW', + findingDescription: 'Test finding for single unsuppress test', + suppressed: true, + 'securityHubUpdatedAtTime#findingId': `2023-01-03T00:00:00Z#${unsuppressSingleFindingId}`, + }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: testFinding, + }), + ); + + const requestBody = { + actionType: 'Unsuppress', + findingIds: [unsuppressSingleFindingId], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(200); + expect(result.headers).toEqual(expectedFindingsHeaders); + }); + + it('should return 200 and unsuppress multiple findings', async () => { + const unsuppressFinding1Id = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/unsuppress-test-1'; + const unsuppressFinding2Id = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/unsuppress-test-2'; + + const additionalFindings = [ + createMockFinding({ + findingId: unsuppressFinding1Id, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:unsuppress-test-1', + severity: 'HIGH', + findingDescription: 'Test finding for unsuppress test 1', + suppressed: true, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${unsuppressFinding1Id}`, + }), + createMockFinding({ + findingId: unsuppressFinding2Id, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:unsuppress-test-2', + severity: 'LOW', + findingDescription: 'Test finding for unsuppress test 2', + suppressed: true, + 'securityHubUpdatedAtTime#findingId': `2023-01-03T00:00:00Z#${unsuppressFinding2Id}`, + }), + ]; + + for (const finding of additionalFindings) { + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ); + } + + const requestBody = { + actionType: 'Unsuppress', + findingIds: [unsuppressFinding1Id, unsuppressFinding2Id], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(200); + expect(result.headers).toEqual(expectedFindingsHeaders); + }); + }); + + describe('Input Validation', () => { + it('should throw BadRequestError when actionType is missing', async () => { + const requestBody = { + findingIds: ['finding-1'], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('Invalid request:'); + }); + + it('should throw BadRequestError when findingIds is missing', async () => { + const requestBody = { + actionType: 'Suppress', + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('Invalid request:'); + }); + + it('should throw BadRequestError when findingIds is empty array', async () => { + const requestBody = { + actionType: 'Suppress', + findingIds: [], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('Invalid request:'); + }); + + it('should throw BadRequestError when actionType is invalid', async () => { + const requestBody = { + actionType: 'InvalidAction', + findingIds: ['finding-1'], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('Invalid request:'); + }); + + it('should throw BadRequestError when findingIds contains non-string values', async () => { + const requestBody = { + actionType: 'Suppress', + findingIds: ['finding-1', 123, null], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('Invalid request:'); + }); + + it('should handle empty request body', async () => { + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: null, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('Invalid request:'); + }); + + it('should handle malformed JSON in request body', async () => { + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: '{"actionType": "Suppress", "findingIds": [', + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow(); + }); + }); + + describe('Batch Operations', () => { + it('should handle large batch of finding IDs', async () => { + // Create a large batch of finding IDs + const findingIds = []; + for (let i = 1; i <= 100; i++) { + findingIds.push(`finding-batch-${i}`); + } + + const requestBody = { + actionType: 'Suppress', + findingIds, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(executeFindingAction(event, context)).rejects.toThrow('No findings found for the provided IDs'); + }); + + it('should handle mixed existing and non-existing finding IDs', async () => { + const existingFinding1Id = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/mixed-test-1'; + const existingFinding2Id = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/mixed-test-2'; + + const additionalFindings = [ + createMockFinding({ + findingId: existingFinding1Id, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:mixed-test-1', + severity: 'HIGH', + findingDescription: 'Test finding for mixed test 1', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${existingFinding1Id}`, + }), + createMockFinding({ + findingId: existingFinding2Id, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:mixed-test-2', + severity: 'MEDIUM', + findingDescription: 'Test finding for mixed test 2', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-02T00:00:00Z#${existingFinding2Id}`, + }), + ]; + + for (const finding of additionalFindings) { + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: finding, + }), + ); + } + + const requestBody = { + actionType: 'Suppress', + findingIds: [ + existingFinding1Id, + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/non-existent-1', + existingFinding2Id, + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/non-existent-2', + ], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(200); + }); + }); + + describe('Response Format', () => { + it('should return correct headers', async () => { + const findingId = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/12345678-1234-1234-1234-123456789013'; + const testFinding = createMockFinding({ + findingId, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-headers', + severity: 'HIGH', + findingDescription: 'Test finding for headers test', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${findingId}`, + }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: testFinding, + }), + ); + + const requestBody = { + actionType: 'Suppress', + findingIds: [findingId], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(200); + expect(result.headers).toEqual(expectedFindingsHeaders); + }); + + it('should return empty body for successful action', async () => { + const findingId = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/12345678-1234-1234-1234-123456789012'; + const testFinding = createMockFinding({ + findingId, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + severity: 'HIGH', + findingDescription: 'Test finding for response format', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${findingId}`, + }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: testFinding, + }), + ); + + const requestBody = { + actionType: 'Suppress', + findingIds: [findingId], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body).toBe(''); + }); + }); + + describe('Remediate Action', () => { + it('should return 202 and initiate remediation for single finding', async () => { + const remediateFindingId = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-finding-remediate'; + + const testFinding = createMockFinding({ + findingId: remediateFindingId, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-remediate', + severity: 'HIGH', + findingDescription: 'Test finding for remediation', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${remediateFindingId}`, + }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: testFinding, + }), + ); + + const requestBody = { + actionType: 'Remediate', + findingIds: [remediateFindingId], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(202); + const responseBody = JSON.parse(result.body); + expect(responseBody.status).toBe('IN_PROGRESS'); + }); + + it('should return 202 and initiate remediation with ticket generation', async () => { + const remediateFindingId = + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-finding-remediate-ticket'; + + const testFinding = createMockFinding({ + findingId: remediateFindingId, + findingType: 'security-control/Lambda.3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test-function-remediate-ticket', + severity: 'HIGH', + findingDescription: 'Test finding for remediation with ticket', + suppressed: false, + 'securityHubUpdatedAtTime#findingId': `2023-01-01T00:00:00Z#${remediateFindingId}`, + }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: findingsTableName, + Item: testFinding, + }), + ); + + const requestBody = { + actionType: 'RemediateAndGenerateTicket', + findingIds: [remediateFindingId], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/findings/action', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await executeFindingAction(event, context); + + expect(result.statusCode).toBe(202); + const responseBody = JSON.parse(result.body); + expect(responseBody.status).toBe('IN_PROGRESS'); + }); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/handlers/preSignUp.test.ts b/source/lambdas/api/__tests__/handlers/preSignUp.test.ts new file mode 100644 index 00000000..2428fd2a --- /dev/null +++ b/source/lambdas/api/__tests__/handlers/preSignUp.test.ts @@ -0,0 +1,332 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { PreSignUpTriggerEvent, Context, Callback } from 'aws-lambda'; +import { preSignUpHandler } from '../../handlers/preSignUp'; +import { mockClient } from 'aws-sdk-client-mock'; +import { + CognitoIdentityProviderClient, + AdminGetUserCommand, + AdminListGroupsForUserCommand, + DescribeIdentityProviderCommand, + AdminLinkProviderForUserCommand, +} from '@aws-sdk/client-cognito-identity-provider'; +import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; +import { userPoolId } from '../../../common/__tests__/envSetup'; +import 'aws-sdk-client-mock-jest'; + +const mockCognitoClient = mockClient(CognitoIdentityProviderClient); + +describe('preSignUpHandler', () => { + let mockCallback: jest.MockedFunction; + let mockContext: Context; + + beforeEach(async () => { + mockCognitoClient.reset(); + jest.clearAllMocks(); + + mockCallback = jest.fn(); + mockContext = {} as Context; + }); + + const createEvent = ( + triggerSource: string, + userAttributes: Record = {}, + userName = 'testuser', + ): PreSignUpTriggerEvent => ({ + version: '1', + region: 'us-east-1', + userPoolId: userPoolId, + userName, + callerContext: { + awsSdkVersion: '1.0.0', + clientId: 'test-client-id', + }, + triggerSource: triggerSource as any, + request: { + userAttributes, + validationData: {}, + clientMetadata: {}, + }, + response: { + autoConfirmUser: false, + autoVerifyEmail: false, + autoVerifyPhone: false, + }, + }); + + describe('PreSignUp_ExternalProvider', () => { + it('should successfully handle external provider sign-up with existing user', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'test@example.com' }, 'SAML_testuser'); + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({ + IdentityProvider: { + AttributeMapping: { email: 'email' }, + }, + }); + mockCognitoClient.on(AdminLinkProviderForUserCommand).resolves({}); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminGetUserCommand, { + UserPoolId: userPoolId, + Username: 'test@example.com', + }); + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminLinkProviderForUserCommand, { + UserPoolId: userPoolId, + DestinationUser: { + ProviderName: 'Cognito', + ProviderAttributeValue: 'test@example.com', + }, + SourceUser: { + ProviderName: 'SAML', + ProviderAttributeName: 'email', + ProviderAttributeValue: 'test@example.com', + }, + }); + expect(mockCallback).toHaveBeenCalledWith(null, event); + }); + + it('should reject external provider sign-up when user not found', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'nonexistent@example.com' }, 'SAML_testuser'); + mockCognitoClient.on(AdminGetUserCommand).rejects(new Error('User not found')); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminGetUserCommand, { + UserPoolId: userPoolId, + Username: 'nonexistent@example.com', + }); + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminLinkProviderForUserCommand); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ message: 'User not found in local user pool' }), + event, + ); + }); + + it('should reject external provider sign-up when provider name cannot be extracted', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'test@example.com' }, 'invalidusername'); + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminLinkProviderForUserCommand); + expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ message: 'No provider name found' }), event); + }); + + it('should reject external provider sign-up when provider name is empty', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'test@example.com' }, '_testuser'); + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminLinkProviderForUserCommand); + expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({ message: 'No provider name found' }), event); + }); + + it('should handle linkFederatedUser failure', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'test1@example.com' }, 'SAML_testuser'); + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'tes1t@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({ + IdentityProvider: { + AttributeMapping: { email: 'email' }, + }, + }); + const linkError = new Error('Link failed'); + mockCognitoClient.on(AdminLinkProviderForUserCommand).rejects(linkError); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminGetUserCommand, { + UserPoolId: userPoolId, + Username: 'test1@example.com', + }); + expect(mockCognitoClient).toHaveReceivedCommand(AdminLinkProviderForUserCommand); + expect(mockCallback).toHaveBeenCalledWith(linkError, event); + }); + }); + + describe('PreSignUp_AdminCreateUser', () => { + it('should allow admin-created user sign-up', async () => { + // ARRANGE + const event = createEvent('PreSignUp_AdminCreateUser', { email: 'admin@example.com' }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminGetUserCommand); + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminLinkProviderForUserCommand); + expect(mockCallback).toHaveBeenCalledWith(null, event); + }); + }); + + describe('Email validation', () => { + it('should reject sign-up with invalid email', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'invalid-email' }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminGetUserCommand); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ message: 'No valid email address found' }), + event, + ); + }); + + it('should reject sign-up with missing email attribute', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { + someAttribute: 'someAttributeValue', + }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminGetUserCommand); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ + message: + '"email" attribute not found in attribute mapping, please ensure you have setup an attribute mapping for "email" in your custom Cognito identity provider', + }), + event, + ); + }); + + it('should reject sign-up with undefined email', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: undefined as any }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminGetUserCommand); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ message: 'No valid email address found' }), + event, + ); + }); + + it('should reject sign-up with empty string email', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: '' }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminGetUserCommand); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ message: 'No valid email address found' }), + event, + ); + }); + }); + + describe('Unsupported trigger sources', () => { + it('should reject sign-up from unsupported trigger source', async () => { + // ARRANGE + const event = createEvent('PreSignUp_SignUp', { email: 'test@example.com' }); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminGetUserCommand); + expect(mockCallback).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Sign-up not allowed from this source' }), + event, + ); + }); + }); + + describe('Error handling', () => { + it('should handle getUserById error', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'test2@example.com' }, 'SAML_testuser'); + const getUserError = new Error('Database error'); + mockCognitoClient.on(AdminGetUserCommand).rejects(getUserError); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminGetUserCommand, { + UserPoolId: userPoolId, + Username: 'test2@example.com', + }); + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminLinkProviderForUserCommand); + expect(mockCallback).toHaveBeenCalledWith(new Error('User not found in local user pool'), event); + }); + + it('should handle non-Error exceptions', async () => { + // ARRANGE + const event = createEvent('PreSignUp_ExternalProvider', { email: 'test3@example.com' }, 'SAML_testuser'); + const stringError = 'String error'; + mockCognitoClient.on(AdminGetUserCommand).rejects(stringError); + + // ACT + await preSignUpHandler(event, mockContext, mockCallback); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminGetUserCommand, { + UserPoolId: userPoolId, + Username: 'test3@example.com', + }); + expect(mockCallback).toHaveBeenCalledWith(new Error('User not found in local user pool'), event); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/handlers/remediations.test.ts b/source/lambdas/api/__tests__/handlers/remediations.test.ts new file mode 100644 index 00000000..75ea0c9b --- /dev/null +++ b/source/lambdas/api/__tests__/handlers/remediations.test.ts @@ -0,0 +1,346 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CognitoIdentityProviderClient, + AdminGetUserCommand, + AdminListGroupsForUserCommand, +} from '@aws-sdk/client-cognito-identity-provider'; +import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; +import { findingsTableName } from '../../../common/__tests__/envSetup'; +import { searchRemediations } from '../../handlers/remediations'; +import { API_HEADERS } from '../../handlers/apiHandler'; +import { createMockContext, createMockEvent, TEST_REQUEST_CONTEXT } from '../utils'; + +const cognitoMock = mockClient(CognitoIdentityProviderClient); + +describe('RemediationsHandler Integration Tests', () => { + let dynamoDBDocumentClient: DynamoDBDocumentClient; + const remediationHistoryTableName = 'test-remediation-history-table'; + const userAccountMappingTableName = 'test-user-account-mapping-table'; + + beforeAll(async () => { + await DynamoDBTestSetup.initialize(); + dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + await DynamoDBTestSetup.createFindingsTable(findingsTableName); + await DynamoDBTestSetup.createRemediationHistoryTable(remediationHistoryTableName); + await DynamoDBTestSetup.createUserAccountMappingTable(userAccountMappingTableName); + }); + + afterAll(async () => { + await DynamoDBTestSetup.deleteTable(findingsTableName); + await DynamoDBTestSetup.deleteTable(remediationHistoryTableName); + await DynamoDBTestSetup.deleteTable(userAccountMappingTableName); + }); + + afterEach(() => { + cognitoMock.reset(); + delete process.env.FINDINGS_TABLE_NAME; + delete process.env.REMEDIATION_HISTORY_TABLE_NAME; + delete process.env.USER_ACCOUNT_MAPPING_TABLE_NAME; + delete process.env.USER_POOL_ID; + delete process.env.ORCHESTRATOR_ARN; + }); + + beforeEach(async () => { + await DynamoDBTestSetup.clearTable(findingsTableName, 'findings'); + await DynamoDBTestSetup.clearTable(remediationHistoryTableName, 'remediationHistory'); + await DynamoDBTestSetup.clearTable(userAccountMappingTableName, 'userAccountMapping'); + process.env.FINDINGS_TABLE_NAME = findingsTableName; + process.env.REMEDIATION_HISTORY_TABLE_NAME = remediationHistoryTableName; + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME = userAccountMappingTableName; + process.env.USER_POOL_ID = 'test-user-pool-id'; + process.env.ORCHESTRATOR_ARN = 'arn:aws:states:us-east-1:123456789012:stateMachine:test-orchestrator'; + + cognitoMock.reset(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: 'admin-user@example.com', + UserAttributes: [ + { Name: 'email', Value: 'admin-user@example.com' }, + { Name: 'custom:invitedBy', Value: 'system@example.com' }, + ], + UserCreateDate: new Date(), + UserStatus: 'CONFIRMED', + }); + + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AdminGroup' }], + }); + }); + + describe('searchRemediations', () => { + it('should return 200 with empty remediations when no data exists', async () => { + const requestBody = { + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc', + }, + ], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/remediations', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchRemediations(event, context); + + expect(result.statusCode).toBe(200); + expect(result.headers).toEqual(API_HEADERS.REMEDIATIONS); + + const body = JSON.parse(result.body); + expect(body.Remediations).toEqual([]); + expect(body.NextToken).toBeUndefined(); + }); + + it('should return 200 with remediations when data exists', async () => { + const remediationItem = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-remediation', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-remediation#arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + resourceType: 'AWS::Lambda::Function', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-01T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-remediation', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'admin-user@example.com', + executionId: 'arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, // 90 days from now + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: remediationHistoryTableName, + Item: remediationItem, + }), + ); + + const requestBody = { + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc', + }, + ], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/remediations', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchRemediations(event, context); + + expect(result.statusCode).toBe(200); + expect(result.headers).toEqual(API_HEADERS.REMEDIATIONS); + + const body = JSON.parse(result.body); + expect(body.Remediations).toHaveLength(1); + expect(body.Remediations[0]).toHaveProperty('findingId'); + expect(body.Remediations[0]).toHaveProperty('accountId', '123456789012'); + expect(body.Remediations[0]).toHaveProperty('remediationStatus', 'SUCCESS'); + expect(body.Remediations[0]).toHaveProperty('severity', 'HIGH'); + + expect(body.Remediations[0]).not.toHaveProperty('findingId#executionId'); + expect(body.Remediations[0]).not.toHaveProperty('lastUpdatedTime#findingId'); + expect(body.Remediations[0]).not.toHaveProperty('REMEDIATION_CONSTANT'); + expect(body.Remediations[0]).not.toHaveProperty('expireAt'); + }); + + it('should filter remediations by accountId', async () => { + const remediation1 = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-1', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-1#exec-1', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test-1', + resourceType: 'AWS::Lambda::Function', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-01T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-1', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'admin-user@example.com', + executionId: 'exec-1', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + const remediation2 = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:987654321098:security-control/Lambda.3/finding/test-2', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:987654321098:security-control/Lambda.3/finding/test-2#exec-2', + accountId: '987654321098', + resourceId: 'arn:aws:lambda:us-east-1:987654321098:function:test-2', + resourceType: 'AWS::Lambda::Function', + severity: 'MEDIUM', + region: 'us-east-1', + remediationStatus: 'FAILED', + lastUpdatedTime: '2023-01-02T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-02T00:00:00Z#arn:aws:securityhub:us-east-1:987654321098:security-control/Lambda.3/finding/test-2', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'admin-user@example.com', + executionId: 'exec-2', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + await Promise.all([ + dynamoDBDocumentClient.send(new PutCommand({ TableName: remediationHistoryTableName, Item: remediation1 })), + dynamoDBDocumentClient.send(new PutCommand({ TableName: remediationHistoryTableName, Item: remediation2 })), + ]); + + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'accountId', + Filter: { + Value: '123456789012', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc', + }, + ], + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/remediations', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const result = await searchRemediations(event, context); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.Remediations).toHaveLength(1); + expect(body.Remediations[0].accountId).toBe('123456789012'); + }); + + it('should throw UnauthorizedError when claims are missing', async () => { + const event = createMockEvent({ + httpMethod: 'POST', + path: '/remediations', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: null, + }, + }, + }); + const context = createMockContext(); + + await expect(searchRemediations(event, context)).rejects.toThrow( + "Cannot read properties of null (reading 'cognito:groups')", + ); + }); + + it('should throw BadRequestError when request validation fails', async () => { + const requestBody = { + Filters: { + CompositeFilters: [ + { + Operator: 'INVALID_OPERATOR', // Invalid operator + StringFilters: [], + }, + ], + }, + }; + + const event = createMockEvent({ + httpMethod: 'POST', + path: '/remediations', + headers: { + 'Content-Type': 'application/json', + authorization: 'Bearer valid-token', + }, + body: JSON.stringify(requestBody), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + await expect(searchRemediations(event, context)).rejects.toThrow('Invalid request'); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/handlers/users.test.ts b/source/lambdas/api/__tests__/handlers/users.test.ts new file mode 100644 index 00000000..3f13ff5b --- /dev/null +++ b/source/lambdas/api/__tests__/handlers/users.test.ts @@ -0,0 +1,1544 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; +import { + AdminAddUserToGroupCommand, + AdminCreateUserCommand, + AdminDeleteUserCommand, + AdminGetUserCommand, + CognitoIdentityProviderClient, + ListUsersCommand, + AdminListGroupsForUserCommand, +} from '@aws-sdk/client-cognito-identity-provider'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; +import { userAccountMappingTableName } from '../../../common/__tests__/envSetup'; +import { UserAccountMapping } from '@asr/data-models'; +import { ForbiddenError, NotFoundError, BadRequestError } from '../../../common/utils/httpErrors'; +import { createMockEvent, createMockContext, TEST_REQUEST_CONTEXT } from '../utils'; +import { + setupMetricsMocks, + cleanupMetricsMocks, + createMetricsTestScope, +} from '../../../common/__tests__/metricsMockSetup'; + +import { getUsers, inviteUser, putUser, deleteUser } from '../../handlers/users'; + +const cognitoMock = mockClient(CognitoIdentityProviderClient); + +describe('UsersHandler', () => { + let dynamoDBDocumentClient: DynamoDBDocumentClient; + + beforeAll(async () => { + await DynamoDBTestSetup.initialize(); + dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + + // Create user account mapping table + await DynamoDBTestSetup.createUserAccountMappingTable(userAccountMappingTableName); + }); + + afterAll(async () => { + await DynamoDBTestSetup.deleteTable(userAccountMappingTableName); + cleanupMetricsMocks(); + }); + + beforeEach(async () => { + cognitoMock.reset(); + setupMetricsMocks(); + await DynamoDBTestSetup.clearTable(userAccountMappingTableName, 'userAccountMapping'); + + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME = userAccountMappingTableName; + + cognitoMock.on(AdminGetUserCommand).callsFake((input) => { + const username = input.Username; + return Promise.resolve({ + Username: username, + UserAttributes: [ + { Name: 'email', Value: username }, + { Name: 'custom:invitedBy', Value: 'system@example.com' }, + ], + UserCreateDate: new Date(), + UserStatus: 'CONFIRMED', + }); + }); + + cognitoMock.on(AdminListGroupsForUserCommand).callsFake((input) => { + const username = input.Username; + let groups = []; + + if (username?.includes('admin-user') || username?.includes('admin@') || username?.includes('super@')) { + groups = [{ GroupName: 'AdminGroup' }]; + } else if (username?.includes('delegated@')) { + groups = [{ GroupName: 'DelegatedAdminGroup' }]; + } else if (username?.includes('operator@')) { + groups = [{ GroupName: 'AccountOperatorGroup' }]; + } else { + groups = [{ GroupName: 'DelegatedAdminGroup' }]; + } + + return Promise.resolve({ Groups: groups }); + }); + }); + + describe('getUsers', () => { + it('should throw 403 when user lacks required groups', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['RegularUserGroup'], + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(getUsers(event, context)).rejects.toThrow(ForbiddenError); + }); + + it('should throw error when DelegatedAdmin tries to access without type parameter', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(getUsers(event, context)).rejects.toThrow( + 'Only Admins can access GET /users without "type" query parameter', + ); + }); + + it('should throw ForbiddenError when cognito:groups is a string containing AdminGroup', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': 'FakeAdminGroup', + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(getUsers(event, context)).rejects.toThrow(new ForbiddenError()); + }); + + it('should throw error when DelegatedAdmin tries to access non-accountOperators type', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + queryStringParameters: { type: 'admins' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(getUsers(event, context)).rejects.toThrow( + 'DelegatedAdminGroup can only fetch Account Operators. You must provide the "type" query parameter with value "accountOperators".', + ); + }); + + it('should throw error when cognito:groups is DelegatedAdminGroup (string) and tries to access non-accountOperators type', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + queryStringParameters: { type: 'admins' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': 'DelegatedAdminGroup', + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(getUsers(event, context)).rejects.toThrow( + 'DelegatedAdminGroup can only fetch Account Operators. You must provide the "type" query parameter with value "accountOperators".', + ); + }); + + it('should return all users for Admin without type filter', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'user1', + Attributes: [ + { Name: 'email', Value: 'user1@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }, + { + Username: 'user2', + Attributes: [ + { Name: 'email', Value: 'user2@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-02'), + UserStatus: 'FORCE_CHANGE_PASSWORD', + }, + ], + }); + + cognitoMock.on(AdminListGroupsForUserCommand, { Username: 'user1' }).resolves({ + Groups: [{ GroupName: 'AdminGroup' }], + }); + + cognitoMock.on(AdminListGroupsForUserCommand, { Username: 'user2' }).resolves({ + Groups: [{ GroupName: 'DelegatedAdminGroup' }], + }); + + // ACT + const result = await getUsers(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body).toHaveLength(2); + expect(body[0].email).toBe('user1@example.com'); + expect(body[0].type).toBe('admin'); + expect(body[0].status).toBe('Confirmed'); + expect(body[1].email).toBe('user2@example.com'); + expect(body[1].type).toBe('delegated-admin'); + expect(body[1].status).toBe('Invited'); + }); + + it('should filter users by type when type parameter is provided', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + queryStringParameters: { type: 'accountOperators' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // Add user account mapping for account operator + const userAccountMapping: UserAccountMapping = { + userId: 'operator@example.com', + accountIds: ['123456789012', '987654321098'], + invitedBy: 'admin@example.com', + invitationTimestamp: '2023-01-01T00:00:00Z', + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: userAccountMapping, + }), + ); + + cognitoMock.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'admin-user', + Attributes: [ + { Name: 'email', Value: 'admin@example.com' }, + { Name: 'custom:invitedBy', Value: 'superadmin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }, + { + Username: 'operator-user', + Attributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-02'), + UserStatus: 'CONFIRMED', + }, + ], + }); + + cognitoMock.on(AdminListGroupsForUserCommand, { Username: 'admin-user' }).resolves({ + Groups: [{ GroupName: 'AdminGroup' }], + }); + + cognitoMock.on(AdminListGroupsForUserCommand, { Username: 'operator-user' }).resolves({ + Groups: [{ GroupName: 'AccountOperatorGroup' }], + }); + + // ACT + const result = await getUsers(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body).toHaveLength(1); + expect(body[0].email).toBe('operator@example.com'); + expect(body[0].type).toBe('account-operator'); + expect(body[0].accountIds).toEqual(['123456789012', '987654321098']); + }); + + it('should skip users with missing required attributes', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'incomplete-user', + Attributes: [ + { Name: 'email', Value: 'incomplete@example.com' }, + // Missing custom:invitedBy + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }, + ], + }); + + // ACT + const result = await getUsers(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body).toHaveLength(0); + }); + + it('should skip users with no recognized groups', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'unrecognized-user', + Attributes: [ + { Name: 'email', Value: 'unrecognized@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }, + ], + }); + + cognitoMock.on(AdminListGroupsForUserCommand, { Username: 'unrecognized-user' }).resolves({ + Groups: [{ GroupName: 'UnknownGroup' }], + }); + + // ACT + const result = await getUsers(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body).toHaveLength(0); + }); + + it('should throw on Cognito service errors', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin-user@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(ListUsersCommand).rejects(new Error('Cognito service error')); + + await expect(getUsers(event, context)).rejects.toThrow(); + }); + }); + + describe('inviteUser', () => { + it('should throw 403 when user lacks required groups', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newuser@example.com', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AccountOperatorGroup'], + username: 'testuser@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow(ForbiddenError); + }); + + it('should throw BadRequestError for invalid email format', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'invalid-email', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow(BadRequestError); + }); + + it('should throw BadRequestError for invalid role', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newuser@example.com', + role: 'InvalidRole', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow(BadRequestError); + }); + + it('should throw BadRequestError when AccountOperator role lacks accountIds', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newuser@example.com', + role: 'AccountOperator', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow( + new BadRequestError('accountIds is required for AccountOperator role'), + ); + }); + + it('should throw BadRequestError when AccountOperator role has empty accountIds', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newuser@example.com', + role: 'AccountOperator', + accountIds: [], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow( + new BadRequestError('Invalid request: accountIds: Array must contain at least 1 element(s)'), + ); + }); + + it('should throw ForbiddenError when DelegatedAdmin tries to create DelegatedAdmin user', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newuser@example.com', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow( + new ForbiddenError('DelegatedAdminGroup can only create AccountOperator users'), + ); + }); + + it('should successfully create DelegatedAdmin user as Admin', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newdelegated@example.com', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + const metricsScope = createMetricsTestScope(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ + User: { Username: 'newdelegated@example.com' }, + }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await inviteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + const body = JSON.parse(result.body); + expect(body.message).toBe('User invited successfully'); + expect(body.email).toBe('newdelegated@example.com'); + expect(cognitoMock).toHaveReceivedCommandWith(AdminCreateUserCommand, { + UserPoolId: expect.any(String), + Username: 'newdelegated@example.com', + UserAttributes: [ + { Name: 'email', Value: 'newdelegated@example.com' }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + expect(cognitoMock).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: expect.any(String), + Username: 'newdelegated@example.com', + GroupName: 'DelegatedAdminGroup', + }); + + // Wait for next event loop tick to allow async HTTP request to complete + await new Promise((resolve) => setTimeout(resolve, 5)); + expect(metricsScope.isDone()).toBe(true); + }); + + it('should successfully create AccountOperator user as Admin with account mappings', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newoperator@example.com', + role: 'AccountOperator', + accountIds: ['123456789012', '987654321098'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ + User: { Username: 'newoperator@example.com' }, + }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await inviteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + const body = JSON.parse(result.body); + expect(body.message).toBe('User invited successfully'); + expect(body.email).toBe('newoperator@example.com'); + expect(cognitoMock).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: expect.any(String), + Username: 'newoperator@example.com', + GroupName: 'AccountOperatorGroup', + }); + + // Verify UserAccountMapping was created in DynamoDB + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'newoperator@example.com' }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.userId).toBe('newoperator@example.com'); + expect(getResponse.Item?.accountIds).toEqual(['123456789012', '987654321098']); + expect(getResponse.Item?.invitedBy).toBe('admin@example.com'); + }); + + it('should successfully create AccountOperator user as DelegatedAdmin', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newoperator@example.com', + role: 'AccountOperator', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ + User: { Username: 'newoperator@example.com' }, + }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await inviteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + const body = JSON.parse(result.body); + expect(body.message).toBe('User invited successfully'); + expect(body.email).toBe('newoperator@example.com'); + + // Verify UserAccountMapping was created in DynamoDB + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'newoperator@example.com' }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.userId).toBe('newoperator@example.com'); + expect(getResponse.Item?.accountIds).toEqual(['123456789012']); + expect(getResponse.Item?.invitedBy).toBe('delegated@example.com'); + }); + + it('should throw when Cognito service fails', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newuser@example.com', + role: 'DelegatedAdmin', + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).rejects(new Error('Cognito service error')); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow('Cognito service error'); + }); + + it('should throw BadRequestError for invalid accountIds format', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newuser@example.com', + role: 'AccountOperator', + accountIds: ['invalid-account-id'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(inviteUser(event, context)).rejects.toThrow(BadRequestError); + }); + + it('should handle string cognito:groups for DelegatedAdmin', async () => { + // ARRANGE + const event = createMockEvent({ + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'newoperator@example.com', + role: 'AccountOperator', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': 'DelegatedAdminGroup', + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminCreateUserCommand).resolves({ + User: { Username: 'newoperator@example.com' }, + }); + cognitoMock.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + const result = await inviteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(201); + + // Verify UserAccountMapping was created in DynamoDB + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'newoperator@example.com' }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.userId).toBe('newoperator@example.com'); + expect(getResponse.Item?.accountIds).toEqual(['123456789012']); + expect(getResponse.Item?.invitedBy).toBe('delegated@example.com'); + }); + }); + + describe('putUser', () => { + it('should throw BadRequestError when user ID is missing', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'PUT', + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + pathParameters: null, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(putUser(event, context)).rejects.toThrow(new BadRequestError('User ID is required')); + }); + + it('should throw BadRequestError when user type is not account-operator', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'PUT', + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + pathParameters: { id: 'user@example.com' }, + body: JSON.stringify({ + type: 'admin', + email: 'myuser@example.com', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(putUser(event, context)).rejects.toThrow( + new BadRequestError('Only account-operator users can be updated'), + ); + }); + + it('should throw BadRequestError for invalid account IDs format', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'PUT', + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + pathParameters: { id: 'user@example.com' }, + body: JSON.stringify({ + type: 'account-operator', + accountIds: ['invalid-account-id'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(putUser(event, context)).rejects.toThrow(BadRequestError); + }); + + it('should throw 403 when user lacks required groups', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'PUT', + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + pathParameters: { id: 'user@example.com' }, + body: JSON.stringify({ + type: 'account-operator', + email: 'user@example.com', + status: 'Confirmed', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AccountOperatorGroup'], + username: 'operator@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(putUser(event, context)).rejects.toThrow(ForbiddenError); + }); + + it('should successfully update account operator user as Admin', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const event = createMockEvent({ + httpMethod: 'PUT', + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + pathParameters: { id: userId }, + body: JSON.stringify({ + type: 'account-operator', + email: userId, + status: 'Confirmed', + accountIds: ['123456789012', '987654321098'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // Mock existing user in Cognito + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: userId, + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AccountOperatorGroup' }], + }); + + // Create existing user account mapping + const existingMapping: UserAccountMapping = { + userId, + accountIds: ['111111111111'], + invitedBy: 'admin@example.com', + invitationTimestamp: '2023-01-01T00:00:00Z', + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: existingMapping, + }), + ); + + // ACT + const result = await putUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe('User updated successfully'); + + // Verify DynamoDB was updated + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.accountIds).toEqual(['123456789012', '987654321098']); + expect(getResponse.Item?.lastModifiedBy).toBe('UsersAPI'); + expect(getResponse.Item?.lastModifiedTimestamp).toBeDefined(); + }); + + it('should successfully update account operator user as DelegatedAdmin', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const event = createMockEvent({ + httpMethod: 'PUT', + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + pathParameters: { id: userId }, + body: JSON.stringify({ + type: 'account-operator', + email: userId, + status: 'Confirmed', + accountIds: ['555555555555'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // Mock existing user in Cognito + cognitoMock.on(AdminGetUserCommand).resolves({ + Username: userId, + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'delegated@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ + Groups: [{ GroupName: 'AccountOperatorGroup' }], + }); + + // ACT + const result = await putUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe('User updated successfully'); + + // Verify DynamoDB was updated + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.accountIds).toEqual(['555555555555']); + expect(getResponse.Item?.invitedBy).toBe('delegated@example.com'); + }); + + it('should throw NotFoundError when user does not exist in Cognito', async () => { + // ARRANGE + const userId = 'nonexistent@example.com'; + const event = createMockEvent({ + httpMethod: 'PUT', + headers: { authorization: 'Bearer valid-token', 'Content-Type': 'application/json' }, + pathParameters: { id: userId }, + body: JSON.stringify({ + type: 'account-operator', + email: userId, + status: 'Confirmed', + accountIds: ['123456789012'], + }), + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).rejects(new Error('User not found')); + + // ACT & ASSERT + await expect(putUser(event, context)).rejects.toThrow(NotFoundError); + }); + }); + + describe('deleteUser', () => { + it('should throw BadRequestError when user ID is missing', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: null, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(deleteUser(event, context)).rejects.toThrow( + new BadRequestError('Valid email address is required for user ID'), + ); + }); + + it('should throw BadRequestError when user ID is not a valid email', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: 'invalid-email' }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // ACT & ASSERT + await expect(deleteUser(event, context)).rejects.toThrow( + new BadRequestError('Valid email address is required for user ID'), + ); + }); + + it('should throw NotFoundError when user does not exist', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent('nonexistent@example.com') }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).rejects(new Error('User not found')); + + // ACT & ASSERT + await expect(deleteUser(event, context)).rejects.toThrow( + new NotFoundError('User nonexistent@example.com not found.'), + ); + }); + + it('should throw 403 when user lacks required groups', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent('user@example.com') }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AccountOperatorGroup'], + username: 'operator@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'user@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + + // ACT & ASSERT + await expect(deleteUser(event, context)).rejects.toThrow(ForbiddenError); + }); + + it('should throw ForbiddenError when DelegatedAdmin tries to delete non-AccountOperator user', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent('admin@example.com') }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'admin@example.com' }, + { Name: 'custom:invitedBy', Value: 'super@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + + // ACT & ASSERT + await expect(deleteUser(event, context)).rejects.toThrow( + new ForbiddenError('DelegatedAdminGroup can only delete AccountOperator users'), + ); + }); + + it('should successfully delete admin user as Admin', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent('admin@example.com') }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'super@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'admin@example.com' }, + { Name: 'custom:invitedBy', Value: 'super@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + + // ACT + const result = await deleteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe('User deleted successfully'); + expect(cognitoMock).toHaveReceivedCommandWith(AdminDeleteUserCommand, { + UserPoolId: expect.any(String), + Username: 'admin@example.com', + }); + }); + + it('should successfully delete account-operator user and remove DynamoDB mapping as Admin', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent(userId) }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // Create user account mapping in DynamoDB + const userAccountMapping: UserAccountMapping = { + userId, + accountIds: ['123456789012', '987654321098'], + invitedBy: 'admin@example.com', + invitationTimestamp: '2023-01-01T00:00:00Z', + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: userAccountMapping, + }), + ); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + + // ACT + const result = await deleteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe('User deleted successfully'); + expect(cognitoMock).toHaveReceivedCommandWith(AdminDeleteUserCommand, { + UserPoolId: expect.any(String), + Username: userId, + }); + + // Verify DynamoDB mapping was deleted + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId }, + }), + ); + expect(getResponse.Item).toBeUndefined(); + }); + + it('should successfully delete account-operator user as DelegatedAdmin', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent(userId) }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['DelegatedAdminGroup'], + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + // Create user account mapping in DynamoDB + const userAccountMapping: UserAccountMapping = { + userId, + accountIds: ['123456789012'], + invitedBy: 'delegated@example.com', + invitationTimestamp: '2023-01-01T00:00:00Z', + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: userAccountMapping, + }), + ); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'delegated@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + + // ACT + const result = await deleteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe('User deleted successfully'); + expect(cognitoMock).toHaveReceivedCommandWith(AdminDeleteUserCommand, { + UserPoolId: expect.any(String), + Username: userId, + }); + + // Verify DynamoDB mapping was deleted + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId }, + }), + ); + expect(getResponse.Item).toBeUndefined(); + }); + + it('should handle URL encoded email addresses correctly', async () => { + // ARRANGE + const userId = 'user+test@example.com'; + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent(userId) }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + + // ACT + const result = await deleteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + expect(cognitoMock).toHaveReceivedCommandWith(AdminDeleteUserCommand, { + UserPoolId: expect.any(String), + Username: userId, + }); + }); + + it('should handle Cognito delete failure', async () => { + // ARRANGE + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent('user@example.com') }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': ['AdminGroup'], + username: 'admin@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'user@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + cognitoMock.on(AdminDeleteUserCommand).rejects(new Error('Cognito delete failed')); + + // ACT & ASSERT + await expect(deleteUser(event, context)).rejects.toThrow('Cognito delete failed'); + }); + + it('should handle string cognito:groups for DelegatedAdmin', async () => { + // ARRANGE + const userId = 'operator@example.com'; + const event = createMockEvent({ + httpMethod: 'DELETE', + headers: { authorization: 'Bearer valid-token' }, + pathParameters: { id: encodeURIComponent(userId) }, + requestContext: { + ...TEST_REQUEST_CONTEXT, + authorizer: { + claims: { + 'cognito:groups': 'DelegatedAdminGroup', + username: 'delegated@example.com', + }, + }, + }, + }); + const context = createMockContext(); + + cognitoMock.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'delegated@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + cognitoMock.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + cognitoMock.on(AdminDeleteUserCommand).resolves({}); + + // ACT + const result = await deleteUser(event, context); + + // ASSERT + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe('User deleted successfully'); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/services/authorization.test.ts b/source/lambdas/api/__tests__/services/authorization.test.ts new file mode 100644 index 00000000..2f4d2c34 --- /dev/null +++ b/source/lambdas/api/__tests__/services/authorization.test.ts @@ -0,0 +1,182 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { AuthorizationService } from '../../services/authorization'; +import { ForbiddenError, UnauthorizedError } from '../../../common/utils/httpErrors'; +import { + CognitoIdentityProviderClient, + AdminGetUserCommand, + AdminListGroupsForUserCommand, +} from '@aws-sdk/client-cognito-identity-provider'; +import { mockClient } from 'aws-sdk-client-mock'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; + +const cognitoMock = mockClient(CognitoIdentityProviderClient); + +describe('AuthorizationService', () => { + let service: AuthorizationService; + let mockLogger: Logger; + const userAccountMappingTableName = 'test-user-account-mapping-table'; + + beforeAll(async () => { + await DynamoDBTestSetup.initialize(); + await DynamoDBTestSetup.createUserAccountMappingTable(userAccountMappingTableName); + }); + + afterAll(async () => { + await DynamoDBTestSetup.deleteTable(userAccountMappingTableName); + }); + + beforeEach(async () => { + await DynamoDBTestSetup.clearTable(userAccountMappingTableName, 'userAccountMapping'); + process.env.USER_POOL_ID = 'us-east-1_testpool'; + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME = userAccountMappingTableName; + mockLogger = new Logger({ serviceName: 'test' }); + service = new AuthorizationService(mockLogger); + + cognitoMock.reset(); + + // Mock AdminGetUserCommand to return user data based on the username + cognitoMock.on(AdminGetUserCommand).callsFake((input) => { + const username = input.Username; + return Promise.resolve({ + Username: username, + UserAttributes: [ + { Name: 'email', Value: username }, + { Name: 'custom:invitedBy', Value: 'system@example.com' }, + ], + UserCreateDate: new Date(), + UserStatus: 'CONFIRMED', + }); + }); + + // Mock AdminListGroupsForUserCommand to return appropriate groups + cognitoMock.on(AdminListGroupsForUserCommand).callsFake((input) => { + const username = input.Username; + let groups = []; + + if (username?.includes('admin')) { + groups = [{ GroupName: 'AdminGroup' }]; + } else { + groups = [{ GroupName: 'DelegatedAdminGroup' }]; + } + + return Promise.resolve({ Groups: groups }); + }); + }); + + describe('authenticateAndAuthorize', () => { + it('should return authenticated user when valid claims and groups', async () => { + // ARRANGE + const claims = { + 'cognito:groups': ['admin', 'user'], + username: 'test@example.com', + }; + const requiredGroups = ['admin']; + + // ACT + const result = await service.authenticateAndAuthorize(claims, requiredGroups); + + // ASSERT + expect(result).toEqual({ + username: 'test@example.com', + groups: ['admin', 'user'], + email: 'test@example.com', + authorizedAccounts: undefined, + }); + }); + + it('should throw ForbiddenError when user lacks required group', async () => { + // ARRANGE + const claims = { + 'cognito:groups': ['user', 'viewer'], + username: 'test@example.com', + }; + const requiredGroups = ['admin']; + + // ACT & ASSERT + await expect(service.authenticateAndAuthorize(claims, requiredGroups)).rejects.toThrow(new ForbiddenError()); + }); + + it('should succeed when user has one of multiple required groups', async () => { + // ARRANGE + const claims = { + 'cognito:groups': ['user', 'editor'], + username: 'test@example.com', + }; + const requiredGroups = ['admin', 'editor']; + + // ACT + const result = await service.authenticateAndAuthorize(claims, requiredGroups); + + // ASSERT + expect(result.username).toBe('test@example.com'); + expect(result.groups).toEqual(['user', 'editor']); + }); + + it('should handle empty groups array', async () => { + // ARRANGE + const claims = { + 'cognito:groups': [], + username: 'test@example.com', + }; + const requiredGroups = ['admin']; + + // ACT & ASSERT + await expect(service.authenticateAndAuthorize(claims, requiredGroups)).rejects.toThrow(new ForbiddenError()); + }); + + it('should handle empty groups string', async () => { + // ARRANGE + const claims = { + 'cognito:groups': '', + username: 'test@example.com', + }; + const requiredGroups = ['admin']; + + // ACT & ASSERT + await expect(service.authenticateAndAuthorize(claims, requiredGroups)).rejects.toThrow(new ForbiddenError()); + }); + + it('should convert cognito:groups from string to array', async () => { + // ARRANGE + const claims = { + 'cognito:groups': 'admin', + username: 'test@example.com', + }; + const requiredGroups = ['admin']; + + // ACT + const result = await service.authenticateAndAuthorize(claims, requiredGroups); + + // ASSERT + expect(result.username).toBe('test@example.com'); + expect(result.groups).toEqual(['admin']); + }); + + it('should not permit groups that include a substring of requiredGroups', async () => { + // ARRANGE + const claims = { + 'cognito:groups': 'fakeadmin', + username: 'test@example.com', + }; + const requiredGroups = ['admin']; + + // ACT & ASSERT + await expect(service.authenticateAndAuthorize(claims, requiredGroups)).rejects.toThrow(new ForbiddenError()); + }); + + it('should handle empty required groups array', async () => { + // ARRANGE + const claims = { + 'cognito:groups': ['user'], + username: 'test@example.com', + }; + const requiredGroups: string[] = []; + + // ACT & ASSERT + await expect(service.authenticateAndAuthorize(claims, requiredGroups)).rejects.toThrow(new ForbiddenError()); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/services/cognito.test.ts b/source/lambdas/api/__tests__/services/cognito.test.ts new file mode 100644 index 00000000..5291efe7 --- /dev/null +++ b/source/lambdas/api/__tests__/services/cognito.test.ts @@ -0,0 +1,1149 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { CognitoService } from '../../services/cognito'; +import 'aws-sdk-client-mock-jest'; +import { + AdminAddUserToGroupCommand, + AdminCreateUserCommand, + AdminDeleteUserCommand, + CognitoIdentityProviderClient, + ListUsersCommand, + AdminGetUserCommand, + AdminListGroupsForUserCommand, + DescribeIdentityProviderCommand, + AdminLinkProviderForUserCommand, + UsernameExistsException, +} from '@aws-sdk/client-cognito-identity-provider'; +import { UserAccountMappingRepository } from '../../../common/repositories/userAccountMappingRepository'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; +import { userAccountMappingTableName } from '../../../common/__tests__/envSetup'; +import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb'; +import { UserAccountMapping } from '@asr/data-models'; +import { mockClient } from 'aws-sdk-client-mock'; +import { createMockUserAccountMapping } from '../../../common/__tests__/userAccountMappingRepository.test'; +import { BadRequestError, NotFoundError } from '../../../common/utils/httpErrors'; + +const mockCognitoClient = mockClient(CognitoIdentityProviderClient); + +describe('CognitoService', () => { + let service: CognitoService; + let mockLogger: Logger; + let dynamoDBDocumentClient: DynamoDBDocumentClient; + let userAccountMappingRepository: UserAccountMappingRepository; + + beforeAll(async () => { + await DynamoDBTestSetup.initialize(); + dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + await DynamoDBTestSetup.createUserAccountMappingTable(userAccountMappingTableName); + }); + + afterAll(async () => { + await DynamoDBTestSetup.deleteTable(userAccountMappingTableName); + }); + + beforeEach(async () => { + mockCognitoClient.reset(); + await DynamoDBTestSetup.clearTable(userAccountMappingTableName, 'userAccountMapping'); + + process.env.USER_POOL_ID = 'us-east-1_testpool'; + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME = userAccountMappingTableName; + + mockLogger = new Logger({ serviceName: 'test' }); + userAccountMappingRepository = new UserAccountMappingRepository( + 'UsersAPI', + userAccountMappingTableName, + dynamoDBDocumentClient, + ); + + service = new CognitoService(mockLogger); + (service as any).userAccountMappingRepository = userAccountMappingRepository; + // Clear cache to ensure clean state between tests + (service as any).userCache.clear(); + }); + + const createUserAccountMapping = (userId: string, accountIds: string[]): UserAccountMapping => + createMockUserAccountMapping({ userId, accountIds }); + + describe('getAllUsers', () => { + it('should return all users with complete data', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'user1', + Attributes: [ + { Name: 'email', Value: 'admin@example.com' }, + { Name: 'custom:invitedBy', Value: 'super@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }, + { + Username: 'user2', + Attributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-02'), + UserStatus: 'FORCE_CHANGE_PASSWORD', + }, + ], + }); + + mockCognitoClient + .on(AdminListGroupsForUserCommand, { Username: 'user1' }) + .resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + mockCognitoClient + .on(AdminListGroupsForUserCommand, { Username: 'user2' }) + .resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: createUserAccountMapping('operator@example.com', ['123456789012']), + }), + ); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + email: 'admin@example.com', + invitedBy: 'super@example.com', + invitationTimestamp: '2023-01-01T00:00:00.000Z', + status: 'Confirmed', + type: 'admin', + }); + expect(result[1]).toEqual({ + email: 'operator@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: '2023-01-02T00:00:00.000Z', + status: 'Invited', + type: 'account-operator', + accountIds: ['123456789012'], + }); + }); + + it('should skip users with missing email', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'user1', + Attributes: [{ Name: 'custom:invitedBy', Value: 'admin@example.com' }], + }, + ], + }); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(0); + }); + + it('should skip users with missing invitedBy', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'user1', + Attributes: [{ Name: 'email', Value: 'test@example.com' }], + }, + ], + }); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(0); + }); + + it('should skip users with no recognized groups', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'user1', + Attributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }, + ], + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'UnknownGroup' }] }); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(0); + }); + + it('should handle empty users response', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({ Users: [] }); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(0); + }); + + it('should handle undefined users response', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({}); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(0); + }); + + it('should handle delegated admin user type', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'user1', + Attributes: [ + { Name: 'email', Value: 'delegated@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }, + ], + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'DelegatedAdminGroup' }] }); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(1); + expect(result[0].type).toBe('delegated-admin'); + }); + + it('should handle empty groups response', async () => { + // ARRANGE + mockCognitoClient.on(ListUsersCommand).resolves({ + Users: [ + { + Username: 'user1', + Attributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }, + ], + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [] }); + + // ACT + const result = await service.getAllUsers(); + + // ASSERT + expect(result).toHaveLength(0); + }); + + it('should throw error when Cognito fails', async () => { + // ARRANGE + const error = new Error('Cognito error'); + mockCognitoClient.on(ListUsersCommand).rejects(error); + + // ACT & ASSERT + await expect(service.getAllUsers()).rejects.toThrow('Cognito error'); + }); + }); + + describe('getUserById', () => { + it('should return user when found with complete data', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + + // ACT + const result = await service.getUserById('user1'); + + // ASSERT + expect(result).toEqual({ + email: 'test@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: '2023-01-01T00:00:00.000Z', + status: 'Confirmed', + type: 'admin', + }); + }); + + it('should return null when user not found', async () => { + // ARRANGE + const error = new Error('User not found'); + mockCognitoClient.on(AdminGetUserCommand).rejects(error); + + // ACT + const result = await service.getUserById('nonexistent'); + + // ASSERT + expect(result).toBeNull(); + }); + + it('should return null when email missing', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [{ Name: 'custom:invitedBy', Value: 'admin@example.com' }], + }); + + // ACT + const result = await service.getUserById('user1'); + + // ASSERT + expect(result).toBeNull(); + }); + + it('should return null when invitedBy missing', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [{ Name: 'email', Value: 'test@example.com' }], + }); + + // ACT + const result = await service.getUserById('user1'); + + // ASSERT + expect(result).toBeNull(); + }); + + it('should return null when no recognized groups', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'UnknownGroup' }] }); + + // ACT + const result = await service.getUserById('user1'); + + // ASSERT + expect(result).toBeNull(); + }); + + it('should handle account operator user type', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'FORCE_CHANGE_PASSWORD', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: createUserAccountMapping('operator@example.com', ['123456789012', '987654321098']), + }), + ); + + // ACT + const result = await service.getUserById('user1'); + + // ASSERT + expect(result).toEqual({ + email: 'operator@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: '2023-01-01T00:00:00.000Z', + status: 'Invited', + type: 'account-operator', + accountIds: ['123456789012', '987654321098'], + }); + }); + }); + + describe('createUser', () => { + it('should successfully create DelegatedAdmin user', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'delegated@example.com' }, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('delegated@example.com', 'DelegatedAdmin', 'admin@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminCreateUserCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'delegated@example.com', + UserAttributes: [ + { Name: 'email', Value: 'delegated@example.com' }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'delegated@example.com', + GroupName: 'DelegatedAdminGroup', + }); + }); + + it('should successfully create AccountOperator user with account mappings', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'operator@example.com' }, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('operator@example.com', 'AccountOperator', 'admin@example.com', [ + '123456789012', + '987654321098', + ]); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminCreateUserCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'operator@example.com', + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'operator@example.com', + GroupName: 'AccountOperatorGroup', + }); + + // Verify UserAccountMapping was created in DynamoDB + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.userId).toBe('operator@example.com'); + expect(getResponse.Item?.accountIds).toEqual(['123456789012', '987654321098']); + expect(getResponse.Item?.invitedBy).toBe('admin@example.com'); + expect(getResponse.Item?.invitationTimestamp).toBeDefined(); + }); + + it('should successfully create AccountOperator user without account mappings when accountIds not provided', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'operator@example.com' }, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('operator@example.com', 'AccountOperator', 'admin@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'operator@example.com', + GroupName: 'AccountOperatorGroup', + }); + + // Verify no UserAccountMapping was created in DynamoDB + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(getResponse.Item).toBeUndefined(); + }); + + it('should throw error when AdminCreateUserCommand fails', async () => { + // ARRANGE + const error = new Error('Cognito create user failed'); + mockCognitoClient.on(AdminCreateUserCommand).rejects(error); + + // ACT & ASSERT + await expect(service.createUser('test@example.com', 'DelegatedAdmin', 'admin@example.com')).rejects.toThrow( + 'Cognito create user failed', + ); + }); + + it('should throw error when AdminAddUserToGroupCommand fails', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'test@example.com' }, + }); + const error = new Error('Cognito add to group failed'); + mockCognitoClient.on(AdminAddUserToGroupCommand).rejects(error); + + // ACT & ASSERT + await expect(service.createUser('test@example.com', 'DelegatedAdmin', 'admin@example.com')).rejects.toThrow( + 'Cognito add to group failed', + ); + }); + + it('should throw error when DynamoDB create fails for AccountOperator', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'operator@example.com' }, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // Mock DynamoDB failure by overriding the repository method + const originalCreate = userAccountMappingRepository.create; + userAccountMappingRepository.create = jest.fn().mockRejectedValue(new Error('DynamoDB error')); + + // ACT & ASSERT + await expect( + service.createUser('operator@example.com', 'AccountOperator', 'admin@example.com', ['123456789012']), + ).rejects.toThrow('DynamoDB error'); + + // Restore original method + userAccountMappingRepository.create = originalCreate; + }); + + it('should handle empty accountIds array for AccountOperator', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'operator@example.com' }, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('operator@example.com', 'AccountOperator', 'admin@example.com', []); + + // ASSERT + + // Verify UserAccountMapping was created with empty accountIds + const getResponse = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(getResponse.Item).toBeDefined(); + expect(getResponse.Item?.accountIds).toEqual([]); + }); + + it('should handle case when AdminCreateUserCommand returns no Username', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: {}, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('test@example.com', 'DelegatedAdmin', 'admin@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'test@example.com', + GroupName: 'DelegatedAdminGroup', + }); + }); + + it('should handle case when AdminCreateUserCommand returns no User', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({}); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('test@example.com', 'DelegatedAdmin', 'admin@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminCreateUserCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'test@example.com', + UserAttributes: [ + { Name: 'email', Value: 'test@example.com' }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + }); + + it('should create correct group mapping for DelegatedAdmin role', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'delegated@example.com' }, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('delegated@example.com', 'DelegatedAdmin', 'admin@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'delegated@example.com', + GroupName: 'DelegatedAdminGroup', + }); + }); + + it('should create correct group mapping for AccountOperator role', async () => { + // ARRANGE + mockCognitoClient.on(AdminCreateUserCommand).resolves({ + User: { Username: 'operator@example.com' }, + }); + mockCognitoClient.on(AdminAddUserToGroupCommand).resolves({}); + + // ACT + await service.createUser('operator@example.com', 'AccountOperator', 'admin@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminAddUserToGroupCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'operator@example.com', + GroupName: 'AccountOperatorGroup', + }); + }); + }); + + describe('updateAccountOperatorUser', () => { + it('should throw NotFoundError when user does not exist', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).rejects(new Error('User not found')); + + // ACT & ASSERT + await expect( + service.updateAccountOperatorUser('nonexistent@example.com', { + type: 'account-operator', + accountIds: ['123456789012'], + }), + ).rejects.toThrow('not found'); + }); + + it('should update account-operator user with new account IDs when mapping exists', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + // Create existing mapping + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: createMockUserAccountMapping({ + userId: 'operator@example.com', + accountIds: ['123456789012'], + }), + }), + ); + + // ACT + await service.updateAccountOperatorUser('operator@example.com', { + type: 'account-operator', + accountIds: ['111111111111', '222222222222'], + }); + + // ASSERT + const result = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(result.Item?.accountIds).toEqual(['111111111111', '222222222222']); + expect(result.Item?.lastModifiedBy).toBe('UsersAPI'); + expect(result.Item?.lastModifiedTimestamp).toBeDefined(); + }); + + it('should create new mapping when updating account-operator user without existing mapping', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'newoperator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + // ACT + await service.updateAccountOperatorUser('newoperator@example.com', { + type: 'account-operator', + accountIds: ['333333333333'], + }); + + // ASSERT + const result = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'newoperator@example.com' }, + }), + ); + expect(result.Item?.userId).toBe('newoperator@example.com'); + expect(result.Item?.accountIds).toEqual(['333333333333']); + expect(result.Item?.invitedBy).toBe('admin@example.com'); + }); + + it('should create mapping with empty array when account-operator user data has no accountIds', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + // ACT + await service.updateAccountOperatorUser('operator@example.com', { type: 'account-operator' }); + + // ASSERT + const result = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(result.Item?.accountIds).toEqual([]); + expect(result.Item?.invitedBy).toBe('admin@example.com'); + }); + it('should handle undefined accountIds by setting empty array', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + // ACT + await service.updateAccountOperatorUser('operator@example.com', {}); + + // ASSERT + const result = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(result.Item?.accountIds).toEqual([]); + expect(result.Item?.invitedBy).toBe('admin@example.com'); + }); + + it('should throw BadRequestError when trying to change user type', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + // ACT & ASSERT + await expect( + service.updateAccountOperatorUser('operator@example.com', { + // @ts-expect-error - testing + type: 'admin', + accountIds: ['123456789012'], + }), + ).rejects.toThrow( + new BadRequestError( + 'Requested user type does not match current type for the user. Modifying the user type is not currently supported.', + ), + ); + }); + + it('should throw BadRequestError when trying to change user status', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + + // ACT & ASSERT + await expect( + service.updateAccountOperatorUser('operator@example.com', { + status: 'Invited', + accountIds: ['123456789012'], + }), + ).rejects.toThrow( + new BadRequestError( + 'Requested user status does not match current status for the user. Modifying the user status is not currently supported.', + ), + ); + }); + }); + + describe('deleteUser', () => { + it('should delete existing admin user successfully', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'admin@example.com' }, + { Name: 'custom:invitedBy', Value: 'super@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + mockCognitoClient.on(AdminDeleteUserCommand).resolves({}); + + // ACT + await service.deleteUser('admin@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminDeleteUserCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'admin@example.com', + }); + }); + + it('should delete account-operator user and remove DynamoDB mapping', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'operator@example.com' }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AccountOperatorGroup' }] }); + mockCognitoClient.on(AdminDeleteUserCommand).resolves({}); + + // Create user account mapping + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: userAccountMappingTableName, + Item: createUserAccountMapping('operator@example.com', ['123456789012']), + }), + ); + + // ACT + await service.deleteUser('operator@example.com'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminDeleteUserCommand, { + UserPoolId: 'us-east-1_testpool', + Username: 'operator@example.com', + }); + + // Verify DynamoDB mapping was deleted + const result = await dynamoDBDocumentClient.send( + new GetCommand({ + TableName: userAccountMappingTableName, + Key: { userId: 'operator@example.com' }, + }), + ); + expect(result.Item).toBeUndefined(); + }); + + it('should throw NotFoundError when user does not exist', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).rejects(new Error('User not found')); + + // ACT & ASSERT + await expect(service.deleteUser('nonexistent@example.com')).rejects.toThrow(NotFoundError); + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminDeleteUserCommand); + }); + + it('should invalidate cache after successful deletion', async () => { + // ARRANGE + const userId = 'cache-test@example.com'; + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + mockCognitoClient.on(AdminDeleteUserCommand).resolves({}); + + // Cache the user first + await service.getUserById(userId); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminGetUserCommand, 1); + + // ACT + await service.deleteUser(userId); + + // ASSERT - Next call should hit Cognito again (cache invalidated) + await service.getUserById(userId); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminGetUserCommand, 2); + }); + + it('should handle Cognito delete failure', async () => { + // ARRANGE + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: 'admin@example.com' }, + { Name: 'custom:invitedBy', Value: 'super@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + mockCognitoClient.on(AdminDeleteUserCommand).rejects(new Error('Cognito delete failed')); + + // ACT & ASSERT + await expect(service.deleteUser('admin@example.com')).rejects.toThrow('Cognito delete failed'); + }); + }); + + describe('caching', () => { + it('should return cached user on subsequent calls', async () => { + // ARRANGE + const userId = 'cached-user@example.com'; + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + UserCreateDate: new Date('2023-01-01'), + UserStatus: 'CONFIRMED', + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'AdminGroup' }] }); + + // ACT + const result1 = await service.getUserById(userId); + const result2 = await service.getUserById(userId); + + // ASSERT + expect(result1).toEqual(result2); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminGetUserCommand, 1); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminListGroupsForUserCommand, 1); + }); + + it('should cache null results', async () => { + // ARRANGE + const userId = 'nonexistent@example.com'; + mockCognitoClient.on(AdminGetUserCommand).rejects(new Error('User not found')); + + // ACT + const result1 = await service.getUserById(userId); + const result2 = await service.getUserById(userId); + + // ASSERT + expect(result1).toBeNull(); + expect(result2).toBeNull(); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminGetUserCommand, 1); + }); + + it('should cache null when email missing', async () => { + // ARRANGE + const userId = 'no-email@example.com'; + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [{ Name: 'custom:invitedBy', Value: 'admin@example.com' }], + }); + + // ACT + const result1 = await service.getUserById(userId); + const result2 = await service.getUserById(userId); + + // ASSERT + expect(result1).toBeNull(); + expect(result2).toBeNull(); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminGetUserCommand, 1); + }); + + it('should cache null when no recognized groups', async () => { + // ARRANGE + const userId = 'no-groups@example.com'; + mockCognitoClient.on(AdminGetUserCommand).resolves({ + UserAttributes: [ + { Name: 'email', Value: userId }, + { Name: 'custom:invitedBy', Value: 'admin@example.com' }, + ], + }); + mockCognitoClient.on(AdminListGroupsForUserCommand).resolves({ Groups: [{ GroupName: 'UnknownGroup' }] }); + + // ACT + const result1 = await service.getUserById(userId); + const result2 = await service.getUserById(userId); + + // ASSERT + expect(result1).toBeNull(); + expect(result2).toBeNull(); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminGetUserCommand, 1); + expect(mockCognitoClient).toHaveReceivedCommandTimes(AdminListGroupsForUserCommand, 1); + }); + }); + + describe('getProviderEmailAttributeName', () => { + it('should return email attribute name when provider has attribute mapping', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({ + IdentityProvider: { + AttributeMapping: { + email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', + }, + }, + }); + + // ACT + const result = await service.getProviderEmailAttributeName('TestProvider'); + + // ASSERT + expect(result).toBe('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'); + expect(mockCognitoClient).toHaveReceivedCommandWith(DescribeIdentityProviderCommand, { + UserPoolId: 'us-east-1_testpool', + ProviderName: 'TestProvider', + }); + }); + + it('should throw error when provider has no attribute mapping object', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({ + IdentityProvider: {}, + }); + + // ACT & ASSERT + await expect(service.getProviderEmailAttributeName('TestProvider')).rejects.toThrow( + 'Could not find attribute mapping for provider TestProvider', + ); + }); + + it('should throw error when IdentityProvider is undefined', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({}); + + // ACT & ASSERT + await expect(service.getProviderEmailAttributeName('TestProvider')).rejects.toThrow( + 'Could not find attribute mapping for provider TestProvider', + ); + }); + + it('should throw error when email attribute mapping is missing', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({ + IdentityProvider: { + AttributeMapping: { + name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name', + }, + }, + }); + + // ACT & ASSERT + await expect(service.getProviderEmailAttributeName('TestProvider')).rejects.toThrow( + 'Could not find email attribute mapping for provider TestProvider. Ensure you have configured an email attribute mapping for this provider.', + ); + }); + + it('should throw error when Cognito command fails', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).rejects(new Error('Provider not found')); + + // ACT & ASSERT + await expect(service.getProviderEmailAttributeName('NonExistentProvider')).rejects.toThrow('Provider not found'); + }); + }); + + describe('linkFederatedUser', () => { + it('should successfully link federated user', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({ + IdentityProvider: { + AttributeMapping: { + email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', + }, + }, + }); + mockCognitoClient.on(AdminLinkProviderForUserCommand).resolves({}); + + // ACT + await service.linkFederatedUser('user@example.com', 'SAML'); + + // ASSERT + expect(mockCognitoClient).toHaveReceivedCommandWith(AdminLinkProviderForUserCommand, { + UserPoolId: 'us-east-1_testpool', + DestinationUser: { + ProviderName: 'Cognito', + ProviderAttributeValue: 'user@example.com', + }, + SourceUser: { + ProviderName: 'SAML', + ProviderAttributeName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress', + ProviderAttributeValue: 'user@example.com', + }, + }); + }); + + it('should throw error when getProviderEmailAttributeName fails', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).rejects(new Error('Provider not found')); + + // ACT & ASSERT + await expect(service.linkFederatedUser('user@example.com', 'InvalidProvider')).rejects.toThrow( + 'Provider not found', + ); + expect(mockCognitoClient).not.toHaveReceivedCommand(AdminLinkProviderForUserCommand); + }); + + it('should throw error when AdminLinkProviderForUserCommand fails', async () => { + // ARRANGE + mockCognitoClient.on(DescribeIdentityProviderCommand).resolves({ + IdentityProvider: { + AttributeMapping: { + email: 'email', + }, + }, + }); + mockCognitoClient.on(AdminLinkProviderForUserCommand).rejects(new Error('Link failed')); + + // ACT & ASSERT + await expect(service.linkFederatedUser('user@example.com', 'SAML')).rejects.toThrow('Link failed'); + }); + }); + + describe('createUser - UsernameExistsException handling', () => { + it('should throw BadRequestError when user already exists', async () => { + // ARRANGE + const usernameExistsError = new UsernameExistsException({ + message: 'An account with the given email already exists.', + $metadata: {}, + }); + mockCognitoClient.on(AdminCreateUserCommand).rejects(usernameExistsError); + + // ACT & ASSERT + await expect(service.createUser('existing@example.com', 'DelegatedAdmin', 'admin@example.com')).rejects.toThrow( + new BadRequestError('User with username existing@example.com already exists.'), + ); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/services/findingsService.test.ts b/source/lambdas/api/__tests__/services/findingsService.test.ts new file mode 100644 index 00000000..59ead618 --- /dev/null +++ b/source/lambdas/api/__tests__/services/findingsService.test.ts @@ -0,0 +1,169 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import nock from 'nock'; +import { + cleanupMetricsMocks, + createMetricsTestScope, + setupMetricsMocks, +} from '../../../common/__tests__/metricsMockSetup'; +import { FindingRepository } from '../../../common/repositories/findingRepository'; +import { AuthenticatedUser } from '../../services/authorization'; +import { FindingsService } from '../../services/findingsService'; + +// Mock the repository +jest.mock('../../../common/repositories/findingRepository'); +jest.mock('../../../common/utils/dynamodb'); + +describe('FindingsService', () => { + let findingsService: FindingsService; + let mockRepository: jest.Mocked; + let mockLogger: Logger; + let mockAuthenticatedUser: AuthenticatedUser; + + beforeEach(() => { + setupMetricsMocks(); + + process.env.FINDINGS_TABLE_NAME = 'testFindingsTable'; + + mockLogger = new Logger({ serviceName: 'test' }); + jest.spyOn(mockLogger, 'error').mockImplementation(); + + findingsService = new FindingsService(mockLogger); + + mockRepository = (findingsService as any).findingRepository as jest.Mocked; + + mockAuthenticatedUser = { + username: 'test-user', + groups: ['AdminGroup'], + authorizedAccounts: undefined, + email: 'test-user@example.com', + }; + }); + + afterEach(async () => { + jest.clearAllMocks(); + delete process.env.FINDINGS_TABLE_NAME; + cleanupMetricsMocks(); + + // Allow the async metrics api call to be invoked + await new Promise((resolve) => setTimeout(resolve, 5)); + }); + + describe('searchFindings', () => { + it('should handle Error exceptions in searchFindings and log them correctly', async () => { + // Arrange + const request = { + NextToken: 'a'.repeat(50), + }; + const errorException = new Error('Database connection failed'); + errorException.stack = 'Error stack trace'; + + mockRepository.searchFindings.mockRejectedValue(errorException); + + await expect(findingsService.searchFindings(mockAuthenticatedUser, request)).rejects.toThrow( + 'Database connection failed', + ); + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error searching findings', + expect.objectContaining({ + request: { + NextToken: 'a'.repeat(20) + '...', + }, + error: 'Database connection failed', + stack: 'Error stack trace', + }), + ); + }); + + it('should handle non-Error exceptions in searchFindings and log them correctly', async () => { + const request = { + NextToken: 'short-token', + }; + const nonErrorException = 'String error message'; + + mockRepository.searchFindings.mockRejectedValue(nonErrorException); + + await expect(findingsService.searchFindings(mockAuthenticatedUser, request)).rejects.toBe('String error message'); + + // Verify that the error logging handles non-Error exceptions correctly + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error searching findings', + expect.objectContaining({ + request: { + NextToken: 'short-token...', + }, + error: 'String error message', + stack: undefined, + }), + ); + }); + + it('should handle request without NextToken in error logging', async () => { + const request = {}; + const errorException = new Error('Repository error'); + + mockRepository.searchFindings.mockRejectedValue(errorException); + + await expect(findingsService.searchFindings(mockAuthenticatedUser, request)).rejects.toThrow('Repository error'); + + // Verify that the error logging handles requests without NextToken + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error searching findings', + expect.objectContaining({ + request: { + NextToken: undefined, + }, + error: 'Repository error', + stack: expect.any(String), + }), + ); + }); + + it('should publish search metrics when searching findings', async () => { + // ARRANGE + const request = { + Filters: { + StringFilters: [ + { + FieldName: 'Severity.Label', + Filter: { Value: 'HIGH', Comparison: 'EQUALS' as const }, + }, + ], + CompositeFilters: [ + { + Operator: 'AND' as const, + StringFilters: [ + { + FieldName: 'ComplianceStatus', + Filter: { Value: 'FAILED', Comparison: 'EQUALS' as const }, + }, + ], + }, + ], + }, + SortCriteria: [{ Field: 'UpdatedAt', SortOrder: 'desc' as const }], + }; + + // Setup separate mock for non-Search metrics API calls + nock('https://metrics.awssolutionsbuilder.com').post('/generic').reply(200).persist(); + const metricsScope = createMetricsTestScope( + /.*search_operation.*filter_types_used.*Severity\.Label.*ComplianceStatus.*filter_count.*%3A2.*has_composite_filters.*true.*sort_fields_used.*UpdatedAt.*resource_type.*Findings.*/, + ); + metricsScope.persist(); + + mockRepository.searchFindings.mockResolvedValue({ items: [], nextToken: undefined }); + + // ACT + await findingsService.searchFindings(mockAuthenticatedUser, request); + + // Allow the async metrics api call to be invoked + await new Promise((resolve) => setTimeout(resolve, 5)); + + // ASSERT + expect(metricsScope.isDone()).toBe(true); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/services/remediationService.test.ts b/source/lambdas/api/__tests__/services/remediationService.test.ts new file mode 100644 index 00000000..1226dc5f --- /dev/null +++ b/source/lambdas/api/__tests__/services/remediationService.test.ts @@ -0,0 +1,568 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; +import { RemediationService } from '../../services/remediationService'; +import { AuthenticatedUser } from '../../services/authorization'; +import { RemediationsRequest } from '@asr/data-models'; +import { DynamoDBTestSetup } from '../../../common/__tests__/dynamodbSetup'; +import { findingsTableName } from '../../../common/__tests__/envSetup'; +import { + setupMetricsMocks, + cleanupMetricsMocks, + createMetricsTestScope, +} from '../../../common/__tests__/metricsMockSetup'; + +describe('RemediationService', () => { + let remediationService: RemediationService; + let mockLogger: Logger; + let mockAuthenticatedUser: AuthenticatedUser; + let dynamoDBDocumentClient: DynamoDBDocumentClient; + const remediationHistoryTableName = 'test-remediation-history-table'; + + beforeAll(async () => { + await DynamoDBTestSetup.initialize(); + dynamoDBDocumentClient = DynamoDBTestSetup.getDocClient(); + await DynamoDBTestSetup.createFindingsTable(findingsTableName); + await DynamoDBTestSetup.createRemediationHistoryTable(remediationHistoryTableName); + }); + + afterAll(async () => { + await DynamoDBTestSetup.deleteTable(findingsTableName); + await DynamoDBTestSetup.deleteTable(remediationHistoryTableName); + }); + + beforeEach(async () => { + await DynamoDBTestSetup.clearTable(findingsTableName, 'findings'); + await DynamoDBTestSetup.clearTable(remediationHistoryTableName, 'remediationHistory'); + setupMetricsMocks(); + + process.env.REMEDIATION_HISTORY_TABLE_NAME = remediationHistoryTableName; + process.env.FINDINGS_TABLE_NAME = findingsTableName; + + mockLogger = new Logger({ serviceName: 'test' }); + jest.spyOn(mockLogger, 'error').mockImplementation(); + jest.spyOn(mockLogger, 'info').mockImplementation(); + jest.spyOn(mockLogger, 'debug').mockImplementation(); + + remediationService = new RemediationService(mockLogger); + + mockAuthenticatedUser = { + username: 'test-user@example.com', + email: 'test-user@example.com', + groups: ['AdminGroup'], + }; + }); + + afterEach(async () => { + jest.clearAllMocks(); + cleanupMetricsMocks(); + delete process.env.REMEDIATION_HISTORY_TABLE_NAME; + delete process.env.FINDINGS_TABLE_NAME; + + // Allow the async metrics api call to be invoked + await new Promise((resolve) => setTimeout(resolve, 5)); + }); + + describe('searchRemediations', () => { + it('should successfully search remediations and return results', async () => { + const remediationItem = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test#arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-01T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'test-user@example.com', + executionId: 'arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, // 90 days from now + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: remediationHistoryTableName, + Item: remediationItem, + }), + ); + + const request: RemediationsRequest = { + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc', + }, + ], + }; + + const result = await remediationService.searchRemediations(mockAuthenticatedUser, request); + + expect(result).toEqual({ + Remediations: [ + { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + lastUpdatedBy: 'test-user@example.com', + executionId: 'arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + consoleLink: + 'https://us-east-1.console.aws.amazon.com/states/home?region=us-east-1#/v2/executions/details/arn%3Aaws%3Astates%3Aus-east-1%3A123456789012%3Aexecution%3ATestStateMachine%3Aexec-123', + }, + ], + NextToken: undefined, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith('Searching remediations with request', { + remediationsRequest: request, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith('Remediation search completed successfully', { + remediationsCount: 1, + hasNextToken: false, + }); + }); + + it('should apply account filtering for AccountOperator users', async () => { + const remediation1 = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-1', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-1#exec-1', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test-1', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-01T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test-1', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'account-operator@example.com', + executionId: 'exec-1', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + const remediation2 = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:987654321098:security-control/Lambda.3/finding/test-2', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:987654321098:security-control/Lambda.3/finding/test-2#exec-2', + accountId: '987654321098', + resourceId: 'arn:aws:lambda:us-east-1:987654321098:function:test-2', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'MEDIUM', + region: 'us-east-1', + remediationStatus: 'FAILED', + lastUpdatedTime: '2023-01-02T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-02T00:00:00Z#arn:aws:securityhub:us-east-1:987654321098:security-control/Lambda.3/finding/test-2', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'account-operator@example.com', + executionId: 'exec-2', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + const remediation3 = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:111111111111:security-control/Lambda.3/finding/test-3', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:111111111111:security-control/Lambda.3/finding/test-3#exec-3', + accountId: '111111111111', // This account should be filtered out + resourceId: 'arn:aws:lambda:us-east-1:111111111111:function:test-3', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'LOW', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-03T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-03T00:00:00Z#arn:aws:securityhub:us-east-1:111111111111:security-control/Lambda.3/finding/test-3', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'account-operator@example.com', + executionId: 'exec-3', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + await Promise.all([ + dynamoDBDocumentClient.send(new PutCommand({ TableName: remediationHistoryTableName, Item: remediation1 })), + dynamoDBDocumentClient.send(new PutCommand({ TableName: remediationHistoryTableName, Item: remediation2 })), + dynamoDBDocumentClient.send(new PutCommand({ TableName: remediationHistoryTableName, Item: remediation3 })), + ]); + + const accountOperatorUser: AuthenticatedUser = { + username: 'account-operator@example.com', + email: 'account-operator@example.com', + groups: ['AccountOperatorGroup'], + authorizedAccounts: ['123456789012', '987654321098'], + }; + + const request: RemediationsRequest = { + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc', + }, + ], + }; + + const result = await remediationService.searchRemediations(accountOperatorUser, request); + + expect(result.Remediations).toHaveLength(2); + expect(result.Remediations.map((r) => r.accountId)).toEqual( + expect.arrayContaining(['123456789012', '987654321098']), + ); + expect(result.Remediations.map((r) => r.accountId)).not.toContain('111111111111'); + }); + + it('should handle filters in the request', async () => { + const successHighRemediation = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/success-high', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/success-high#exec-1', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:success-high', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-01T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/success-high', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'test-user@example.com', + executionId: 'exec-1', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + const failedHighRemediation = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/failed-high', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/failed-high#exec-2', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:failed-high', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'FAILED', + lastUpdatedTime: '2023-01-02T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-02T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/failed-high', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'test-user@example.com', + executionId: 'exec-2', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + const successMediumRemediation = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/success-medium', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/success-medium#exec-3', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:success-medium', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'MEDIUM', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-03T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-03T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/success-medium', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'test-user@example.com', + executionId: 'exec-3', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + }; + + await Promise.all([ + dynamoDBDocumentClient.send( + new PutCommand({ TableName: remediationHistoryTableName, Item: successHighRemediation }), + ), + dynamoDBDocumentClient.send( + new PutCommand({ TableName: remediationHistoryTableName, Item: failedHighRemediation }), + ), + dynamoDBDocumentClient.send( + new PutCommand({ TableName: remediationHistoryTableName, Item: successMediumRemediation }), + ), + ]); + + const request: RemediationsRequest = { + Filters: { + CompositeFilters: [ + { + Operator: 'AND', + StringFilters: [ + { + FieldName: 'remediationStatus', + Filter: { + Value: 'SUCCESS', + Comparison: 'EQUALS', + }, + }, + { + FieldName: 'severity', + Filter: { + Value: 'HIGH', + Comparison: 'EQUALS', + }, + }, + ], + }, + ], + }, + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'asc', + }, + ], + }; + + const result = await remediationService.searchRemediations(mockAuthenticatedUser, request); + + expect(result.Remediations).toHaveLength(1); + expect(result.Remediations[0].remediationStatus).toBe('SUCCESS'); + expect(result.Remediations[0].severity).toBe('HIGH'); + expect(result.Remediations[0].findingId).toContain('success-high'); + }); + + it('should return empty results when no data exists', async () => { + const request: RemediationsRequest = { + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc', + }, + ], + }; + + const result = await remediationService.searchRemediations(mockAuthenticatedUser, request); + + expect(result).toEqual({ + Remediations: [], + NextToken: undefined, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith('Searching remediations with request', { + remediationsRequest: request, + }); + + expect(mockLogger.debug).toHaveBeenCalledWith('Remediation search completed successfully', { + remediationsCount: 0, + hasNextToken: false, + }); + }); + + it('should remove internal fields from API response', async () => { + const remediationItem = { + findingType: 'security-control/Lambda.3', + findingId: 'test-finding-id', + 'findingId#executionId': 'internal-composite-key', + 'lastUpdatedTime#findingId': 'internal-lsi-key', + REMEDIATION_CONSTANT: 'remediation', + expireAt: 1672531200, + accountId: '123456789012', + resourceId: 'test-resource', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + lastUpdatedBy: 'test-user@example.com', + executionId: 'arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: remediationHistoryTableName, + Item: remediationItem, + }), + ); + + const request: RemediationsRequest = {}; + + const result = await remediationService.searchRemediations(mockAuthenticatedUser, request); + + expect(result.Remediations[0]).toEqual({ + findingType: 'security-control/Lambda.3', + findingId: 'test-finding-id', + accountId: '123456789012', + resourceId: 'test-resource', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + lastUpdatedBy: 'test-user@example.com', + executionId: 'arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + consoleLink: + 'https://us-east-1.console.aws.amazon.com/states/home?region=us-east-1#/v2/executions/details/arn%3Aaws%3Astates%3Aus-east-1%3A123456789012%3Aexecution%3ATestStateMachine%3Aexec-123', + }); + + expect(result.Remediations[0]).not.toHaveProperty('findingId#executionId'); + expect(result.Remediations[0]).not.toHaveProperty('lastUpdatedTime#findingId'); + expect(result.Remediations[0]).not.toHaveProperty('REMEDIATION_CONSTANT'); + expect(result.Remediations[0]).not.toHaveProperty('expireAt'); + }); + + it('should publish search metrics when searching remediations', async () => { + // ARRANGE + const request: RemediationsRequest = { + Filters: { + StringFilters: [ + { + FieldName: 'remediationStatus', + Filter: { Value: 'SUCCESS', Comparison: 'EQUALS' as const }, + }, + ], + CompositeFilters: [ + { + Operator: 'AND' as const, + StringFilters: [ + { + FieldName: 'severity', + Filter: { Value: 'HIGH', Comparison: 'EQUALS' as const }, + }, + ], + }, + ], + }, + SortCriteria: [{ Field: 'lastUpdatedTime', SortOrder: 'desc' as const }], + }; + + const metricsScope = createMetricsTestScope( + /.*search_operation.*filter_types_used.*remediationStatus.*severity.*filter_count.*%3A2.*has_composite_filters.*true.*sort_fields_used.*lastUpdatedTime.*resource_type.*Remediations.*/, + ); + metricsScope.persist(); + + // ACT + await remediationService.searchRemediations(mockAuthenticatedUser, request); + + // Allow the async metrics api call to be invoked + await new Promise((resolve) => setTimeout(resolve, 5)); + + // ASSERT + expect(metricsScope.isDone()).toBe(true); + }); + }); + + describe('exportRemediationHistory', () => { + beforeEach(() => { + process.env.CSV_EXPORT_BUCKET_NAME = 'test-export-bucket'; + + jest + .spyOn(remediationService['s3Client'], 'uploadCsvAndGeneratePresignedUrl') + .mockResolvedValue('https://test-bucket.s3.amazonaws.com/test-file.csv?presigned=true'); + }); + + afterEach(() => { + delete process.env.CSV_EXPORT_BUCKET_NAME; + jest.restoreAllMocks(); + }); + + it('should generate CSV with user-friendly headers', async () => { + const remediationItem = { + findingType: 'security-control/Lambda.3', + findingId: 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test', + 'findingId#executionId': + 'arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test#arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + accountId: '123456789012', + resourceId: 'arn:aws:lambda:us-east-1:123456789012:function:test', + resourceType: 'AWS::Lambda::Function', + resourceTypeNormalized: 'awslambdafunction', + severity: 'HIGH', + region: 'us-east-1', + remediationStatus: 'SUCCESS', + lastUpdatedTime: '2023-01-01T00:00:00Z', + 'lastUpdatedTime#findingId': + '2023-01-01T00:00:00Z#arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test', + REMEDIATION_CONSTANT: 'remediation', + lastUpdatedBy: 'test-user@example.com', + executionId: 'arn:aws:states:us-east-1:123456789012:execution:TestStateMachine:exec-123', + expireAt: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60, + error: 'Test error message', + }; + + await dynamoDBDocumentClient.send( + new PutCommand({ + TableName: remediationHistoryTableName, + Item: remediationItem, + }), + ); + + const exportRequest = { + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc' as const, + }, + ], + }; + + const result = await remediationService.exportRemediationHistory(mockAuthenticatedUser, exportRequest); + + expect(result.downloadUrl).toBe('https://test-bucket.s3.amazonaws.com/test-file.csv?presigned=true'); + + const uploadCall = jest.mocked(remediationService['s3Client'].uploadCsvAndGeneratePresignedUrl).mock.calls[0]; + const csvContent = uploadCall[2]; + + const expectedHeaders = + 'Finding ID,Account,Resource ID,Resource Type,Finding Type,Severity,Region,Status,Execution Timestamp,Executed By,Execution ID,Error'; + expect(csvContent).toContain(expectedHeaders); + + const lines = csvContent.split('\n'); + expect(lines[0]).toBe(expectedHeaders); + expect(lines[1]).toContain('arn:aws:securityhub:us-east-1:123456789012:security-control/Lambda.3/finding/test'); + expect(lines[1]).toContain('123456789012'); + expect(lines[1]).toContain('SUCCESS'); + expect(lines[1]).toContain('Test error message'); + }); + + it('should generate CSV with headers only when no data exists', async () => { + const exportRequest = { + SortCriteria: [ + { + Field: 'lastUpdatedTime', + SortOrder: 'desc' as const, + }, + ], + }; + + const result = await remediationService.exportRemediationHistory(mockAuthenticatedUser, exportRequest); + + expect(result.downloadUrl).toBe('https://test-bucket.s3.amazonaws.com/test-file.csv?presigned=true'); + + const uploadCall = jest.mocked(remediationService['s3Client'].uploadCsvAndGeneratePresignedUrl).mock.calls[0]; + const csvContent = uploadCall[2]; + + const expectedHeaders = + 'Finding ID,Account,Resource ID,Resource Type,Finding Type,Severity,Region,Status,Execution Timestamp,Executed By,Execution ID,Error'; + expect(csvContent).toBe(expectedHeaders); + }); + }); +}); diff --git a/source/lambdas/api/__tests__/utils.ts b/source/lambdas/api/__tests__/utils.ts new file mode 100644 index 00000000..32e07ebc --- /dev/null +++ b/source/lambdas/api/__tests__/utils.ts @@ -0,0 +1,134 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import { FindingTableItem, ASFFFinding } from '@asr/data-models'; +import { deflate } from 'pako'; + +export const TEST_REQUEST_CONTEXT = { + accountId: '123456789012', + apiId: 'test-api', + authorizer: { + claims: {}, + }, + httpMethod: 'GET', + identity: { + accessKey: null, + accountId: null, + apiKey: null, + apiKeyId: null, + caller: null, + clientCert: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: '127.0.0.1', + user: null, + userAgent: 'test-agent', + userArn: null, + }, + path: '/users', + protocol: 'HTTP/1.1', + requestId: 'test-request-id', + requestTime: '01/Jan/2023:00:00:00 +0000', + requestTimeEpoch: 1672531200, + resourceId: 'test-resource', + resourcePath: '/users', + stage: 'test', +}; + +export const createMockEvent = (overrides: Partial = {}): APIGatewayProxyEvent => ({ + body: null, + headers: {}, + multiValueHeaders: {}, + httpMethod: 'GET', + isBase64Encoded: false, + path: '/users', + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: TEST_REQUEST_CONTEXT, + resource: '/users', + ...overrides, +}); + +export const createMockContext = (): Context => ({ + callbackWaitsForEmptyEventLoop: false, + functionName: 'test-function', + functionVersion: '1', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function', + memoryLimitInMB: '128', + awsRequestId: 'test-request-id', + logGroupName: '/aws/lambda/test-function', + logStreamName: '2023/01/01/[$LATEST]test-stream', + getRemainingTimeInMillis: () => 30000, + done: () => {}, + fail: () => {}, + succeed: () => {}, +}); + +export const createMockFinding = (overrides: Partial = {}): FindingTableItem => { + const defaultFinding = { + findingType: 'security-control/Lambda.3', + findingId: 'test-finding-id', + findingDescription: 'Test finding description', + accountId: '123456789012', + resourceId: 'arn:aws:s3:::test-bucket', + resourceType: 'AWS::S3::Bucket', + resourceTypeNormalized: 'awss3bucket', + severity: 'HIGH', + severityNormalized: 3, + region: 'us-east-1', + remediationStatus: 'NOT_STARTED' as const, + securityHubUpdatedAtTime: '2023-01-01T00:00:00Z', + lastUpdatedTime: '2023-01-01T00:00:00Z', + 'securityHubUpdatedAtTime#findingId': '2023-01-01T00:00:00Z#test-finding-id', + 'severityNormalized#securityHubUpdatedAtTime#findingId': '3#2023-01-01T00:00:00Z#test-finding-id', + findingIdControl: 'Lambda.3', + FINDING_CONSTANT: 'finding' as const, + suppressed: false, + creationTime: '2023-01-01T00:00:00Z', + expireAt: Math.floor(Date.now() / 1000) + 8 * 24 * 60 * 60, // 8 days from now + ...overrides, + }; + + // Create a valid ASFF finding structure + const asffFinding: ASFFFinding = { + SchemaVersion: '2018-10-08', + Id: defaultFinding.findingId, + ProductArn: 'arn:aws:securityhub:us-east-1::product/aws/securityhub', + GeneratorId: 'security-control', + AwsAccountId: defaultFinding.accountId, + Types: ['Sensitive Data Identifications/PII'], + CreatedAt: defaultFinding.creationTime, + UpdatedAt: defaultFinding.lastUpdatedTime, + Severity: { + Label: defaultFinding.severity as 'HIGH' | 'MEDIUM' | 'LOW' | 'CRITICAL' | 'INFORMATIONAL', + }, + Title: 'Test Security Finding', + Description: defaultFinding.findingDescription, + Resources: [ + { + Type: defaultFinding.resourceType || 'AWS::S3::Bucket', + Id: defaultFinding.resourceId, + Region: defaultFinding.region, + }, + ], + Compliance: { + Status: 'FAILED', + SecurityControlId: defaultFinding.findingIdControl || 'Lambda.3', + }, + Region: defaultFinding.region, + }; + + // Compress the ASFF finding JSON + const compressedFindingJSON = deflate(JSON.stringify(asffFinding)); + + return { + ...defaultFinding, + findingJSON: compressedFindingJSON, + }; +}; diff --git a/source/lambdas/api/clients/ASRS3Client.ts b/source/lambdas/api/clients/ASRS3Client.ts new file mode 100644 index 00000000..397ab0db --- /dev/null +++ b/source/lambdas/api/clients/ASRS3Client.ts @@ -0,0 +1,140 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CopyObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { MAX_PRESIGNED_URL_EXPIRY_SECONDS } from '../../common/constants/apiConstant'; + +export class ASRS3Client { + private s3Client: S3Client; + private logger: Logger; + + constructor(region?: string) { + this.logger = new Logger({ + serviceName: 'S3', + logLevel: (process.env.LOG_LEVEL as any) || 'INFO', + }); + + const currentRegion = region || process.env.AWS_REGION || 'us-east-1'; + + this.s3Client = new S3Client({ + region: currentRegion, + }); + } + + async readJsonFile(bucketName: string, fileQualifiedName: string): Promise> { + const command = new GetObjectCommand({ + Bucket: bucketName, + Key: fileQualifiedName, + }); + + try { + const response = await this.s3Client.send(command); + + if (!response.Body) { + throw new Error('No body in S3 response'); + } + + const bodyContents = await response.Body.transformToString(); + return JSON.parse(bodyContents); + } catch (error) { + this.logger.error(`Failed to read JSON file ${fileQualifiedName} from bucket ${bucketName}:`, { error }); + throw error; + } + } + + async copyFile( + sourceBucketName: string, + targetBucketName: string, + sourcePrefix: string, + targetPrefix: string, + fileName: string, + ): Promise { + const sourceKey = sourcePrefix + fileName; + const targetKey = targetPrefix + fileName; + + this.logger.debug(`Copy ${sourceKey} from ${sourceBucketName}`); + + const command = new CopyObjectCommand({ + CopySource: `${sourceBucketName}/${sourceKey}`, + Bucket: targetBucketName, + Key: targetKey, + }); + + try { + await this.s3Client.send(command); + this.logger.debug(`Copied ${targetKey} to ${targetBucketName}`); + } catch (error: any) { + this.logger.error(`Failed to copy key ${sourceKey}`); + + if (error.name === 'AccessDenied') { + this.logger.error( + 'Access denied, make sure (1) the key exists in source bucket, (2) this lambda function ' + + 'has s3:read permissions to the source bucket and (3) s3:put permissions to the target bucket', + ); + } + + this.logger.error('Copy error:', { error }); + throw error; + } + } + + async writeJsonAsFile(bucketName: string, qualifiedFileName: string, jsonObject: Record): Promise { + const command = new PutObjectCommand({ + Bucket: bucketName, + Key: qualifiedFileName, + Body: JSON.stringify(jsonObject), + ContentType: 'application/json', + Metadata: { + 'Content-Type': 'application/json', + }, + }); + + try { + await this.s3Client.send(command); + this.logger.debug(`Successfully wrote JSON file ${qualifiedFileName} to bucket ${bucketName}`); + } catch (error) { + this.logger.error(`Failed to write JSON file ${qualifiedFileName} to bucket ${bucketName}:`, { error }); + throw error; + } + } + + async uploadCsvAndGeneratePresignedUrl(bucketName: string, fileName: string, csvContent: string): Promise { + const putCommand = new PutObjectCommand({ + Bucket: bucketName, + Key: fileName, + Body: csvContent, + ContentType: 'text/csv', + ContentDisposition: `attachment; filename="${fileName}"`, + }); + + try { + await this.s3Client.send(putCommand); + this.logger.debug(`Successfully uploaded CSV file ${fileName} to bucket ${bucketName}`); + + const getCommand = new GetObjectCommand({ + Bucket: bucketName, + Key: fileName, + }); + + const ttlDays = Number(process.env.PRESIGNED_URL_TTL_DAYS) || 1; + const expiresInSeconds = Math.min(ttlDays * 24 * 60 * 60, MAX_PRESIGNED_URL_EXPIRY_SECONDS); + const presignedUrl = await getSignedUrl(this.s3Client, getCommand, { + expiresIn: expiresInSeconds, + }); + + this.logger.debug('Successfully generated pre-signed URL', { + fileName, + bucketName, + expiresInSeconds, + expiresInDays: ttlDays, + }); + + return presignedUrl; + } catch (error) { + this.logger.error(`Failed to upload CSV file ${fileName} to bucket ${bucketName}:`, { error }); + throw error; + } + } +} diff --git a/source/lambdas/api/handlers/apiHandler.ts b/source/lambdas/api/handlers/apiHandler.ts new file mode 100644 index 00000000..3c3f6501 --- /dev/null +++ b/source/lambdas/api/handlers/apiHandler.ts @@ -0,0 +1,190 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { dynamicImport } from 'tsimportlib'; +import { BadRequestError, HttpError, NotFoundError, UnauthorizedError } from '../../common/utils/httpErrors'; +import { executeFindingAction, searchFindings } from './findings'; +import { exportRemediations, searchRemediations } from './remediations'; +import { deleteUser, getUsers, inviteUser, putUser } from './users'; + +const logger = new Logger({ serviceName: 'ApiRouter' }); + +type ErrorWithStatusCode = Error & { statusCode?: number }; +const ALLOWED_ORIGINS = [process.env.WEB_UI_URL!, 'http://localhost:3000'].filter(Boolean); + +const BASE_CORS_HEADERS = { + 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', +} as const; + +export const API_HEADERS = { + FINDINGS: { + ...BASE_CORS_HEADERS, + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', + }, + REMEDIATIONS: { + ...BASE_CORS_HEADERS, + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', + }, + USERS: { + ...BASE_CORS_HEADERS, + 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', + }, +} as const; + +export function createResponse(statusCode: number, body: any, headers: Record): APIGatewayProxyResult { + return { + statusCode, + headers, + body: JSON.stringify(body), + }; +} + +function createErrorResponse(error: ErrorWithStatusCode, origin: string) { + const isHttpError = error instanceof HttpError; + return createResponse( + isHttpError ? error.statusCode : 400, + { + error: isHttpError ? error.name : 'Error', + message: isHttpError ? error.message : 'An unexpected error occurred.', + }, + { + ...BASE_CORS_HEADERS, + 'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0], + 'Content-Type': 'application/json', + }, + ); +} + +const routes = [ + { + method: 'GET', + path: '/users', + handler: getUsers, + }, + { + method: 'POST', + path: '/users', + handler: inviteUser, + }, + { + method: 'PUT', + path: '/users/{id}', + handler: putUser, + }, + { + method: 'DELETE', + path: '/users/{id}', + handler: deleteUser, + }, + { + method: 'POST', + path: '/findings', + handler: searchFindings, + }, + { + method: 'POST', + path: '/findings/action', + handler: executeFindingAction, + }, + { + method: 'POST', + path: '/remediations', + handler: searchRemediations, + }, + { + method: 'POST', + path: '/export', + handler: exportRemediations, + }, +]; + +export const handler = async (event: APIGatewayProxyEvent, context: Context) => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpHeaderNormalizer } = (await dynamicImport( + '@middy/http-header-normalizer', + module, + )) as typeof import('@middy/http-header-normalizer'); + const { default: httpRouterHandler } = (await dynamicImport( + '@middy/http-router', + module, + )) as typeof import('@middy/http-router'); + const { default: cors } = (await dynamicImport('@middy/http-cors', module)) as typeof import('@middy/http-cors'); + + /** + * middy middleware chain: + * applies custom or prepackaged middlewares to each request and response. + * - applies all applicable middlewares to the request from top to bottom, + * - routes to a handler function determined by httpRouterHandler + * - applies all applicable middlewares to the response from bottom to top + * each middleware is an object that can have a "before" function applied to the request, + * an "after" function applied to the response, and an "onError" function applied to the response. + */ + const middlewareHandler = middy() + .use( + cors({ + headers: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', + origins: ALLOWED_ORIGINS, + }), + ) + .use({ + before: (request) => { + const { event } = request; + + logger.info('Processing API request', { + method: event?.httpMethod, + path: event?.path, + requestId: request.context?.awsRequestId, + userAgent: event.headers['user-agent'], + }); + + const headerKeys = Object.keys(event.headers).map((header) => header.toLowerCase()); + + if (headerKeys.includes('x-amzn-requestid') || headerKeys.includes('x-amz-request-id')) + throw new BadRequestError('X-Amzn-Requestid header is not allowed'); + + const claims = event.requestContext?.authorizer?.claims; + if (!claims) throw new UnauthorizedError('No authorization claims found'); + + const missingClaims = []; + if (!('cognito:groups' in claims)) missingClaims.push('cognito:groups'); + if (!('username' in claims)) missingClaims.push('username'); + + if (missingClaims.length > 0) { + logger.warn(`Missing required claims: ${missingClaims.join(', ')}`); + throw new UnauthorizedError(`Could not read claims.`); + } + }, + onError: (request) => { + const error = request.error as ErrorWithStatusCode; + const origin = request.event.headers.origin; + + logger.error('API request failed', { + method: request.event.httpMethod, + path: request.event.path, + errorName: error.name, + errorMessage: error.message, + statusCode: error.statusCode, + stack: error.stack, + requestId: request.context?.awsRequestId, + userAgent: request.event.headers['user-agent'], + origin: origin, + }); + + return createErrorResponse(error, origin); + }, + }) + .use(httpHeaderNormalizer()) + .handler( + httpRouterHandler({ + // @ts-expect-error - middy httpRouterHandler incorrectly throws a type error for `event` + routes: routes, + notFoundResponse: ({ method, path }) => { + throw new NotFoundError(`Method ${method} with path ${path} not found.`); + }, + }), + ); + + return middlewareHandler(event, context); +}; diff --git a/source/lambdas/api/handlers/baseHandler.ts b/source/lambdas/api/handlers/baseHandler.ts new file mode 100644 index 00000000..8f62994b --- /dev/null +++ b/source/lambdas/api/handlers/baseHandler.ts @@ -0,0 +1,146 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { BadRequestError, ForbiddenError } from '../../common/utils/httpErrors'; +import { AuthenticatedUser, AuthorizationService } from '../services/authorization'; + +/** + * AWS API Gateway Cognito Authorizer Claims structure + */ +export interface CognitoClaims { + username: string; + 'cognito:groups': string | string[]; + email?: string; + sub?: string; + aud?: string; + iss?: string; + exp?: number; + iat?: number; + token_use?: string; + [key: string]: unknown; +} + +export interface AccessValidationContext { + accountIds?: string[]; + resourceIds?: string[]; + [key: string]: unknown; // Allow for additional context data +} + +export interface AccessRule { + requiredGroups: string[]; + validator?: (user: AuthenticatedUser, context?: AccessValidationContext) => void | Promise; +} + +export class BaseHandler { + protected readonly authorizationService: AuthorizationService; + + constructor(protected readonly logger: Logger) { + this.authorizationService = new AuthorizationService(logger); + } + + async validateAccess(claims: CognitoClaims, rules: AccessRule): Promise { + const authenticatedUser = await this.authorizationService.authenticateAndAuthorize(claims, rules.requiredGroups); + + if (rules.validator) { + await rules.validator(authenticatedUser); + } + + return authenticatedUser; + } + + createAccessRules(accountIds: string[]): AccessRule { + return { + requiredGroups: ['AdminGroup', 'DelegatedAdminGroup', 'AccountOperatorGroup'], + validator: async (user) => { + if (user.groups.includes('AdminGroup') || user.groups.includes('DelegatedAdminGroup')) { + return; + } + + if (user.groups.includes('AccountOperatorGroup')) { + if (!user.authorizedAccounts?.length) { + throw new ForbiddenError('No authorized accounts'); + } + + if (accountIds.length > 0) { + const unauthorized = accountIds.filter((id) => !user.authorizedAccounts!.includes(id)); + if (unauthorized.length > 0) { + throw new ForbiddenError('Insufficient permissions'); + } + } + return; + } + throw new ForbiddenError('Insufficient permissions'); + }, + }; + } + + extractAccountIdsFromRequest(request: { + Filters?: { CompositeFilters?: Array<{ StringFilters?: Array<{ FieldName: string; Filter: { Value: string } }> }> }; + }): string[] { + if (!request.Filters?.CompositeFilters) { + return []; + } + + const accountIds = request.Filters.CompositeFilters.flatMap( + (compositeFilter) => compositeFilter.StringFilters || [], + ) + .filter((stringFilter) => stringFilter.FieldName === 'accountId') + .map((stringFilter) => stringFilter.Filter.Value); + + return Array.from(new Set(accountIds)); + } + + extractAccountIdsFromArns(arns: string[]): string[] { + const accountIds: string[] = []; + + for (const arn of arns) { + const arnMatch = arn.match(/^arn:aws:securityhub:[^:]+:(\d{12}):/); + if (arnMatch) { + const accountId = arnMatch[1]; + if (!accountIds.includes(accountId)) { + accountIds.push(accountId); + } + } else { + this.logger.warn('Could not extract account ID from ARN', { arn }); + } + } + + return accountIds; + } + + /** + * Extracts, validates, and returns the typed body from an API Gateway event + * Combines body extraction, schema validation, and error handling in one method + * When using httpJsonBodyParser middleware, the body is already parsed + * @param event - The API Gateway event + * @param schema - The Zod schema to validate against + * @param errorPrefix - Optional prefix for validation error messages + * @returns The validated and typed body + * @throws BadRequestError if validation fails + */ + extractValidatedBody( + event: APIGatewayProxyEvent, + schema: { + safeParse: (data: unknown) => { + success: boolean; + data?: T; + error?: { issues: Array<{ path: (string | number)[]; message: string }> }; + }; + }, + errorPrefix: string = 'Invalid request', + ): T { + const parsedBody = (event.body as unknown) || {}; + const validationResult = schema.safeParse(parsedBody); + + if (!validationResult.success) { + const errorDetails = + validationResult.error?.issues?.map((issue) => `${issue.path.join('.')}: ${issue.message}`)?.join('; ') || + 'Validation failed'; + throw new BadRequestError(`${errorPrefix}: ${errorDetails}`); + } + + return validationResult.data!; + } +} diff --git a/source/lambdas/api/handlers/cfnResponse.ts b/source/lambdas/api/handlers/cfnResponse.ts new file mode 100644 index 00000000..9677bd02 --- /dev/null +++ b/source/lambdas/api/handlers/cfnResponse.ts @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as https from 'https'; +import * as url from 'url'; +import { CloudFormationCustomResourceEvent, Context } from 'aws-lambda'; + +export const SUCCESS = 'SUCCESS'; +export const FAILED = 'FAILED'; + +// copied from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html +// and converted from JS to TS +export function send( + event: CloudFormationCustomResourceEvent, + context: Context, + responseStatus: typeof SUCCESS | typeof FAILED, + responseData: Record, + physicalResourceId?: string, + noEcho?: boolean, +): Promise { + return new Promise((resolve, reject) => { + 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: https.RequestOptions = { + hostname: parsedUrl.hostname, + port: 443, + path: parsedUrl.path, + method: 'PUT', + headers: { + 'content-type': '', + 'content-length': responseBody.length, + }, + }; + + const request = https.request(options, (response) => { + console.log('Status code: ' + response.statusCode); + resolve(context.done()); + }); + + request.on('error', (error) => { + console.log('send(..) failed executing https.request(..): ' + maskCredentialsAndSignature(error.message)); + reject(context.done(error)); + }); + + request.write(responseBody); + request.end(); + }); +} + +function maskCredentialsAndSignature(message: string): string { + return message + .replace(/X-Amz-Credential=[^&\s]+/i, 'X-Amz-Credential=*****') + .replace(/X-Amz-Signature=[^&\s]+/i, 'X-Amz-Signature=*****'); +} diff --git a/source/lambdas/api/handlers/deployWebui.ts b/source/lambdas/api/handlers/deployWebui.ts new file mode 100644 index 00000000..06243ae8 --- /dev/null +++ b/source/lambdas/api/handlers/deployWebui.ts @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CloudFormationCustomResourceEvent, Context } from 'aws-lambda'; +import { ASRS3Client } from '../clients/ASRS3Client'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { FAILED, send, SUCCESS } from './cfnResponse'; + +export interface WebUIConfig { + SrcBucket: string; + SrcPath: string; + WebUIBucket: string; + awsExports: Record; +} + +export interface WebUIManifest { + files: string[]; +} + +const logger = new Logger({ + serviceName: 'DeployWebUI', + logLevel: (process.env.LOG_LEVEL as any) || 'INFO', +}); + +export async function lambdaHandler(event: CloudFormationCustomResourceEvent, context: Context): Promise { + logger.info('Event:', { event }); + + try { + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + const deployer = new WebUIDeployer(); + await deployer.deploy(); + } + logger.info('SUCCESS:', { event }); + await send(event, context, SUCCESS, { Message: 'WebUI successfully deployed' }); + } catch (error) { + logger.error('An error occurred:', { error }); + await send(event, context, FAILED, { Message: 'An error occurred' }); + } +} + +export class WebUIDeployer { + private logger: Logger; + private s3: ASRS3Client; + + constructor() { + this.logger = new Logger({ + serviceName: 'WebUIDeployer', + logLevel: (process.env.LOG_LEVEL as any) || 'INFO', + }); + this.s3 = new ASRS3Client(); + } + + async deploy(): Promise { + /** + * To deploy the solution console: + * - copy all files from source bucket that begin with /solution-name/version/webui to target bucket + * - create aws-exports.json in target bucket + * As opposed to service-type frontend projects, the configuration is not known at build time + * because URLs and IDs only get created at deploy time. + * For that reason, aws-exports.json cannot be included in the build + * but has to be created dynamically at deploy time. + */ + const configString = process.env.CONFIG; + if (!configString) { + throw new Error('CONFIG environment variable is required'); + } + + const config: WebUIConfig = JSON.parse(configString); + this.logger.info('Config:', { config }); + + await this.copyUIFilesToConsoleBucket(config); + await this.createConfigFile(config); + } + + private async createConfigFile(config: WebUIConfig): Promise { + this.logger.info('Reading awsExports'); + const exports = config.awsExports; + this.logger.info('AWS Exports:', { exports }); + + const webuiBucket = config.WebUIBucket; + this.logger.info(`Creating aws-exports.json in ${webuiBucket}`); + await this.s3.writeJsonAsFile(webuiBucket, 'aws-exports.json', exports); + } + + private async copyUIFilesToConsoleBucket(config: WebUIConfig): Promise { + const webuiBucketName = config.WebUIBucket; + const sourceBucketName = config.SrcBucket; + const keyPrefix = config.SrcPath; + + // the webui-manifest.json has to be generated by build.sh. it lists all files of the bundled frontend + const manifestFile = keyPrefix + 'webui-manifest.json'; + this.logger.info(`Reading ${manifestFile} from ${sourceBucketName}`); + + const configJson = (await this.s3.readJsonFile(sourceBucketName, manifestFile)) as WebUIManifest; + const webUIFileNames = configJson.files; + + this.logger.info(`Deploying files from bucket ${sourceBucketName}, path ${keyPrefix} to ${webuiBucketName}`); + + for (const fileName of webUIFileNames) { + await this.s3.copyFile(sourceBucketName, webuiBucketName, keyPrefix, '', fileName); + } + + this.logger.info('WebUI assets copied successfully'); + } +} diff --git a/source/lambdas/api/handlers/findings.ts b/source/lambdas/api/handlers/findings.ts new file mode 100644 index 00000000..9e1dbd8b --- /dev/null +++ b/source/lambdas/api/handlers/findings.ts @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { dynamicImport } from 'tsimportlib'; +import { FindingsActionRequestSchema, FindingsRequestSchema } from '@asr/data-models'; + +import { SCOPE_NAME } from '../../common/constants/apiConstant'; +import { FindingsService } from '../services/findingsService'; +import { API_HEADERS, createResponse } from './apiHandler'; +import { BaseHandler, CognitoClaims } from './baseHandler'; + +const logger = new Logger({ serviceName: SCOPE_NAME }); +const tracer = new Tracer({ serviceName: SCOPE_NAME }); +const findingsService = new FindingsService(logger); +const baseHandler = new BaseHandler(logger); + +async function searchFindingsHandler(event: APIGatewayProxyEvent): Promise { + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + const findingsRequest = baseHandler.extractValidatedBody(event, FindingsRequestSchema); + + const requestedAccountIds = baseHandler.extractAccountIdsFromRequest(findingsRequest); + const authenticatedUser = await baseHandler.validateAccess( + claims, + baseHandler.createAccessRules(requestedAccountIds), + ); + + logger.debug('Searching findings', { + username: authenticatedUser.username, + groups: authenticatedUser.groups, + hasAuthorizedAccounts: !!authenticatedUser.authorizedAccounts, + }); + + // Pass authenticated user to service layer for account filtering + const result = await findingsService.searchFindings(authenticatedUser, findingsRequest); + + return createResponse(200, result, API_HEADERS.FINDINGS); +} + +async function executeFindingActionHandler(event: APIGatewayProxyEvent): Promise { + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + + // Validate request body first to get finding IDs + const actionRequest = baseHandler.extractValidatedBody(event, FindingsActionRequestSchema); + + const accountIds = baseHandler.extractAccountIdsFromArns(actionRequest.findingIds); + const authenticatedUser = await baseHandler.validateAccess(claims, baseHandler.createAccessRules(accountIds)); + + logger.debug('Executing finding action', { + username: authenticatedUser.username, + groups: authenticatedUser.groups, + actionType: actionRequest.actionType, + findingCount: actionRequest.findingIds.length, + }); + + await findingsService.executeAction(actionRequest, authenticatedUser.email); + + // Determine status code based on action type + const getStatusCodeForAction = (actionType: string): number => { + switch (actionType) { + case 'Suppress': + case 'Unsuppress': + return 200; + case 'Remediate': + case 'RemediateAndGenerateTicket': + return 202; + default: + return 202; + } + }; + + const statusCode = getStatusCodeForAction(actionRequest.actionType); + const responseBody = + statusCode === 202 && + (actionRequest.actionType === 'Remediate' || actionRequest.actionType === 'RemediateAndGenerateTicket') + ? { status: 'IN_PROGRESS' } + : ''; + + return createResponse(statusCode, responseBody, API_HEADERS.FINDINGS); +} + +export const searchFindings = async (event: APIGatewayProxyEvent, context: Context): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpJsonBodyParser } = (await dynamicImport( + '@middy/http-json-body-parser', + module, + )) as typeof import('@middy/http-json-body-parser'); + + const middlewareHandler = middy(searchFindingsHandler) + .use(httpJsonBodyParser()) + .use(injectLambdaContext(logger)) + .use(captureLambdaHandler(tracer)); + return middlewareHandler(event, context); +}; + +export const executeFindingAction = async ( + event: APIGatewayProxyEvent, + context: Context, +): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpJsonBodyParser } = (await dynamicImport( + '@middy/http-json-body-parser', + module, + )) as typeof import('@middy/http-json-body-parser'); + + const middlewareHandler = middy(executeFindingActionHandler) + .use(httpJsonBodyParser()) + .use(injectLambdaContext(logger)) + .use(captureLambdaHandler(tracer)); + return middlewareHandler(event, context); +}; diff --git a/source/lambdas/api/handlers/preSignUp.ts b/source/lambdas/api/handlers/preSignUp.ts new file mode 100644 index 00000000..0d5b3e7b --- /dev/null +++ b/source/lambdas/api/handlers/preSignUp.ts @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { PreSignUpTriggerEvent, Context, Callback } from 'aws-lambda'; +import { CognitoService } from '../services/cognito'; +import { z } from 'zod'; + +const logger = new Logger({ serviceName: 'PreSignUpHandler' }); + +const validateEmail = (email: string | undefined): boolean => { + return !!email && z.string().email().safeParse(email).success; +}; + +const extractProviderName = (userName: string): string | null => { + const parts = userName.split('_'); + return parts.length > 1 ? parts[0] : null; +}; + +const handleExternalProvider = async ( + event: PreSignUpTriggerEvent, + userEmail: string, + callback: Callback, +): Promise => { + const cognitoService = new CognitoService(logger, event.userPoolId); + + const existingUser = await cognitoService.getUserById(userEmail); + + if (!existingUser) { + logger.error('Rejecting federated sign-up - no matching user found', { email: userEmail }); + callback(new Error('User not found in local user pool'), event); + return; + } + + const providerName = extractProviderName(event.userName); + if (!providerName) { + logger.error(`Rejecting federated sign-up - could not extract provider name from user name ${event.userName}`); + callback(new Error('No provider name found'), event); + return; + } + + await cognitoService.linkFederatedUser(userEmail, providerName); + logger.info('Federated user linked to existing profile', { + email: userEmail, + existingUserType: existingUser.type, + }); + callback(null, event); +}; + +export const preSignUpHandler = async (event: PreSignUpTriggerEvent, _: Context, callback: Callback): Promise => { + try { + logger.info('PreSignUp trigger invoked', { + triggerSource: event.triggerSource, + userPoolId: event.userPoolId, + userName: event.userName, + }); + + const { triggerSource, request } = event; + + if (!('email' in request.userAttributes)) { + logger.error('Rejecting sign-up - email attribute not found in userAttributes', { + userAttributes: request.userAttributes, + }); + callback( + new Error( + '"email" attribute not found in attribute mapping, please ensure you have setup an attribute mapping for "email" in your custom Cognito identity provider', + ), + event, + ); + return; + } + + const userEmail = request.userAttributes.email; + + if (!validateEmail(userEmail)) { + logger.error('Rejecting sign-up - no valid email found', { userAttributes: request.userAttributes }); + callback(new Error('No valid email address found'), event); + return; + } + + switch (triggerSource) { + case 'PreSignUp_ExternalProvider': + await handleExternalProvider(event, userEmail, callback); + return; + + case 'PreSignUp_AdminCreateUser': + logger.info('Admin-created user sign-up - passing through', { email: userEmail }); + callback(null, event); + return; + + default: + logger.error('Rejecting sign-up from unsupported trigger source', { + triggerSource, + email: userEmail, + }); + callback(new Error('Sign-up not allowed from this source'), event); + } + } catch (error) { + logger.error('Error in PreSignUp handler', { + error: error instanceof Error ? error.message : String(error), + triggerSource: event.triggerSource, + }); + callback(error instanceof Error ? error : new Error(String(error)), event); + } +}; diff --git a/source/lambdas/api/handlers/remediations.ts b/source/lambdas/api/handlers/remediations.ts new file mode 100644 index 00000000..fcb0c119 --- /dev/null +++ b/source/lambdas/api/handlers/remediations.ts @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { dynamicImport } from 'tsimportlib'; +import { RemediationsRequestSchema, ExportRequestSchema } from '@asr/data-models'; +import { RemediationService } from '../services/remediationService'; +import { createResponse, API_HEADERS } from './apiHandler'; +import { SCOPE_NAME } from '../../common/constants/apiConstant'; +import { BaseHandler, CognitoClaims } from './baseHandler'; + +const logger = new Logger({ serviceName: SCOPE_NAME }); +const tracer = new Tracer({ serviceName: SCOPE_NAME }); +const remediationService = new RemediationService(logger); +const baseHandler = new BaseHandler(logger); + +async function searchRemediationsHandler(event: APIGatewayProxyEvent): Promise { + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + const remediationsRequest = baseHandler.extractValidatedBody(event, RemediationsRequestSchema); + + const requestedAccountIds = baseHandler.extractAccountIdsFromRequest(remediationsRequest); + const authenticatedUser = await baseHandler.validateAccess( + claims, + baseHandler.createAccessRules(requestedAccountIds), + ); + + logger.debug('Searching remediations', { + username: authenticatedUser.username, + groups: authenticatedUser.groups, + hasAuthorizedAccounts: !!authenticatedUser.authorizedAccounts, + }); + + const result = await remediationService.searchRemediations(authenticatedUser, remediationsRequest); + + return createResponse(200, result, API_HEADERS.REMEDIATIONS); +} + +async function exportRemediationsHandler(event: APIGatewayProxyEvent): Promise { + logger.debug('Export remediations handler started', { + httpMethod: event.httpMethod, + path: event.path, + hasBody: !!event.body, + }); + + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + const exportRequest = baseHandler.extractValidatedBody(event, ExportRequestSchema); + const requestedAccountIds = baseHandler.extractAccountIdsFromRequest(exportRequest); + const authenticatedUser = await baseHandler.validateAccess( + claims, + baseHandler.createAccessRules(requestedAccountIds), + ); + + const result = await remediationService.exportRemediationHistory(authenticatedUser, exportRequest); + + logger.debug('Export completed successfully', { + username: authenticatedUser.username, + hasDownloadUrl: !!result.downloadUrl, + }); + + return createResponse(200, result, API_HEADERS.REMEDIATIONS); +} + +export const searchRemediations = async ( + event: APIGatewayProxyEvent, + context: Context, +): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpJsonBodyParser } = (await dynamicImport( + '@middy/http-json-body-parser', + module, + )) as typeof import('@middy/http-json-body-parser'); + + const middlewareHandler = middy(searchRemediationsHandler) + .use(httpJsonBodyParser()) + .use(injectLambdaContext(logger)) + .use(captureLambdaHandler(tracer)); + return middlewareHandler(event, context); +}; + +export const exportRemediations = async ( + event: APIGatewayProxyEvent, + context: Context, +): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpJsonBodyParser } = (await dynamicImport( + '@middy/http-json-body-parser', + module, + )) as typeof import('@middy/http-json-body-parser'); + + const middlewareHandler = middy(exportRemediationsHandler) + .use(httpJsonBodyParser()) + .use(injectLambdaContext(logger)) + .use(captureLambdaHandler(tracer)); + return middlewareHandler(event, context); +}; diff --git a/source/lambdas/api/handlers/users.ts b/source/lambdas/api/handlers/users.ts new file mode 100644 index 00000000..00018c9a --- /dev/null +++ b/source/lambdas/api/handlers/users.ts @@ -0,0 +1,231 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { Tracer } from '@aws-lambda-powertools/tracer'; +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { CognitoService } from '../services/cognito'; +import { captureLambdaHandler } from '@aws-lambda-powertools/tracer/middleware'; +import { injectLambdaContext } from '@aws-lambda-powertools/logger/middleware'; +import { createResponse } from './apiHandler'; +import { dynamicImport } from 'tsimportlib'; +import { AccountOperatorUser, InviteUserRequest, User, PutUserRequest } from '@asr/data-models'; +import { BadRequestError, ForbiddenError, NotFoundError } from '../../common/utils/httpErrors'; +import { z } from 'zod'; +import { sendMetrics } from '../../common/utils/metricsUtils'; +import { BaseHandler, CognitoClaims, AccessRule } from './baseHandler'; +import { API_HEADERS } from './apiHandler'; + +const logger = new Logger({ serviceName: 'UsersAPI' }); +const tracer = new Tracer({ serviceName: 'UsersAPI' }); +const cognitoService = new CognitoService(logger); +const baseHandler = new BaseHandler(logger); + +async function validateAccess(claims: CognitoClaims, rules: AccessRule) { + return await baseHandler.validateAccess(claims, rules); +} + +function createGetUsersAccessRules(userType?: string): AccessRule { + return { + requiredGroups: ['AdminGroup', 'DelegatedAdminGroup'], + validator: (user, context) => { + const groups = user.groups; + const isAdmin = groups.includes('AdminGroup'); + const isDelegatedAdmin = groups.includes('DelegatedAdminGroup'); + + if (isAdmin) return; + + if (!userType) { + throw new ForbiddenError('Only Admins can access GET /users without "type" query parameter'); + } + + if (isDelegatedAdmin && userType !== 'accountOperators') { + throw new ForbiddenError( + 'DelegatedAdminGroup can only fetch Account Operators. You must provide the "type" query parameter with value "accountOperators".', + ); + } + }, + }; +} + +function createInviteUserAccessRules(role?: string): AccessRule { + return { + requiredGroups: ['AdminGroup', 'DelegatedAdminGroup'], + validator: (user, context) => { + const groups = user.groups; + const isAdmin = groups.includes('AdminGroup'); + const isDelegatedAdmin = groups.includes('DelegatedAdminGroup'); + + if (isAdmin) return; + + if (isDelegatedAdmin && role !== 'AccountOperator') { + throw new ForbiddenError('DelegatedAdminGroup can only create AccountOperator users'); + } + }, + }; +} + +function createUpdateUserAccessRules(): AccessRule { + return { + requiredGroups: ['AdminGroup', 'DelegatedAdminGroup'], + }; +} + +function createDeleteUserAccessRules(targetUserType?: string): AccessRule { + return { + requiredGroups: ['AdminGroup', 'DelegatedAdminGroup'], + validator: (user, context) => { + const groups = user.groups; + const isAdmin = groups.includes('AdminGroup'); + const isDelegatedAdmin = groups.includes('DelegatedAdminGroup'); + + if (isAdmin) return; + + if (isDelegatedAdmin && targetUserType !== 'account-operator') { + throw new ForbiddenError('DelegatedAdminGroup can only delete AccountOperator users'); + } + }, + }; +} + +function filterUsersByType(users: User[], userType?: string): User[] { + const userTypeToCognitoGroupName = { + accountOperators: 'account-operator', + delegatedAdmins: 'delegated-admin', + admins: 'admin', + }; + const userTypeAsKey = userType as keyof typeof userTypeToCognitoGroupName; + + if (userType && !userTypeToCognitoGroupName[userTypeAsKey]) { + throw new BadRequestError(`Invalid user type: ${userType}`); + } + + return userType ? users.filter((user) => user.type === userTypeToCognitoGroupName[userTypeAsKey]) : users; +} + +async function getUsersHandler(event: APIGatewayProxyEvent, _: Context): Promise { + const userType = event.queryStringParameters?.type; + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + + await validateAccess(claims, createGetUsersAccessRules(userType)); + const allUsers = await cognitoService.getAllUsers(); + const filteredUsers = filterUsersByType(allUsers, userType); + + logger.debug('Successfully retrieved users', { userCount: filteredUsers.length, userType }); + return createResponse(200, filteredUsers, API_HEADERS.USERS); +} + +async function inviteUserHandler(event: APIGatewayProxyEvent, _: Context): Promise { + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + const inviteUsersRequest = baseHandler.extractValidatedBody(event, InviteUserRequest); + + const { email, role, accountIds } = inviteUsersRequest; + if (role === 'AccountOperator' && (!accountIds || !Array.isArray(accountIds) || accountIds.length === 0)) { + throw new BadRequestError('accountIds is required for AccountOperator role'); + } + + const authenticatedUser = await validateAccess(claims, createInviteUserAccessRules(role)); + + await cognitoService.createUser(email, role, authenticatedUser.email, accountIds); + + await sendMetrics({ user_invitation: { user_type: role } }); + + logger.debug('Successfully invited user', { email, role }); + return createResponse(201, { message: 'User invited successfully', email }, API_HEADERS.USERS); +} + +async function putUserHandler(event: APIGatewayProxyEvent, _: Context): Promise { + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + const userId = event.pathParameters?.id; + if (!userId) { + throw new BadRequestError('User ID is required'); + } + + const requestUserData = baseHandler.extractValidatedBody(event, PutUserRequest); + + if (requestUserData.type !== 'account-operator') { + throw new BadRequestError('Only account-operator users can be updated'); + } + + if (userId !== requestUserData.email) + throw new BadRequestError('You may not update the userId (email) of an existing user.'); + + await validateAccess(claims, createUpdateUserAccessRules()); + + const accountOperatorData = requestUserData as Partial; + await cognitoService.updateAccountOperatorUser(userId, accountOperatorData); + logger.debug('Successfully updated user', { userId }); + return createResponse(200, { message: 'User updated successfully' }, API_HEADERS.USERS); +} + +async function deleteUserHandler(event: APIGatewayProxyEvent, _: Context): Promise { + const claims = event.requestContext?.authorizer?.claims as CognitoClaims; + const userId = event.pathParameters?.id; + if (!userId || !z.string().email().safeParse(userId).success) { + throw new BadRequestError('Valid email address is required for user ID'); + } + const targetUser = await cognitoService.getUserById(userId); + if (!targetUser) { + throw new NotFoundError(`User ${userId} not found.`); + } + + await validateAccess(claims, createDeleteUserAccessRules(targetUser.type)); + + await cognitoService.deleteUser(userId); + logger.info('Successfully deleted user', { userId }); + return createResponse(200, { message: 'User deleted successfully' }, API_HEADERS.USERS); +} + +export const getUsers = async (event: APIGatewayProxyEvent, context: Context): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + + const middlewareHandler = middy(getUsersHandler).use(injectLambdaContext(logger)).use(captureLambdaHandler(tracer)); + return middlewareHandler(event, context); +}; + +export const inviteUser = async (event: APIGatewayProxyEvent, context: Context): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpJsonBodyParser } = (await dynamicImport( + '@middy/http-json-body-parser', + module, + )) as typeof import('@middy/http-json-body-parser'); + + const middlewareHandler = middy(inviteUserHandler) + .use(injectLambdaContext(logger)) + .use(captureLambdaHandler(tracer)) + .use(httpJsonBodyParser()); + return middlewareHandler(event, context); +}; + +export const putUser = async (event: APIGatewayProxyEvent, context: Context): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpJsonBodyParser } = (await dynamicImport( + '@middy/http-json-body-parser', + module, + )) as typeof import('@middy/http-json-body-parser'); + const { default: httpUrlEncodePathParser } = (await dynamicImport( + '@middy/http-urlencode-path-parser', + module, + )) as typeof import('@middy/http-urlencode-path-parser'); + + const middlewareHandler = middy(putUserHandler) + .use(injectLambdaContext(logger)) + .use(captureLambdaHandler(tracer)) + .use(httpJsonBodyParser()) + .use(httpUrlEncodePathParser()); + return middlewareHandler(event, context); +}; + +export const deleteUser = async (event: APIGatewayProxyEvent, context: Context): Promise => { + const { default: middy } = (await dynamicImport('@middy/core', module)) as typeof import('@middy/core'); + const { default: httpUrlEncodePathParser } = (await dynamicImport( + '@middy/http-urlencode-path-parser', + module, + )) as typeof import('@middy/http-urlencode-path-parser'); + + const middlewareHandler = middy(deleteUserHandler) + .use(injectLambdaContext(logger)) + .use(captureLambdaHandler(tracer)) + .use(httpUrlEncodePathParser()); + return middlewareHandler(event, context); +}; diff --git a/source/lambdas/api/services/authorization.ts b/source/lambdas/api/services/authorization.ts new file mode 100644 index 00000000..aa5491b1 --- /dev/null +++ b/source/lambdas/api/services/authorization.ts @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { UserAccountMappingRepository } from '../../common/repositories/userAccountMappingRepository'; +import { createDynamoDBClient } from '../../common/utils/dynamodb'; +import { ForbiddenError } from '../../common/utils/httpErrors'; +import { CognitoService } from './cognito'; +import type { CognitoClaims } from '../handlers/baseHandler'; + +export interface AuthenticatedUser { + username: string; + groups: string[]; + authorizedAccounts?: string[]; + email: string; +} + +export class AuthorizationService { + private readonly userAccountMappingRepository: UserAccountMappingRepository; + private readonly cognitoService: CognitoService; + + constructor(private readonly logger: Logger) { + this.userAccountMappingRepository = new UserAccountMappingRepository( + 'AuthorizationService', + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME!, + createDynamoDBClient({}), + ); + this.cognitoService = new CognitoService(logger); + } + + async authenticateAndAuthorize(claims: CognitoClaims, requiredGroups: string[]): Promise { + const rawGroupsClaim = claims['cognito:groups']; + // groups could be a string, in which case we need to convert it into an array such that includes() + // does not simply search for substrings matching each group in requiredGroups + const groups = Array.isArray(rawGroupsClaim) ? rawGroupsClaim : rawGroupsClaim.split(','); + const username = claims.username; + + this.logger.info('User groups retrieved', { groupCount: groups.length }); + + // Check authorization + const hasRequiredGroup = requiredGroups.some((group) => groups.includes(group)); + if (!hasRequiredGroup) { + this.logger.warn(`User ${username} lacks required authorization`); + throw new ForbiddenError(); + } + + // Get user email from Cognito + const userEmailResult = await this.cognitoService.getUserEmail(username); + if (!userEmailResult?.email) { + this.logger.error('Could not retrieve user email from Cognito', { username }); + throw new ForbiddenError('Invalid user'); + } + const email = userEmailResult.email; + + // Load authorized accounts for Account Operators + let authorizedAccounts: string[] | undefined; + const isAccountOperator = groups.includes('AccountOperatorGroup'); + + if (isAccountOperator) { + try { + authorizedAccounts = await this.userAccountMappingRepository.getUserAccounts(email); + this.logger.debug('Loaded authorized accounts for Account Operator', { + username, + accountCount: authorizedAccounts?.length || 0, + email, + }); + } catch (error) { + this.logger.error('Failed to load authorized accounts for Account Operator', { + username, + email, + error: error instanceof Error ? error.message : String(error), + }); + throw new ForbiddenError('Unable to verify authorized accounts for the user'); + } + } + + return { username, groups, authorizedAccounts, email }; + } +} diff --git a/source/lambdas/api/services/baseSearchService.ts b/source/lambdas/api/services/baseSearchService.ts new file mode 100644 index 00000000..e72c3dec --- /dev/null +++ b/source/lambdas/api/services/baseSearchService.ts @@ -0,0 +1,223 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; +import { SearchCriteria, SearchFilter } from '@asr/data-models'; +import { AuthenticatedUser } from './authorization'; +import { DEFAULT_PAGE_SIZE } from '../../common/constants/apiConstant'; +import { createDynamoDBClient } from '../../common/utils/dynamodb'; +import { sendMetrics } from '../../common/utils/metricsUtils'; +import { normalizeResourceType } from '../../common/services/findingDataService'; +import { FindingAbstractData } from '@asr/data-models'; + +type ResourceType = 'Findings' | 'Remediations'; + +interface StringFilter { + FieldName?: string; + Filter?: { + Value?: string; + Comparison?: 'CONTAINS' | 'NOT_CONTAINS' | 'EQUALS' | 'NOT_EQUALS' | 'GREATER_THAN_OR_EQUAL' | 'LESS_THAN_OR_EQUAL'; + }; +} + +interface CompositeFilter { + Operator: 'AND' | 'OR'; + StringFilters: StringFilter[]; +} + +interface SearchRequest { + Filters?: { + StringFilters?: StringFilter[]; + CompositeFilters?: CompositeFilter[]; + CompositeOperator?: 'AND' | 'OR'; + }; + SortCriteria?: Array<{ + Field: string; + SortOrder: 'asc' | 'desc'; + }>; + NextToken?: string; +} + +// Searches for Resource Type use resourceTypeNormalized instead of resourceType since the value of resourceType is inconsistent +const RESOURCE_TYPE_SEARCH_FIELD: keyof FindingAbstractData = 'resourceTypeNormalized'; + +export abstract class BaseSearchService { + protected readonly dynamoDBClient: DynamoDBDocumentClient; + + protected constructor(protected readonly logger: Logger) { + this.dynamoDBClient = createDynamoDBClient({ maxAttempts: 10 }); + } + + /** + * Validates and converts a string filter to SearchFilter format + * @param stringFilter - The string filter to validate and convert + * @returns SearchFilter object if valid, null if invalid + */ + private validateAndConvertStringFilter(stringFilter: StringFilter): SearchFilter | null { + if ( + !stringFilter.FieldName || + !stringFilter.Filter || + !stringFilter.Filter.Value || + !stringFilter.Filter.Comparison + ) { + return null; + } + + let fieldName = stringFilter.FieldName; + let normalizedValue = stringFilter.Filter.Value; + + // ResourceType is a special case since the format varies between Security Hub & Security Hub CSPM + if (stringFilter.FieldName.toLowerCase() === 'resourcetype') { + normalizedValue = normalizeResourceType(stringFilter.Filter.Value); + fieldName = RESOURCE_TYPE_SEARCH_FIELD; + } + + return { + fieldName: fieldName, + value: normalizedValue, + comparison: stringFilter.Filter.Comparison, + }; + } + + /** + * Validates if a string filter has all required fields + * @param stringFilter - The string filter to validate + * @returns true if the filter is valid + */ + protected isValidStringFilter(stringFilter: StringFilter): boolean { + return this.validateAndConvertStringFilter(stringFilter) !== null; + } + + /** + * Processes string filters and adds valid ones to the filters array + * @param stringFilters - Array of string filters to process + * @param filters - Target array to add valid filters to + */ + private processStringFilters(stringFilters: StringFilter[], filters: SearchFilter[]): void { + for (const stringFilter of stringFilters) { + const convertedFilter = this.validateAndConvertStringFilter(stringFilter); + if (convertedFilter) { + filters.push(convertedFilter); + } + } + } + + /** + * Processes composite filters and adds valid string filters to the filters array + * @param compositeFilters - Array of composite filters to process + * @param filters - Target array to add valid filters to + */ + private processCompositeFilters(compositeFilters: CompositeFilter[], filters: SearchFilter[]): void { + for (const compositeFilter of compositeFilters) { + this.processStringFilters(compositeFilter.StringFilters, filters); + } + } + + /** + * Converts a search request to internal search criteria format + * @param request - The search request (FindingsRequest, RemediationsRequest, etc.) + * @param resourceType - The type of resource being searched for (Findings, Remediations) + * @returns SearchCriteria for repository layer + */ + protected async convertToSearchCriteria( + request: T, + resourceType: ResourceType, + ): Promise { + const filters: SearchFilter[] = []; + let hasCompositeFilters = false; + + if (request.Filters?.StringFilters) { + this.processStringFilters(request.Filters.StringFilters, filters); + } + + if (request.Filters?.CompositeFilters) { + hasCompositeFilters = true; + this.processCompositeFilters(request.Filters.CompositeFilters, filters); + } + + const sortCriteria = request.SortCriteria?.[0]; + + const uniqueFilters = new Set(filters.map((filter) => filter.fieldName)); + await sendMetrics({ + search_operation: { + filter_types_used: [...uniqueFilters], + filter_count: filters.length, + has_composite_filters: hasCompositeFilters, + sort_fields_used: sortCriteria?.Field ? [sortCriteria.Field] : [], // leaving open to extension with multiple sort fields + resource_type: resourceType, + }, + }); + + return { + filters, + sortField: sortCriteria?.Field, + sortOrder: sortCriteria?.SortOrder, + pageSize: DEFAULT_PAGE_SIZE, + nextToken: request.NextToken, + }; + } + + /** + * Applies account filtering for account operators by adding authorized account filters + * @param authenticatedUser - The authenticated user with potential account restrictions + * @param request - The search request to modify (FindingsRequest, RemediationsRequest, etc.) + * @returns The same request type with account filters applied if needed + */ + protected applyAccountFilteringForAccountOperators( + authenticatedUser: AuthenticatedUser, + request: T, + ): T { + if (!authenticatedUser.authorizedAccounts) { + return request; + } + + const hasAccountIdInStringFilters = request.Filters?.StringFilters?.some( + (stringFilter) => stringFilter.FieldName === 'accountId' && this.isValidStringFilter(stringFilter), + ); + + const hasAccountIdInCompositeFilters = request.Filters?.CompositeFilters?.some((compositeFilter) => + compositeFilter.StringFilters?.some( + (stringFilter) => stringFilter.FieldName === 'accountId' && this.isValidStringFilter(stringFilter), + ), + ); + + const hasAccountIdFilter = hasAccountIdInStringFilters || hasAccountIdInCompositeFilters; + + if (hasAccountIdFilter) { + return request; + } + + const userAllowedAccountIds = authenticatedUser.authorizedAccounts; + + const accountIdFilters = userAllowedAccountIds.map((accountId: string) => ({ + FieldName: 'accountId', + Filter: { + Value: accountId, + Comparison: 'EQUALS' as const, + }, + })); + + const accountCompositeFilter: CompositeFilter = { + Operator: 'OR', + StringFilters: accountIdFilters, + }; + + const modifiedRequest: T = { ...request }; + if (!modifiedRequest.Filters) { + modifiedRequest.Filters = { + CompositeFilters: [accountCompositeFilter], + CompositeOperator: 'AND', + }; + } else { + const existingFilters = modifiedRequest.Filters.CompositeFilters || []; + modifiedRequest.Filters = { + ...modifiedRequest.Filters, + CompositeFilters: [...existingFilters, accountCompositeFilter], + CompositeOperator: 'AND', + }; + } + + return modifiedRequest; + } +} diff --git a/source/lambdas/api/services/cognito.ts b/source/lambdas/api/services/cognito.ts new file mode 100644 index 00000000..a5895c88 --- /dev/null +++ b/source/lambdas/api/services/cognito.ts @@ -0,0 +1,363 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { + AdminAddUserToGroupCommand, + AdminCreateUserCommand, + AdminDeleteUserCommand, + AdminGetUserCommand, + AdminListGroupsForUserCommand, + AdminLinkProviderForUserCommand, + CognitoIdentityProviderClient, + ListUsersCommand, + UsernameExistsException, + DescribeIdentityProviderCommand, +} from '@aws-sdk/client-cognito-identity-provider'; +import { UserAccountMappingRepository } from '../../common/repositories/userAccountMappingRepository'; +import { createDynamoDBClient } from '../../common/utils/dynamodb'; +import { AccountOperatorUser, AdminUser, DelegatedAdminUser, User } from '@asr/data-models'; +import { BadRequestError, NotFoundError } from '../../common/utils/httpErrors'; + +export class CognitoService { + private readonly cognitoClient: CognitoIdentityProviderClient; + private readonly userPoolId: string; + private readonly userAccountMappingRepository: UserAccountMappingRepository; + private readonly userCache = new Map(); + + constructor( + private readonly logger: Logger, + userPoolId?: string, + ) { + this.cognitoClient = new CognitoIdentityProviderClient({}); + this.userPoolId = userPoolId ?? process.env.USER_POOL_ID!; + this.userAccountMappingRepository = new UserAccountMappingRepository( + 'UsersAPI', + process.env.USER_ACCOUNT_MAPPING_TABLE_NAME!, + createDynamoDBClient({}), + ); + } + + async getAllUsers(): Promise { + try { + const response = await this.cognitoClient.send( + new ListUsersCommand({ + UserPoolId: this.userPoolId, + }), + ); + + const users: User[] = []; + for (const cognitoUser of response?.Users || []) { + const email = cognitoUser.Attributes?.find((attr) => attr.Name === 'email')?.Value; + const invitedBy = cognitoUser.Attributes?.find((attr) => attr.Name === 'custom:invitedBy')?.Value; + const username = cognitoUser.Username!; + + if (!email || !invitedBy) { + this.logger.warn('Skipping user with missing required attributes', { username }); + continue; + } + + const groupsResponse = await this.cognitoClient.send( + new AdminListGroupsForUserCommand({ + UserPoolId: this.userPoolId, + Username: username, + }), + ); + + const groups = groupsResponse.Groups?.map((group) => group.GroupName!) || []; + const userType = this.determineUserType(groups); + + if (!userType) { + this.logger.warn('Skipping user with no recognized groups', { username, groups }); + continue; + } + + const user = await this.constructUserFromCognitoData( + email, + invitedBy, + userType, + cognitoUser.UserCreateDate, + cognitoUser.UserStatus, + ); + if (user) { + users.push(user); + } + } + + return users; + } catch (error) { + this.logger.error('Failed to retrieve users', { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + } + + async getUserById(userId: string): Promise { + const cached = this.userCache.get(userId); + if (cached) { + return cached.user; + } + + try { + const response = await this.cognitoClient.send( + new AdminGetUserCommand({ + UserPoolId: this.userPoolId, + Username: userId, + }), + ); + + const email = response.UserAttributes?.find((attr) => attr.Name === 'email')?.Value; + const invitedBy = response.UserAttributes?.find((attr) => attr.Name === 'custom:invitedBy')?.Value; + + if (!email || !invitedBy) { + this.userCache.set(userId, { user: null }); + return null; + } + + const groupsResponse = await this.cognitoClient.send( + new AdminListGroupsForUserCommand({ + UserPoolId: this.userPoolId, + Username: userId, + }), + ); + + const groups = groupsResponse.Groups?.map((group) => group.GroupName!) || []; + const userType = this.determineUserType(groups); + + if (!userType) { + this.userCache.set(userId, { user: null }); + return null; + } + + const user = await this.constructUserFromCognitoData( + email, + invitedBy, + userType, + response.UserCreateDate, + response.UserStatus, + ); + + this.userCache.set(userId, { user }); + return user; + } catch (error) { + this.logger.error('Failed to retrieve user by ID', { + userId, + error: error instanceof Error ? error.message : String(error), + }); + this.userCache.set(userId, { user: null }); + return null; + } + } + + async getUserEmail(userId: string): Promise<{ email: string } | null> { + try { + const user = await this.getUserById(userId); + const email = user?.email; + + if (!email) { + this.logger.warn('User missing email attribute', { userId }); + return null; + } + + return { email }; + } catch (error) { + this.logger.error('Failed to retrieve user email by ID', { + userId, + userPoolId: this.userPoolId, + error: error instanceof Error ? error.message : String(error), + }); + return null; + } + } + + private async constructUserFromCognitoData( + email: string, + invitedBy: string, + userType: 'admin' | 'delegated-admin' | 'account-operator', + userCreateDate?: Date, + userStatus?: string, + ): Promise { + const baseUser = { + email, + invitedBy, + invitationTimestamp: userCreateDate?.toISOString() || new Date().toISOString(), + status: userStatus === 'CONFIRMED' ? ('Confirmed' as const) : ('Invited' as const), + }; + + switch (userType) { + case 'admin': + return { ...baseUser, type: 'admin' } as AdminUser; + case 'delegated-admin': + return { ...baseUser, type: 'delegated-admin' } as DelegatedAdminUser; + case 'account-operator': { + const accountIds = (await this.userAccountMappingRepository.getUserAccounts(email)) ?? []; + return { ...baseUser, type: 'account-operator', accountIds } as AccountOperatorUser; + } + } + } + + async createUser( + email: string, + role: 'DelegatedAdmin' | 'AccountOperator', + invitedBy: string, + accountIds?: string[], + ): Promise { + try { + await this.cognitoClient.send( + new AdminCreateUserCommand({ + UserPoolId: this.userPoolId, + Username: email, + UserAttributes: [ + { Name: 'email', Value: email }, + { Name: 'email_verified', Value: 'true' }, + { Name: 'custom:invitedBy', Value: invitedBy }, + ], + }), + ); + + const groupName = role === 'DelegatedAdmin' ? 'DelegatedAdminGroup' : 'AccountOperatorGroup'; + + await this.cognitoClient.send( + new AdminAddUserToGroupCommand({ + UserPoolId: this.userPoolId, + Username: email, + GroupName: groupName, + }), + ); + + if (role === 'AccountOperator' && accountIds) { + await this.userAccountMappingRepository.create({ + userId: email, + accountIds, + invitedBy, + invitationTimestamp: new Date().toISOString(), + }); + } + } catch (error) { + this.logger.error('Failed to create user', { + email, + role, + error: error instanceof Error ? error.message : String(error), + }); + if (error instanceof UsernameExistsException) + throw new BadRequestError(`User with username ${email} already exists.`); + throw error; + } + } + + async updateAccountOperatorUser(userId: string, userData: Partial): Promise { + const existingUser = await this.getUserById(userId); + if (!existingUser) { + throw new NotFoundError(`User ${userId} not found.`); + } + + if (userData.type && existingUser.type !== userData.type) { + throw new BadRequestError( + 'Requested user type does not match current type for the user. Modifying the user type is not currently supported.', + ); + } + + if (userData.status && existingUser.status !== userData.status) { + throw new BadRequestError( + 'Requested user status does not match current status for the user. Modifying the user status is not currently supported.', + ); + } + + const existingMapping = await this.userAccountMappingRepository.findById(userId, ''); + if (existingMapping) { + await this.userAccountMappingRepository.put({ + ...existingMapping, + accountIds: userData.accountIds ?? [], + }); + } else { + await this.userAccountMappingRepository.create({ + userId, + accountIds: userData.accountIds ?? [], + invitedBy: existingUser.invitedBy, + invitationTimestamp: new Date().toISOString(), + }); + } + this.userCache.delete(userId); + } + + async deleteUser(userId: string): Promise { + const user = await this.getUserById(userId); + if (!user) { + throw new NotFoundError(`User ${userId} not found.`); + } + + if (user.type === 'account-operator') { + await this.userAccountMappingRepository.deleteIfExists(userId, ''); + } + + await this.cognitoClient.send( + new AdminDeleteUserCommand({ + UserPoolId: this.userPoolId, + Username: userId, + }), + ); + + this.userCache.delete(userId); + } + + async getProviderEmailAttributeName(providerName: string): Promise { + const describeProviderResponse = await this.cognitoClient.send( + new DescribeIdentityProviderCommand({ + UserPoolId: this.userPoolId, + ProviderName: providerName, + }), + ); + + if (!describeProviderResponse?.IdentityProvider?.AttributeMapping) { + this.logger.error(`Could not find attribute mapping object for provider ${providerName}`); + throw new Error(`Could not find attribute mapping for provider ${providerName}`); + } + + const emailAttributeName = describeProviderResponse.IdentityProvider.AttributeMapping.email; + + if (!emailAttributeName) { + this.logger.error( + `Could not find attribute mapping for email in provider ${providerName}. Ensure this provider is configured with an attribute mapping for the cognito email attribute.`, + ); + throw new Error( + `Could not find email attribute mapping for provider ${providerName}. Ensure you have configured an email attribute mapping for this provider.`, + ); + } + + return emailAttributeName; + } + + async linkFederatedUser(email: string, providerName: string): Promise { + const providerEmailAttributeName = await this.getProviderEmailAttributeName(providerName); + await this.cognitoClient.send( + new AdminLinkProviderForUserCommand({ + UserPoolId: this.userPoolId, + DestinationUser: { + ProviderName: 'Cognito', + ProviderAttributeValue: email, + }, + SourceUser: { + ProviderName: providerName, + ProviderAttributeName: providerEmailAttributeName, + ProviderAttributeValue: email, + }, + }), + ); + + this.logger.info('Linked federated user to existing user profile', { email, providerName }); + } + + private determineUserType(groups: string[]): 'admin' | 'delegated-admin' | 'account-operator' | null { + if (groups.includes('AdminGroup')) { + return 'admin'; + } + if (groups.includes('DelegatedAdminGroup')) { + return 'delegated-admin'; + } + if (groups.includes('AccountOperatorGroup')) { + return 'account-operator'; + } + return null; + } +} diff --git a/source/lambdas/api/services/findingsService.ts b/source/lambdas/api/services/findingsService.ts new file mode 100644 index 00000000..20fbc262 --- /dev/null +++ b/source/lambdas/api/services/findingsService.ts @@ -0,0 +1,257 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + ActionResult, + RemediationResult, + SuppressionResult, + FindingsActionRequest, + FindingsRequest, +} from '@asr/data-models'; +import { Logger } from '@aws-lambda-powertools/logger'; +import crypto from 'crypto'; +import { inflate } from 'pako'; +import { SCOPE_NAME } from '../../common/constants/apiConstant'; +import { FindingRepository } from '../../common/repositories/findingRepository'; +import { RemediationHistoryRepository } from '../../common/repositories/remediationHistoryRepository'; +import type { ASFFFinding, FindingApiResponse, FindingTableItem } from '@asr/data-models'; +import { ErrorUtils } from '../../common/utils/errorUtils'; +import { getSecurityHubConsoleUrl } from '../../common/utils/findingUtils'; +import { BadRequestError } from '../../common/utils/httpErrors'; +import { sendMetrics } from '../../common/utils/metricsUtils'; +import { executeOrchestrator } from '../../common/utils/orchestrator'; +import { mapRemediationStatus } from '../../common/utils/remediationStatusMapper'; +import { AuthenticatedUser } from './authorization'; +import { BaseSearchService } from './baseSearchService'; + +export class FindingsService extends BaseSearchService { + private readonly findingRepository: FindingRepository; + private readonly remediationHistoryRepository: RemediationHistoryRepository; + + constructor(logger: Logger) { + super(logger); + + this.findingRepository = new FindingRepository(SCOPE_NAME, process.env.FINDINGS_TABLE_NAME!, this.dynamoDBClient); + + this.remediationHistoryRepository = new RemediationHistoryRepository( + SCOPE_NAME, + process.env.REMEDIATION_HISTORY_TABLE_NAME!, + this.dynamoDBClient, + process.env.FINDINGS_TABLE_NAME!, + ); + } + + async searchFindings( + authenticatedUser: AuthenticatedUser, + request: FindingsRequest, + ): Promise<{ Findings: FindingApiResponse[]; NextToken?: string }> { + try { + const modifiedRequest = this.applyAccountFilteringForAccountOperators(authenticatedUser, request); + const searchCriteria = await this.convertToSearchCriteria(modifiedRequest, 'Findings'); + const searchResult = await this.findingRepository.searchFindings(searchCriteria); + + return { + Findings: searchResult.items.map((item) => this.convertToApiResponse(item)), + NextToken: searchResult.nextToken, + }; + } catch (error) { + this.logger.error('Error searching findings', { + request: { + ...request, + NextToken: request.NextToken ? `${request.NextToken.substring(0, 20)}...` : undefined, + }, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + throw error; + } + } + + async executeAction(request: FindingsActionRequest, principal: string): Promise { + try { + const findings = await this.findingRepository.findByFindingIds(request.findingIds); + + if (findings.length === 0) { + throw new BadRequestError('No findings found for the provided IDs'); + } + + const fieldUpdates = await this.getFieldUpdatesForAction(request.actionType, findings); + + if (request.actionType === 'Remediate' || request.actionType === 'RemediateAndGenerateTicket') { + await this.executeRemediationWithHistory(findings, fieldUpdates, principal); + } else { + if (this.isSuppressionResult(fieldUpdates)) { + const updatedFindings = this.prepareUpdatedFindings(principal, findings, fieldUpdates); + await this.findingRepository.putAll(...updatedFindings); + } else { + throw new Error('Invalid field updates for suppression action'); + } + } + } catch (error) { + this.logger.error('Error executing action', { + actionType: request.actionType, + error: ErrorUtils.formatErrorMessage(error), + }); + + if (error instanceof BadRequestError) throw error; + throw new BadRequestError('Failed to execute action on findings'); + } + } + + private async executeRemediationWithHistory( + findings: FindingTableItem[], + fieldUpdates: ActionResult, + principal: string, + ): Promise { + const { remediationStatus, executionIdsByFindingId = new Map() } = fieldUpdates as RemediationResult; + + for (const finding of findings) { + const executionId = executionIdsByFindingId.get(finding.findingId); + const updatedFinding = { + ...finding, + remediationStatus: mapRemediationStatus(remediationStatus), + ...(executionId && { executionId }), + lastUpdatedBy: principal, + }; + + await this.remediationHistoryRepository.createRemediationHistoryWithFindingUpdate(updatedFinding, executionId); + } + } + + private prepareUpdatedFindings( + principal: string, + findings: FindingTableItem[], + fieldUpdates: SuppressionResult, + ): FindingTableItem[] { + return findings.map((finding) => ({ + ...finding, + ...fieldUpdates, + lastUpdatedBy: principal, + lastUpdatedTime: new Date().toISOString(), + })); + } + + private isSuppressionResult(result: ActionResult): result is SuppressionResult { + return 'suppressed' in result; + } + + private async getFieldUpdatesForAction(actionType: string, findings: FindingTableItem[]): Promise { + switch (actionType) { + case 'Suppress': + await sendMetrics({ finding_suppressed: 1 }); + return { suppressed: true }; + case 'Unsuppress': + return { suppressed: false }; + case 'Remediate': + return await this.executeRemediationWithTracking('Remediate', findings); + case 'RemediateAndGenerateTicket': + return await this.executeRemediationWithTracking('RemediateAndGenerateTicket', findings); + default: + throw new Error(`Unsupported action type: ${actionType}`); + } + } + + private static extractASFFFinding(findingTableItem: FindingTableItem): ASFFFinding { + try { + if (!findingTableItem.findingJSON) { + throw new Error('No findingJSON data available'); + } + const decompressed = inflate(findingTableItem.findingJSON, { to: 'string' }); + return JSON.parse(decompressed); + } catch (error) { + throw new Error(`Failed to extract ASFF finding: ${ErrorUtils.formatErrorMessage(error)}`); + } + } + + private static buildOrchestratorInput(asffFinding: ASFFFinding, actionType: string): string { + const actionName = actionType === 'RemediateAndGenerateTicket' ? 'ASR:Remediate&Ticket' : 'Remediate with ASR'; + + return JSON.stringify({ + version: '0', + id: crypto.randomUUID(), + 'detail-type': 'Security Hub Findings - API Action', + source: 'aws.securityhub', + account: asffFinding.AwsAccountId, + region: asffFinding.Region, + time: new Date().toISOString(), + resources: [ + `arn:aws:securityhub:${asffFinding.Region}:${asffFinding.AwsAccountId}:action/custom/api-${actionType.toLowerCase()}`, + ], + detail: { + findings: [asffFinding], + actionName, + actionDescription: `API-triggered ${actionType}`, + }, + }); + } + + /** + * Common method for executing remediation actions + */ + private async executeRemediationWithTracking( + actionType: string, + findings: FindingTableItem[], + ): Promise { + const findingCount = findings.length || 0; + + this.logger.debug(`Starting ${actionType} async process`, { + findingCount, + }); + + try { + const executionIdsByFindingId = new Map(); + + for (const findingTableItem of findings) { + const asffFinding = FindingsService.extractASFFFinding(findingTableItem); + const orchestratorInput = FindingsService.buildOrchestratorInput(asffFinding, actionType); + const executionId = await executeOrchestrator(orchestratorInput, this.logger); + + if (executionId) { + executionIdsByFindingId.set(findingTableItem.findingId, executionId); + } else { + this.logger.warn('Failed to get execution ID for finding', { + findingId: findingTableItem.findingId, + actionType, + }); + } + } + + return { + remediationStatus: 'IN_PROGRESS', + executionIdsByFindingId, + }; + } catch (error) { + const errorMessage = ErrorUtils.formatErrorMessage(error); + + this.logger.error(`Failed to execute ${actionType} orchestrator`, { + error: errorMessage, + findingCount, + }); + + return { + remediationStatus: 'FAILED', + error: errorMessage, + }; + } + } + + private convertToApiResponse(item: FindingTableItem): FindingApiResponse { + // Remove internal fields and return only API-relevant data + const { + 'securityHubUpdatedAtTime#findingId': _lsiSortKey, + findingJSON: _findingJSON, + findingIdControl: _findingIdControl, + FINDING_CONSTANT: _findingConstant, + lastUpdatedBy: _lastUpdatedBy, + expireAt: _expireAt, + ...baseApiResponse + } = item; + + const consoleLink = getSecurityHubConsoleUrl(baseApiResponse.findingId); + + return { + ...baseApiResponse, + consoleLink, + }; + } +} diff --git a/source/lambdas/api/services/remediationService.ts b/source/lambdas/api/services/remediationService.ts new file mode 100644 index 00000000..02b6d7b2 --- /dev/null +++ b/source/lambdas/api/services/remediationService.ts @@ -0,0 +1,269 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Logger } from '@aws-lambda-powertools/logger'; +import { ASRS3Client } from '../clients/ASRS3Client'; +import { RemediationHistoryRepository } from '../../common/repositories/remediationHistoryRepository'; +import type { RemediationHistoryApiResponse, RemediationHistoryTableItem } from '@asr/data-models'; +import { RemediationsRequest, ExportRequest, SearchCriteria } from '@asr/data-models'; +import { AuthenticatedUser } from './authorization'; +import { SCOPE_NAME } from '../../common/constants/apiConstant'; +import { BaseSearchService } from './baseSearchService'; +import { getStepFunctionsConsoleUrl } from '../../common/utils/findingUtils'; + +export class RemediationService extends BaseSearchService { + private readonly remediationHistoryRepository: RemediationHistoryRepository; + private readonly s3Client: ASRS3Client; + + constructor(logger: Logger) { + super(logger); + + this.remediationHistoryRepository = new RemediationHistoryRepository( + SCOPE_NAME, + process.env.REMEDIATION_HISTORY_TABLE_NAME!, + this.dynamoDBClient, + process.env.FINDINGS_TABLE_NAME!, + ); + + this.s3Client = new ASRS3Client(); + } + + async searchRemediations( + authenticatedUser: AuthenticatedUser, + request: RemediationsRequest, + ): Promise<{ Remediations: RemediationHistoryApiResponse[]; NextToken?: string }> { + this.logger.debug('Searching remediations with request', { remediationsRequest: request }); + + try { + const modifiedRequest = this.applyAccountFilteringForAccountOperators(authenticatedUser, request); + const searchCriteria = await this.convertToSearchCriteria(modifiedRequest, 'Remediations'); + + this.logger.debug('Executing remediation search with criteria', { + filtersCount: searchCriteria.filters.length, + sortOrder: searchCriteria.sortOrder, + pageSize: searchCriteria.pageSize, + hasNextToken: !!searchCriteria.nextToken, + }); + + const searchResult = await this.remediationHistoryRepository.searchRemediations(searchCriteria); + + this.logger.debug('Remediation search completed successfully', { + remediationsCount: searchResult.items.length, + hasNextToken: !!searchResult.nextToken, + }); + + return { + Remediations: searchResult.items.map((item) => this.convertToApiResponse(item)), + NextToken: searchResult.nextToken, + }; + } catch (error) { + this.logger.error('Error searching remediations', { + request: { + ...request, + NextToken: request.NextToken ? `${request.NextToken.substring(0, 20)}...` : undefined, + }, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + throw error; + } + } + + private convertToApiResponse(item: RemediationHistoryTableItem): RemediationHistoryApiResponse { + // Remove internal fields and return only API-relevant data + const { + 'findingId#executionId': _compositeKey, + 'lastUpdatedTime#findingId': _lsiSortKey, + REMEDIATION_CONSTANT: _remediationConstant, + expireAt: _expireAt, + ...baseApiResponse + } = item; + + const consoleLink = getStepFunctionsConsoleUrl(baseApiResponse.executionId); + + return { + ...baseApiResponse, + consoleLink, + }; + } + + async exportRemediationHistory( + authenticatedUser: AuthenticatedUser, + request: ExportRequest, + ): Promise<{ downloadUrl: string }> { + this.logger.debug('Starting remediation history export', { + request, + username: authenticatedUser.username, + hasFilters: !!request.Filters, + }); + + try { + const searchCriteria = await this.buildExportSearchCriteria(authenticatedUser, request); + + const allRemediations = await this.fetchAllRemediationsForExport(searchCriteria); + + this.logger.debug('Remediation data prepared for export', { + totalRemediations: allRemediations.length, + hasFilters: !!request.Filters, + }); + + const csvContent = this.convertRemediationsToCSV(allRemediations); + + const downloadUrl = await this.uploadToS3AndGenerateUrl(csvContent); + + this.logger.debug('Remediation history export completed successfully', { + totalRemediations: allRemediations.length, + csvSizeBytes: csvContent.length, + hasDownloadUrl: !!downloadUrl, + }); + + return { downloadUrl }; + } catch (error) { + this.logger.error('Error exporting remediation history', { + request, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + throw error; + } + } + + private async buildExportSearchCriteria( + authenticatedUser: AuthenticatedUser, + request: ExportRequest, + ): Promise { + const modifiedRequest = this.applyAccountFilteringForAccountOperators(authenticatedUser, request); + + const searchCriteria = await this.convertToSearchCriteria(modifiedRequest, 'Remediations'); + + return { + ...searchCriteria, + nextToken: undefined, // Always start from beginning for export + pageSize: 100, // Large page size for export + }; + } + + private async fetchAllRemediationsForExport(searchCriteria: SearchCriteria): Promise { + const allRemediations: RemediationHistoryTableItem[] = []; + let nextToken: string | undefined; + let batchCount = 0; + + this.logger.debug('Starting export data fetch with unlimited size', { + totalFilters: searchCriteria.filters.length, + }); + + do { + const result = await this.remediationHistoryRepository.searchRemediations({ + ...searchCriteria, + nextToken, + }); + + allRemediations.push(...result.items); + nextToken = result.nextToken; + batchCount++; + + this.logger.debug('Fetched batch for export', { + batchNumber: batchCount, + batchSize: result.items.length, + totalSoFar: allRemediations.length, + hasMore: !!nextToken, + }); + + if (batchCount > 100) { + this.logger.warn('Export reached maximum batch limit', { + batchCount, + totalRecords: allRemediations.length, + }); + break; + } + } while (nextToken); + + this.logger.info('Export data fetch completed', { + totalBatches: batchCount, + totalRecords: allRemediations.length, + }); + + return allRemediations; + } + + private convertRemediationsToCSV(remediations: RemediationHistoryTableItem[]): string { + const displayHeaders = [ + 'Finding ID', + 'Account', + 'Resource ID', + 'Resource Type', + 'Finding Type', + 'Severity', + 'Region', + 'Status', + 'Execution Timestamp', + 'Executed By', + 'Execution ID', + 'Error', + ]; + + const fieldNames = [ + 'findingId', + 'accountId', + 'resourceId', + 'resourceTypeNormalized', + 'findingType', + 'severity', + 'region', + 'remediationStatus', + 'lastUpdatedTime', + 'lastUpdatedBy', + 'executionId', + 'error', + ]; + + const csvRows = [displayHeaders.join(',')]; + + if (remediations.length === 0) { + this.logger.info('No remediation data found for export - returning empty CSV with headers only'); + return csvRows.join('\n'); + } + + for (const remediation of remediations) { + const row = fieldNames.map((fieldName) => { + const value = remediation[fieldName as keyof RemediationHistoryTableItem]; + if (value === null || value === undefined) { + return ''; + } + const stringValue = String(value); + if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) { + return `"${stringValue.replace(/"/g, '""')}"`; + } + return stringValue; + }); + csvRows.push(row.join(',')); + } + + this.logger.debug('CSV conversion completed', { + totalRows: csvRows.length - 1, // Exclude header row + totalColumns: displayHeaders.length, + }); + + return csvRows.join('\n'); + } + + private async uploadToS3AndGenerateUrl(csvContent: string): Promise { + const bucketName = process.env.CSV_EXPORT_BUCKET_NAME; + if (!bucketName) { + throw new Error('CSV_EXPORT_BUCKET_NAME environment variable not set'); + } + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const fileName = `remediation-history-export-${timestamp}.csv`; + + const presignedUrl = await this.s3Client.uploadCsvAndGeneratePresignedUrl(bucketName, fileName, csvContent); + + this.logger.debug('Successfully uploaded to S3 and generated pre-signed URL', { + fileName, + bucketName, + urlGenerated: true, + }); + + return presignedUrl; + } +} diff --git a/source/lambdas/common/__tests__/dynamodbSetup.ts b/source/lambdas/common/__tests__/dynamodbSetup.ts new file mode 100644 index 00000000..a13b8503 --- /dev/null +++ b/source/lambdas/common/__tests__/dynamodbSetup.ts @@ -0,0 +1,261 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + CreateTableCommand, + DeleteTableCommand, + DescribeTableCommand, + ListTablesCommand, + waitUntilTableExists, + waitUntilTableNotExists, +} from '@aws-sdk/client-dynamodb'; +import { DeleteCommand, DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb'; +import { createDynamoDBClient } from '../utils/dynamodb'; + +export class DynamoDBTestSetup { + private static docClient: DynamoDBDocumentClient; + + static async initialize() { + this.docClient = createDynamoDBClient({ + endpoint: 'http://127.0.0.1:8000', + region: 'us-east-1', + credentials: { + accessKeyId: 'fakeMyKeyId', + secretAccessKey: 'fakeSecretAccessKey', + }, + }); + } + + static getDocClient(): DynamoDBDocumentClient { + return this.docClient; + } + + static async createFindingsTable(tableName: string) { + if (!this.docClient) { + throw new Error('DynamoDBTestSetup not initialized. Call DynamoDBTestSetup.initialize() first.'); + } + if (await this.tableExists(tableName)) return; + + await this.docClient.send( + new CreateTableCommand({ + TableName: tableName, + KeySchema: [ + { AttributeName: 'findingType', KeyType: 'HASH' }, + { AttributeName: 'findingId', KeyType: 'RANGE' }, + ], + AttributeDefinitions: [ + { AttributeName: 'findingType', AttributeType: 'S' }, + { AttributeName: 'findingId', AttributeType: 'S' }, + { AttributeName: 'accountId', AttributeType: 'S' }, + { AttributeName: 'resourceId', AttributeType: 'S' }, + { AttributeName: 'severity', AttributeType: 'S' }, + { AttributeName: 'FINDING_CONSTANT', AttributeType: 'S' }, + { AttributeName: 'securityHubUpdatedAtTime#findingId', AttributeType: 'S' }, + ], + LocalSecondaryIndexes: [ + { + IndexName: 'securityHubUpdatedAtTime-findingId-LSI', + KeySchema: [ + { AttributeName: 'findingType', KeyType: 'HASH' }, + { AttributeName: 'securityHubUpdatedAtTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + ], + GlobalSecondaryIndexes: [ + { + IndexName: 'accountId-securityHubUpdatedAtTime-GSI', + KeySchema: [ + { AttributeName: 'accountId', KeyType: 'HASH' }, + { AttributeName: 'securityHubUpdatedAtTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + { + IndexName: 'resourceId-securityHubUpdatedAtTime-GSI', + KeySchema: [ + { AttributeName: 'resourceId', KeyType: 'HASH' }, + { AttributeName: 'securityHubUpdatedAtTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + { + IndexName: 'severity-securityHubUpdatedAtTime-GSI', + KeySchema: [ + { AttributeName: 'severity', KeyType: 'HASH' }, + { AttributeName: 'securityHubUpdatedAtTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + { + IndexName: 'allFindings-securityHubUpdatedAtTime-GSI', + KeySchema: [ + { AttributeName: 'FINDING_CONSTANT', KeyType: 'HASH' }, + { AttributeName: 'securityHubUpdatedAtTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + ], + BillingMode: 'PAY_PER_REQUEST', + }), + ); + + await waitUntilTableExists({ client: this.docClient, maxWaitTime: 30 }, { TableName: tableName }); + } + + static async createConfigTable(tableName: string) { + if (!this.docClient) { + throw new Error('DynamoDBTestSetup not initialized. Call DynamoDBTestSetup.initialize() first.'); + } + if (await this.tableExists(tableName)) return; + + await this.docClient.send( + new CreateTableCommand({ + TableName: tableName, + KeySchema: [{ AttributeName: 'controlId', KeyType: 'HASH' }], + AttributeDefinitions: [{ AttributeName: 'controlId', AttributeType: 'S' }], + BillingMode: 'PAY_PER_REQUEST', + }), + ); + + await waitUntilTableExists({ client: this.docClient, maxWaitTime: 30 }, { TableName: tableName }); + } + + static async createUserAccountMappingTable(tableName: string) { + if (await this.tableExists(tableName)) return; + + await this.docClient.send( + new CreateTableCommand({ + TableName: tableName, + KeySchema: [{ AttributeName: 'userId', KeyType: 'HASH' }], + AttributeDefinitions: [{ AttributeName: 'userId', AttributeType: 'S' }], + BillingMode: 'PAY_PER_REQUEST', + }), + ); + + await waitUntilTableExists({ client: this.docClient, maxWaitTime: 30 }, { TableName: tableName }); + } + + static async createRemediationHistoryTable(tableName: string) { + if (await this.tableExists(tableName)) return; + + await this.docClient.send( + new CreateTableCommand({ + TableName: tableName, + KeySchema: [ + { AttributeName: 'findingType', KeyType: 'HASH' }, + { AttributeName: 'findingId#executionId', KeyType: 'RANGE' }, + ], + AttributeDefinitions: [ + { AttributeName: 'findingType', AttributeType: 'S' }, + { AttributeName: 'findingId#executionId', AttributeType: 'S' }, + { AttributeName: 'findingId', AttributeType: 'S' }, + { AttributeName: 'accountId', AttributeType: 'S' }, + { AttributeName: 'resourceId', AttributeType: 'S' }, + { AttributeName: 'REMEDIATION_CONSTANT', AttributeType: 'S' }, + { AttributeName: 'lastUpdatedTime#findingId', AttributeType: 'S' }, + ], + GlobalSecondaryIndexes: [ + { + IndexName: 'lastUpdatedTime-findingIdIndex', + KeySchema: [ + { AttributeName: 'findingType', KeyType: 'HASH' }, + { AttributeName: 'lastUpdatedTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + { + IndexName: 'allRemediations-lastUpdatedTime-GSI', + KeySchema: [ + { AttributeName: 'REMEDIATION_CONSTANT', KeyType: 'HASH' }, + { AttributeName: 'lastUpdatedTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + { + IndexName: 'accountId-lastUpdatedTime-GSI', + KeySchema: [ + { AttributeName: 'accountId', KeyType: 'HASH' }, + { AttributeName: 'lastUpdatedTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + { + IndexName: 'resourceId-lastUpdatedTime-GSI', + KeySchema: [ + { AttributeName: 'resourceId', KeyType: 'HASH' }, + { AttributeName: 'lastUpdatedTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + { + IndexName: 'findingId-lastUpdatedTime-GSI', + KeySchema: [ + { AttributeName: 'findingId', KeyType: 'HASH' }, + { AttributeName: 'lastUpdatedTime#findingId', KeyType: 'RANGE' }, + ], + Projection: { ProjectionType: 'ALL' }, + }, + ], + BillingMode: 'PAY_PER_REQUEST', + }), + ); + + await waitUntilTableExists({ client: this.docClient, maxWaitTime: 30 }, { TableName: tableName }); + } + + static async deleteTable(tableName: string) { + if (!(await this.tableExists(tableName))) return; + + await this.docClient.send(new DeleteTableCommand({ TableName: tableName })); + await waitUntilTableNotExists({ client: this.docClient, maxWaitTime: 30 }, { TableName: tableName }); + } + + static async cleanup() { + const response = await this.docClient.send(new ListTablesCommand({})); + const { TableNames } = response || {}; + if (TableNames && TableNames.length > 0) { + await Promise.all(TableNames.map((tableName) => this.deleteTable(tableName))); + } + } + + static async tableExists(tableName: string): Promise { + if (!this.docClient) { + throw new Error('DynamoDBTestSetup not initialized. Call DynamoDBTestSetup.initialize() first.'); + } + try { + await this.docClient.send(new DescribeTableCommand({ TableName: tableName })); + return true; + } catch (error: any) { + if (error.name === 'ResourceNotFoundException') { + return false; + } + throw error; + } + } + + static async clearTable( + tableName: string, + tableType: 'findings' | 'config' | 'userAccountMapping' | 'remediationHistory', + ) { + if (!(await this.tableExists(tableName))) return; + + const scanResult = await this.docClient.send(new ScanCommand({ TableName: tableName })); + const { Items } = scanResult || { Items: [] }; + if (Items && Items.length > 0) { + for (const item of Items) { + let key; + if (tableType === 'findings') { + key = { findingType: item.findingType, findingId: item.findingId }; + } else if (tableType === 'remediationHistory') { + key = { findingType: item.findingType, 'findingId#executionId': item['findingId#executionId'] }; + } else if (tableType === 'userAccountMapping') { + key = { userId: item.userId }; + } else { + key = { controlId: item.controlId }; + } + await this.docClient.send(new DeleteCommand({ TableName: tableName, Key: key })); + } + } + } +} diff --git a/source/lambdas/common/__tests__/envSetup.ts b/source/lambdas/common/__tests__/envSetup.ts new file mode 100644 index 00000000..a6f074d2 --- /dev/null +++ b/source/lambdas/common/__tests__/envSetup.ts @@ -0,0 +1,35 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const findingsTableName = 'test-findings-table'; +export const configTableName = 'test-config-table'; +export const userAccountMappingTableName = 'test-user-account-mapping-table'; +export const remediationHistoryTableName = 'test-remediation-history-table'; +export const userPoolId = 'us-east-1_testpool'; +export const mockAccountId = '123456789012'; + +// Set environment variables before any imports (jest setupFiles) +process.env.AWS_REGION = 'us-east-1'; +process.env.FINDINGS_TABLE_NAME = findingsTableName; +process.env.USER_ACCOUNT_MAPPING_TABLE_NAME = userAccountMappingTableName; +process.env.REMEDIATION_HISTORY_TABLE_NAME = remediationHistoryTableName; +process.env.FINDINGS_TABLE_ARN = `arn:aws:dynamodb:us-east-1:123456789012:table/${findingsTableName}`; +process.env.REMEDIATION_CONFIG_TABLE_ARN = `arn:aws:dynamodb:us-east-1:123456789012:table/${configTableName}`; +process.env.REMEDIATION_HISTORY_TABLE_ARN = `arn:aws:dynamodb:us-east-1:123456789012:table/${remediationHistoryTableName}`; +process.env.ORCHESTRATOR_ARN = 'arn:aws:states:us-east-1:123456789012:stateMachine:orchestrator'; +process.env.SOLUTION_TRADEMARKEDNAME = 'ASR-Test'; +process.env.DYNAMODB_ENDPOINT = 'http://127.0.0.1:8000'; +process.env.USER_POOL_ID = 'us-east-1_testpool'; +process.env.LOG_LEVEL = 'debug'; +process.env.AWS_REGION = 'us-east-1'; +process.env.AWS_ACCESS_KEY_ID = 'fakeMyKeyId'; +process.env.AWS_SECRET_ACCESS_KEY = 'fakeSecretAccessKey'; +process.env.AWS_SECURITY_TOKEN = 'testing'; +process.env.AWS_SESSION_TOKEN = 'testing'; +process.env.SOLUTION_VERSION = 'v1.0.0'; +process.env.FINDINGS_TTL_DAYS = '8'; +process.env.WEB_UI_URL = 'https://d1234abcd.cloudfront.net'; +process.env.CSV_EXPORT_BUCKET_NAME = 'test-csv-export-bucket'; +process.env.PRESIGNED_URL_TTL_DAYS = '1'; +process.env.AWS_ACCOUNT_ID = mockAccountId; +process.env.STACK_ID = 'test-stack-id'; diff --git a/source/lambdas/common/__tests__/filterPattern.test.ts b/source/lambdas/common/__tests__/filterPattern.test.ts new file mode 100644 index 00000000..984d9aba --- /dev/null +++ b/source/lambdas/common/__tests__/filterPattern.test.ts @@ -0,0 +1,212 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +describe('Filter Pattern Validation Tests', () => { + describe('Filter Mode Pattern Validation', () => { + it('should accept valid filter modes', () => { + const validModes = ['Include', 'Exclude', 'Disabled']; + + validModes.forEach((mode) => { + expect(['Include', 'Exclude', 'Disabled'].includes(mode)).toBe(true); + }); + }); + + it('should reject invalid filter modes', () => { + const invalidModes = ['include', 'INCLUDE', 'InvalidMode', '', 'enabled', 'disabled', 'exclude']; + + invalidModes.forEach((mode) => { + expect(['Include', 'Exclude', 'Disabled'].includes(mode)).toBe(false); + }); + }); + }); + + describe('Account ID Pattern Validation', () => { + it('should accept valid account IDs', () => { + const validAccountIds = [ + '123456789012', + '234567890123', + '123456789012,234567890123', + '123456789012, 234567890123', + '123456789012 , 234567890123 , 345678901234', + ]; + + validAccountIds.forEach((accountIdString) => { + const accountIds = accountIdString + .split(',') + .map((id) => id.trim()) + .filter((id) => id); + accountIds.forEach((accountId) => { + // Valid account ID should be 12 digits + expect(accountId).toMatch(/^\d{12}$/); + }); + }); + }); + + it('should reject invalid account IDs', () => { + const invalidAccountIds = [ + '12345678901', // Too short (11 digits) + '1234567890123', // Too long (13 digits) + 'abc123456789', // Contains letters + '123-456-789', // Contains hyphens + '', // Empty string + '123456789012,', // Trailing comma + ',123456789012', // Leading comma + '123456789012,,234567890123', // Double comma + ]; + + invalidAccountIds.forEach((accountIdString) => { + // Check for malformed comma patterns first + const hasMalformedCommas = /^,|,$|,,/.test(accountIdString); + // Check individual values after filtering + const accountIds = accountIdString + .split(',') + .map((id) => id.trim()) + .filter((id) => id); + const hasInvalidId = accountIds.some((accountId) => !/^\d{12}$/.test(accountId)); + expect(hasMalformedCommas || hasInvalidId || accountIds.length === 0).toBe(true); + }); + }); + }); + + describe('OU ID Pattern Validation', () => { + it('should accept valid OU IDs', () => { + const validOuIds = [ + 'ou-1234567890', + 'ou-abcd123456', + 'ou-1234567890,ou-abcd123456', + 'ou-1234567890, ou-abcd123456', + 'ou-root-123456789, ou-1234567890abcd', + ]; + + validOuIds.forEach((ouIdString) => { + const ouIds = ouIdString + .split(',') + .map((ou) => ou.trim()) + .filter((ou) => ou); + ouIds.forEach((ouId) => { + // Valid OU ID should start with 'ou-' followed by alphanumeric characters + expect(ouId).toMatch(/^ou(-root)?-[a-zA-Z0-9]+$/); + }); + }); + }); + + it('should reject invalid OU IDs', () => { + const invalidOuIds = [ + 'o-1234567890', // Wrong prefix + 'ou1234567890', // Missing dash + 'OU-1234567890', // Wrong case + 'ou-', // No ID part + '', // Empty string + 'ou-123,', // Trailing comma + ',ou-123', // Leading comma + 'ou-123,,ou-456', // Double comma + ]; + + invalidOuIds.forEach((ouIdString) => { + // Check for malformed comma patterns first + const hasMalformedCommas = /^,|,$|,,/.test(ouIdString); + // Check individual values after filtering + const ouIds = ouIdString + .split(',') + .map((ou) => ou.trim()) + .filter((ou) => ou); + const hasInvalidId = ouIds.some((ouId) => !/^ou(-root)?-[a-zA-Z0-9]+$/.test(ouId)); + expect(hasMalformedCommas || hasInvalidId || ouIds.length === 0).toBe(true); + }); + }); + }); + + describe('Tag Key Pattern Validation', () => { + it('should accept valid tag keys', () => { + const validTagKeys = [ + 'Environment', + 'Project', + 'CostCenter', + 'Environment,Project', + 'Environment, Project', + 'Environment , Project , CostCenter', + 'env-type', + 'project_name', + 'aws:cloudformation:stack-name', + ]; + + validTagKeys.forEach((tagKeyString) => { + const tagKeys = tagKeyString + .split(',') + .map((tag) => tag.trim()) + .filter((tag) => tag); + tagKeys.forEach((tagKey) => { + // Valid tag key should be non-empty and contain valid characters + expect(tagKey.length).toBeGreaterThan(0); + expect(tagKey.length).toBeLessThanOrEqual(128); + // AWS tag keys can contain letters, numbers, spaces, and some special characters + expect(tagKey).toMatch(/^[a-zA-Z0-9\s+\-=._:/]+$/); + }); + }); + }); + + it('should reject invalid tag keys', () => { + const invalidTagKeys = [ + '', // Empty string + 'Environment,', // Trailing comma + ',Environment', // Leading comma + 'Env,,Project', // Double comma + 'x'.repeat(129), // Too long (>128 characters) + 'tag + + diff --git a/source/webui/package-lock.json b/source/webui/package-lock.json new file mode 100644 index 00000000..03d67043 --- /dev/null +++ b/source/webui/package-lock.json @@ -0,0 +1,9998 @@ +{ + "name": "webui", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webui", + "dependencies": { + "@aws-amplify/ui-react": "6.11.2", + "@cloudscape-design/components": "3.0.1048", + "@reduxjs/toolkit": "2.8.2", + "@types/node": "24.2.0", + "@types/react": "18.3.23", + "@types/react-dom": "18.3.7", + "aws-amplify": "6.15.5", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-redux": "9.2.0", + "react-router-dom": "7.8.1", + "zod": "3.25.76" + }, + "devDependencies": { + "@testing-library/jest-dom": "6.7.0", + "@testing-library/react": "16.3.0", + "@testing-library/user-event": "14.6.1", + "@types/react": "18.3.23", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react-swc": "3.11.0", + "@vitest/coverage-v8": "3.2.4", + "eslint": "9.33.0", + "jsdom": "26.1.0", + "msw": "2.10.5", + "prettier": "3.6.2", + "typescript": "5.9.2", + "vite": "7.1.11", + "vitest": "3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@aws-amplify/analytics": { + "version": "7.0.86", + "resolved": "https://registry.npmjs.org/@aws-amplify/analytics/-/analytics-7.0.86.tgz", + "integrity": "sha512-1CEaP6hA1kdBcDbXOg0MFw7Kpwu6Lt93S7r0/Oy/vu9bR3AzKPe/muT8euljmCFl/bY16zqZvY6vUW+WeA0jIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-firehose": "3.621.0", + "@aws-sdk/client-kinesis": "3.621.0", + "@aws-sdk/client-personalize-events": "3.621.0", + "@smithy/util-utf8": "2.0.0", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/analytics/node_modules/@smithy/util-utf8": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.0.tgz", + "integrity": "sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/api": { + "version": "6.3.17", + "resolved": "https://registry.npmjs.org/@aws-amplify/api/-/api-6.3.17.tgz", + "integrity": "sha512-r7nmL7F8w60CAaSoOlX0YAUPCtxfHflhObe4XXGsAmXipcRi8xah/+ybGmwVLDp4J40dLir4bUaA84A/BW3doA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/api-graphql": "4.7.21", + "@aws-amplify/api-rest": "4.3.0", + "@aws-amplify/data-schema": "^1.7.0", + "rxjs": "^7.8.1", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/api-graphql": { + "version": "4.7.21", + "resolved": "https://registry.npmjs.org/@aws-amplify/api-graphql/-/api-graphql-4.7.21.tgz", + "integrity": "sha512-rSfXtFyCJQeevDGE7TbDgH2xQKvukc+zO+Q9YinevPkGpL9zY4kl9Y5BfxKB/vPCvxR2DQTjeNu4fMlsjt9Zgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/api-rest": "4.3.0", + "@aws-amplify/core": "6.13.1", + "@aws-amplify/data-schema": "^1.7.0", + "@aws-sdk/types": "3.387.0", + "graphql": "15.8.0", + "rxjs": "^7.8.1", + "tslib": "^2.5.0", + "uuid": "^11.0.0" + } + }, + "node_modules/@aws-amplify/api-graphql/node_modules/@aws-sdk/types": { + "version": "3.387.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.387.0.tgz", + "integrity": "sha512-YTjFabNwjTF+6yl88f0/tWff018qmmgMmjlw45s6sdVKueWxdxV68U7gepNLF2nhaQPZa6FDOBoA51NaviVs0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^2.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-amplify/api-rest": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/api-rest/-/api-rest-4.3.0.tgz", + "integrity": "sha512-gU8/uFOM5iwMN/FJV6UHSMij3tXQXO/cdtQsTuw5XfcwIeauAIefu5MUY6lTyJbIUny8IRbmjs8DjCUsms13Dw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/auth": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/auth/-/auth-6.15.0.tgz", + "integrity": "sha512-VahO4aN/jxufP225bpWBfX4LCK9lram3mSNQsCSs2hJdDnEKvFdpd90cQz6XXRybyYOmkhjNeAgGp4qLwv5BHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "5.2.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/auth/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-amplify/core": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@aws-amplify/core/-/core-6.13.1.tgz", + "integrity": "sha512-PtSGhC7pv+xdr4Ozy6jzeZraB/kjBZiK9tOLOaBH5Trt0pBuLbh2mneE7a72xHGf64XJbl7v3MWPaVscb8KXkw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/types": "3.398.0", + "@smithy/util-hex-encoding": "2.0.0", + "@types/uuid": "^9.0.0", + "js-cookie": "^3.0.5", + "rxjs": "^7.8.1", + "tslib": "^2.5.0", + "uuid": "^11.0.0" + } + }, + "node_modules/@aws-amplify/core/node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/@aws-amplify/data-schema": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-1.21.1.tgz", + "integrity": "sha512-ZR7zHcjW9NKlCI39F03Ou/q//fobYNRe0w++3Ne75FU2eGGpi7MCIYEP5Hghued/PZkAuarF5dRt79aQt76V8w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/data-schema-types": "*", + "@smithy/util-base64": "^3.0.0", + "@types/aws-lambda": "^8.10.134", + "@types/json-schema": "^7.0.15", + "rxjs": "^7.8.1" + } + }, + "node_modules/@aws-amplify/data-schema-types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-1.2.0.tgz", + "integrity": "sha512-1hy2r7jl3hQ5J/CGjhmPhFPcdGSakfme1ZLjlTMJZILfYifZLSlGRKNCelMb3J5N9203hyeT5XDi5yR47JL1TQ==", + "license": "Apache-2.0", + "dependencies": { + "graphql": "15.8.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/@aws-amplify/datastore": { + "version": "5.0.88", + "resolved": "https://registry.npmjs.org/@aws-amplify/datastore/-/datastore-5.0.88.tgz", + "integrity": "sha512-kpmat3af05QBe488pv3Zo07iFVZjA9SkIa3ZJTb8OQYZw+iV717G94fnHnpGrt7+RyyMkxLsxN8NKbEsta9Xcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/api": "6.3.17", + "@aws-amplify/api-graphql": "4.7.21", + "buffer": "4.9.2", + "idb": "5.0.6", + "immer": "9.0.6", + "rxjs": "^7.8.1", + "ulid": "^2.3.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/datastore/node_modules/immer": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz", + "integrity": "sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@aws-amplify/notifications": { + "version": "2.0.86", + "resolved": "https://registry.npmjs.org/@aws-amplify/notifications/-/notifications-2.0.86.tgz", + "integrity": "sha512-OEf1JGSC8r8EtqORRgpE6aG67TjhV5sWyEM/DakXu8ZqYgErqfrSxiVjLsutIz8LJA4tvjGUFgLheZ3IjEtE8Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.398.0", + "lodash": "^4.17.21", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/storage": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@aws-amplify/storage/-/storage-6.9.5.tgz", + "integrity": "sha512-wUKg/gffDRdJ3P08HQ7HTTfmB8+WBS8lPxXAzMk67LD4yaFNfIrzQCExTO5uz6at1rmZeN4eHvQicxcwCR9m1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.398.0", + "@smithy/md5-js": "2.0.7", + "buffer": "4.9.2", + "crc-32": "1.2.2", + "fast-xml-parser": "^4.4.1", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@aws-amplify/core": "^6.1.0" + } + }, + "node_modules/@aws-amplify/ui": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@aws-amplify/ui/-/ui-6.10.3.tgz", + "integrity": "sha512-dWi2W6TrGMPFW5uvDUHpch2Z28m683KsPbFt2E+a4UQp05GP5qXgCa+jIkxVZmoPop3//udfRxwszGi0MQWX9Q==", + "license": "Apache-2.0", + "dependencies": { + "csstype": "^3.1.1", + "lodash": "4.17.21", + "tslib": "^2.5.2" + }, + "peerDependencies": { + "@aws-amplify/core": "*", + "aws-amplify": "^6.14.3", + "xstate": "^4.33.6" + }, + "peerDependenciesMeta": { + "xstate": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/ui-react": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@aws-amplify/ui-react/-/ui-react-6.11.2.tgz", + "integrity": "sha512-LNr4jTjupphiSfInk+vBY81JruoLm/ReOaOuPye0XIT6rPFV24bhm/nphIACJnD0LWhc9VMpv/BahVZKUi4eHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/ui": "6.10.3", + "@aws-amplify/ui-react-core": "3.4.3", + "@radix-ui/react-direction": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.10", + "@radix-ui/react-slider": "^1.3.2", + "@xstate/react": "^3.2.2", + "lodash": "4.17.21", + "qrcode": "1.5.0", + "tslib": "^2.5.2" + }, + "peerDependencies": { + "@aws-amplify/core": "*", + "aws-amplify": "^6.14.3", + "react": "^16.14.0 || ^17.0 || ^18.0 || ^19", + "react-dom": "^16.14 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "aws-amplify": { + "optional": true + } + } + }, + "node_modules/@aws-amplify/ui-react-core": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@aws-amplify/ui-react-core/-/ui-react-core-3.4.3.tgz", + "integrity": "sha512-B0ODGYbCy2nOcm7Ni/3cv+qGVtDkiYvsnojtaN2DyjXJVE33cGRyqsbwhoaWSetUoNGkN2C6Q/twJPHwOq6a2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-amplify/ui": "6.10.3", + "@xstate/react": "^3.2.2", + "lodash": "4.17.21", + "react-hook-form": "^7.53.2", + "xstate": "^4.33.6" + }, + "peerDependencies": { + "aws-amplify": "^6.14.3", + "react": "^16.14 || ^17 || ^18 || ^19" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "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/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==", + "license": "Apache-2.0", + "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-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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-firehose": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-firehose/-/client-firehose-3.621.0.tgz", + "integrity": "sha512-XAjAkXdb35PDvBYph609Fxn4g00HYH/U6N4+KjF9gLQrdTU+wkjf3D9YD02DZNbApJVcu4eIxWh/8M25YkW02A==", + "license": "Apache-2.0", + "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-firehose/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kinesis": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kinesis/-/client-kinesis-3.621.0.tgz", + "integrity": "sha512-53Omt/beFmTQPjQNpMuPMk5nMzYVsXCRiO+MeqygZEKYG1fWw/UGluCWVbi7WjClOHacsW8lQcsqIRvkPDFNag==", + "license": "Apache-2.0", + "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/eventstream-serde-browser": "^3.0.5", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.4", + "@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", + "@smithy/util-waiter": "^3.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kinesis/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kinesis/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kinesis/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kinesis/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-kinesis/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-personalize-events": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-personalize-events/-/client-personalize-events-3.621.0.tgz", + "integrity": "sha512-qkVkqYvOe3WVuVNL/gRITGYFfHJCx2ijGFK7H3hNUJH3P4AwskmouAd1pWf+3cbGedRnj2is7iw7E602LeJIHA==", + "license": "Apache-2.0", + "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-personalize-events/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-personalize-events/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-personalize-events/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-personalize-events/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-personalize-events/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "peer": true, + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "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==", + "license": "Apache-2.0", + "peer": 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/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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sts/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "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==", + "license": "Apache-2.0", + "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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/core/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" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/token-providers/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.398.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.398.0.tgz", + "integrity": "sha512-r44fkS+vsEgKCuEuTV+TIk0t0m5ZlXHNjSDYEUvzLStbbfUFiNus/YG4UCa0wOk9R7VuQI67badsvvPeVPCGDQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^2.2.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/@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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-node/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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, + "license": "ISC", + "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", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@bundled-es-modules/tough-cookie/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", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@cloudscape-design/collection-hooks": { + "version": "1.0.74", + "resolved": "https://registry.npmjs.org/@cloudscape-design/collection-hooks/-/collection-hooks-1.0.74.tgz", + "integrity": "sha512-yAcD7vjFqbwqMCamUcKRXp403u8RcmC9izyPEYiWod9elt7x0GT1ypPyo9ZRyQuFrBsv2nwubBUrChcYaWooZw==", + "license": "Apache-2.0", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@cloudscape-design/component-toolkit": { + "version": "1.0.0-beta.113", + "resolved": "https://registry.npmjs.org/@cloudscape-design/component-toolkit/-/component-toolkit-1.0.0-beta.113.tgz", + "integrity": "sha512-NIv2EuWDV8GislPUxn2EoNjfFFntZIsZjSLG3fIRptsq3n9hg+8VcGPZthtvCf7YEJzxAFPFxISDbc4FroqaEg==", + "license": "Apache-2.0", + "dependencies": { + "@juggle/resize-observer": "^3.3.1", + "tslib": "^2.3.1" + } + }, + "node_modules/@cloudscape-design/components": { + "version": "3.0.1048", + "resolved": "https://registry.npmjs.org/@cloudscape-design/components/-/components-3.0.1048.tgz", + "integrity": "sha512-nF9IJPJKCa0pRRBgHC9mKDDfBCiljYkx5D4PcG7zSSpTzUItO9lu6kyE5lv71pJpHhVYIxnjkvrKUC/ddR09IA==", + "license": "Apache-2.0", + "dependencies": { + "@cloudscape-design/collection-hooks": "^1.0.0", + "@cloudscape-design/component-toolkit": "^1.0.0-beta", + "@cloudscape-design/test-utils-core": "^1.0.0", + "@cloudscape-design/theming-runtime": "^1.0.0", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", + "@juggle/resize-observer": "^3.3.1", + "ace-builds": "^1.34.0", + "balanced-match": "^1.0.2", + "clsx": "^1.1.0", + "d3-shape": "^1.3.7", + "date-fns": "^2.25.0", + "intl-messageformat": "^10.3.1", + "mnth": "^2.0.0", + "react-keyed-flatten-children": "^2.2.1", + "react-transition-group": "^4.4.2", + "tslib": "^2.4.0", + "weekstart": "^1.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@cloudscape-design/components/node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/@cloudscape-design/test-utils-core": { + "version": "1.0.62", + "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-core/-/test-utils-core-1.0.62.tgz", + "integrity": "sha512-sLmUZ0cDucdW6sPJvNLNvCVr7PsV6jLYg7OKnRL+4xwKb92Z/hduW/pyHQFDXZu7doEjayej2DYG3gAOwHdpTw==", + "license": "Apache-2.0", + "dependencies": { + "css-selector-tokenizer": "^0.8.0", + "css.escape": "^1.5.1" + } + }, + "node_modules/@cloudscape-design/theming-runtime": { + "version": "1.0.82", + "resolved": "https://registry.npmjs.org/@cloudscape-design/theming-runtime/-/theming-runtime-1.0.82.tgz", + "integrity": "sha512-YNpr4JZ5tJWjAcfH1JKAup2mZvIeA9YgPfaDpAE3DuD1sgaELb9yGGR+pMc2xWZMO2OEK3BPdZfLiXEWFaIBRg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", + "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.1", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", + "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/icu-skeleton-parser": "1.8.14", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", + "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.15.tgz", + "integrity": "sha512-SwHMGa8Z47LawQN0rog0sT+6JpiL0B7eW9p1Bb7iCeKDGTI5Ez25TSc2l8kw52VV7hA4sX/C78CGkMrKXfuspA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": 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-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?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.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "license": "Apache-2.0" + }, + "node_modules/@mswjs/interceptors": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.6.tgz", + "integrity": "sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==", + "dev": true, + "license": "MIT", + "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/@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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.3.tgz", + "integrity": "sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.3.tgz", + "integrity": "sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.3.tgz", + "integrity": "sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.3.tgz", + "integrity": "sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.3.tgz", + "integrity": "sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.3.tgz", + "integrity": "sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.3.tgz", + "integrity": "sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.3.tgz", + "integrity": "sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.3.tgz", + "integrity": "sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.3.tgz", + "integrity": "sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.3.tgz", + "integrity": "sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.3.tgz", + "integrity": "sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.3.tgz", + "integrity": "sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.3.tgz", + "integrity": "sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.3.tgz", + "integrity": "sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.3.tgz", + "integrity": "sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.3.tgz", + "integrity": "sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.3.tgz", + "integrity": "sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.3.tgz", + "integrity": "sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.3.tgz", + "integrity": "sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@smithy/abort-controller": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.9.tgz", + "integrity": "sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/abort-controller/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.13.tgz", + "integrity": "sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/types": "^3.7.2", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/config-resolver/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.7.tgz", + "integrity": "sha512-8olpW6mKCa0v+ibCjoCzgZHQx1SQmZuW/WkrdZo73wiTprTH6qhmskT60QLFdT9DRa5mXxjz89kQPZ7ZSsoqqg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^3.0.11", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-stream": "^3.3.4", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.8.tgz", + "integrity": "sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/property-provider": "^3.1.11", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.10.tgz", + "integrity": "sha512-323B8YckSbUH0nMIpXn7HZsAVKHYHFUODa8gG9cHo0ySvA1fr5iWaNT+iIL0UCqUzG6QPHA3BSsBtRQou4mMqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.7.2", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/eventstream-codec/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-codec/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.14.tgz", + "integrity": "sha512-kbrt0vjOIihW3V7Cqj1SXQvAI5BR8SnyQYsandva0AOR307cXAc+IhPngxIPslxTLfxwDpNu0HzCAq6g42kCPg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.13", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.11.tgz", + "integrity": "sha512-P2pnEp4n75O+QHjyO7cbw/vsw5l93K/8EWyjNCAAybYwUmj3M+hjSQZ9P5TVdUgEG08ueMAP5R4FkuSkElZ5tQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.13.tgz", + "integrity": "sha512-zqy/9iwbj8Wysmvi7Lq7XFLeDgjRpTbCfwBhJa8WbrylTAHiAu6oQTwdY7iu2lxigbc9YYr9vPv5SzYny5tCXQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.13", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.13.tgz", + "integrity": "sha512-L1Ib66+gg9uTnqp/18Gz4MDpJPKRE44geOjOQ2SVc0eiaO5l255ADziATZgjQjqumC7yPtp1XnjHlF1srcwjKw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.10", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.9.tgz", + "integrity": "sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.4", + "@smithy/querystring-builder": "^3.0.7", + "@smithy/types": "^3.5.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/fetch-http-handler/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.11.tgz", + "integrity": "sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "@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/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-node/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-node/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-node/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.11.tgz", + "integrity": "sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/invalid-dependency/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-2.0.7.tgz", + "integrity": "sha512-2i2BpXF9pI5D1xekqUsgQ/ohv5+H//G9FlawJrkOJskV18PgJ8LiNbLiskMeYt07yAsSTZR7qtlcAaa/GQLWww==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^2.3.1", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.13.tgz", + "integrity": "sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-content-length/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.8.tgz", + "integrity": "sha512-OEJZKVUEhMOqMs3ktrTWp7UvvluMJEvD5XgQwRePSbDg1VvBaL8pX8mwPltFn6wk1GySbcVwwyldL8S+iqnrEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.5.7", + "@smithy/middleware-serde": "^3.0.11", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", + "@smithy/util-middleware": "^3.0.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "3.0.34", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.34.tgz", + "integrity": "sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/protocol-http": "^4.1.8", + "@smithy/service-error-classification": "^3.0.11", + "@smithy/smithy-client": "^3.7.0", + "@smithy/types": "^3.7.2", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry/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" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.11.tgz", + "integrity": "sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-serde/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.11.tgz", + "integrity": "sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-stack/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.12.tgz", + "integrity": "sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-config-provider/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.3.tgz", + "integrity": "sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.9", + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-http-handler/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.11.tgz", + "integrity": "sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/property-provider/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz", + "integrity": "sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/protocol-http/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.11.tgz", + "integrity": "sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "@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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.11.tgz", + "integrity": "sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-parser/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz", + "integrity": "sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/service-error-classification/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.12.tgz", + "integrity": "sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.4.tgz", + "integrity": "sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.11", + "@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/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.7.0.tgz", + "integrity": "sha512-9wYrjAZFlqWhgVo3C4y/9kpc68jgiSsKUnsFPzr/MSiRL93+QRDafGTfhhKAb2wsr69Ru87WTiqSfQusSmWipA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.5.7", + "@smithy/middleware-endpoint": "^3.2.8", + "@smithy/middleware-stack": "^3.0.11", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "@smithy/util-stream": "^3.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/smithy-client/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.11.tgz", + "integrity": "sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/url-parser/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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-base64/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-base64/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-base64/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.34", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.34.tgz", + "integrity": "sha512-FumjjF631lR521cX+svMLBj3SwSDh9VdtyynTYDAiBDEf8YPP5xORNXKQ9j0105o5+ARAGnOOP/RqSl40uXddA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.11", + "@smithy/smithy-client": "^3.7.0", + "@smithy/types": "^3.7.2", + "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.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.34", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.34.tgz", + "integrity": "sha512-vN6aHfzW9dVVzkI0wcZoUXvfjkl4CSbM9nE//08lmUMyf00S75uuCpTrqF9uD4bD9eldIXlt53colrlwKAT8Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^3.0.13", + "@smithy/credential-provider-imds": "^3.2.8", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/property-provider": "^3.1.11", + "@smithy/smithy-client": "^3.7.0", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.7.tgz", + "integrity": "sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-endpoints/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz", + "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.11.tgz", + "integrity": "sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-middleware/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz", + "integrity": "sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-retry/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.4.tgz", + "integrity": "sha512-SGhGBG/KupieJvJSZp/rfHHka8BFgj56eek9px4pp7lZbOF+fRiVr4U7A3y3zJD8uGhxq32C5D96HxsTC9BckQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^4.1.3", + "@smithy/node-http-handler": "^3.3.3", + "@smithy/types": "^3.7.2", + "@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/fetch-http-handler": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.3.tgz", + "integrity": "sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-stream/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream/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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream/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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.2.0.tgz", + "integrity": "sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.9", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-waiter/node_modules/@smithy/types": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz", + "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.23" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.3", + "@swc/core-darwin-x64": "1.13.3", + "@swc/core-linux-arm-gnueabihf": "1.13.3", + "@swc/core-linux-arm64-gnu": "1.13.3", + "@swc/core-linux-arm64-musl": "1.13.3", + "@swc/core-linux-x64-gnu": "1.13.3", + "@swc/core-linux-x64-musl": "1.13.3", + "@swc/core-win32-arm64-msvc": "1.13.3", + "@swc/core-win32-ia32-msvc": "1.13.3", + "@swc/core-win32-x64-msvc": "1.13.3" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz", + "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz", + "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz", + "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz", + "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz", + "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz", + "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz", + "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz", + "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz", + "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz", + "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.24.tgz", + "integrity": "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", + "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.152", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.152.tgz", + "integrity": "sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "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.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xstate/react": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz", + "integrity": "sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.2", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@xstate/fsm": "^2.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "xstate": "^4.37.2" + }, + "peerDependenciesMeta": { + "@xstate/fsm": { + "optional": true + }, + "xstate": { + "optional": true + } + } + }, + "node_modules/ace-builds": { + "version": "1.43.2", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.43.2.tgz", + "integrity": "sha512-3wzJUJX0RpMc03jo0V8Q3bSb/cKPnS7Nqqw8fVHsCCHweKMiTIxT3fP46EhjmVy6MCuxwP801ere+RW245phGw==", + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": 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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-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, + "license": "MIT", + "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, + "license": "(MIT OR CC0-1.0)", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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/ast-v8-to-istanbul": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.7.tgz", + "integrity": "sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-amplify": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/aws-amplify/-/aws-amplify-6.15.5.tgz", + "integrity": "sha512-FdH2V4z/mkBkVRBb1Mk9jBxM1ieoW+6kmVSS8V1lLQP4v91ImBa5Kc+BEek+Fo++eNzfgtZD7cLUTEqQzbkvWw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-amplify/analytics": "7.0.86", + "@aws-amplify/api": "6.3.17", + "@aws-amplify/auth": "6.15.0", + "@aws-amplify/core": "6.13.1", + "@aws-amplify/datastore": "5.0.88", + "@aws-amplify/notifications": "2.0.86", + "@aws-amplify/storage": "6.9.5", + "tslib": "^2.5.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", + "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.1.tgz", + "integrity": "sha512-48af6xm9gQK8rhIcOxWwdGzIervm8BVTin+yRp9HEvU20BtVZ2lBywlIJBzwaDtvo0FvjeL7QdCADoUoqIbV3A==", + "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": ">=18" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/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/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, + "license": "ISC", + "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/cliui/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/cliui/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/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "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/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "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/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, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "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/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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, + "license": "Apache-2.0", + "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.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.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, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphql": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", + "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "license": "MIT", + "engines": { + "node": ">= 10.x" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "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, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/idb/-/idb-5.0.6.tgz", + "integrity": "sha512-/PFvOWPzRcEPmlDt5jEvzVZVs0wyd/EvGvkDIcbBpGuMMLQKrTPG0TxvE2UJtgZtCQCmOtM2QD7yQJBVEjKGOw==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.16", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", + "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.2", + "tslib": "^2.8.0" + } + }, + "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, + "license": "MIT", + "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==", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "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-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, + "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/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/mnth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mnth/-/mnth-2.0.0.tgz", + "integrity": "sha512-3ZH4UWBGpAwCKdfjynLQpUDVZWMe6vRHwarIpMdGLUp89CVR9hjzgyWERtMyqx+fPEqQ/PsAxFwvwPxLFxW40A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.0" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "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.10.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.10.5.tgz", + "integrity": "sha512-0EsQCrCI1HbhpBWd89DvmxY6plmvrM96b0sCIztnvcNHQbXn5vqwm1KlXslo6u4wN9LFGLC1WFjjgljcQhe40A==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.39.1", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "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/msw/node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "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, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/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==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/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==", + "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/qrcode/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "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" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-keyed-flatten-children": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-keyed-flatten-children/-/react-keyed-flatten-children-2.2.1.tgz", + "integrity": "sha512-6yBLVO6suN8c/OcJk1mzIrUHdeEzf5rtRVBhxEXAHO49D7SlJ70cG4xrSJrBIAG7MMeQ+H/T151mM2dRDNnFaA==", + "license": "MIT", + "dependencies": { + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/react-keyed-flatten-children/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz", + "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.1.tgz", + "integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "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" + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.46.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.3.tgz", + "integrity": "sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.3", + "@rollup/rollup-android-arm64": "4.46.3", + "@rollup/rollup-darwin-arm64": "4.46.3", + "@rollup/rollup-darwin-x64": "4.46.3", + "@rollup/rollup-freebsd-arm64": "4.46.3", + "@rollup/rollup-freebsd-x64": "4.46.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.3", + "@rollup/rollup-linux-arm-musleabihf": "4.46.3", + "@rollup/rollup-linux-arm64-gnu": "4.46.3", + "@rollup/rollup-linux-arm64-musl": "4.46.3", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.3", + "@rollup/rollup-linux-ppc64-gnu": "4.46.3", + "@rollup/rollup-linux-riscv64-gnu": "4.46.3", + "@rollup/rollup-linux-riscv64-musl": "4.46.3", + "@rollup/rollup-linux-s390x-gnu": "4.46.3", + "@rollup/rollup-linux-x64-gnu": "4.46.3", + "@rollup/rollup-linux-x64-musl": "4.46.3", + "@rollup/rollup-win32-arm64-msvc": "4.46.3", + "@rollup/rollup-win32-ia32-msvc": "4.46.3", + "@rollup/rollup-win32-x64-msvc": "4.46.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/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.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "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, + "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-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "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-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-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "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/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.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ulid": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz", + "integrity": "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==", + "license": "MIT", + "bin": { + "ulid": "bin/cli.js" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, + "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", + "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, + "license": "BSD-2-Clause", + "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", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "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/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/weekstart": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/weekstart/-/weekstart-1.1.0.tgz", + "integrity": "sha512-ZO3I7c7J9nwGN1PZKZeBYAsuwWEsCOZi5T68cQoVNYrzrpp5Br0Bgi0OF4l8kH/Ez7nKfxa5mSsXjsgris3+qg==", + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/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==", + "license": "MIT" + }, + "node_modules/wrap-ansi/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==", + "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xstate": { + "version": "4.38.3", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz", + "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, + "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, + "license": "ISC", + "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/yargs/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/yargs/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/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/source/webui/package.json b/source/webui/package.json new file mode 100644 index 00000000..8f292e85 --- /dev/null +++ b/source/webui/package.json @@ -0,0 +1,51 @@ +{ + "name": "webui", + "private": true, + "type": "module", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com/solutions" + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "format": "prettier --ignore-path ../.gitignore --write \"**/*.+(js|ts|tsx|json)\"", + "preview": "vite preview", + "test:watch": "vitest", + "test": "vitest --run ./src/__tests__/ --coverage" + }, + "dependencies": { + "@aws-amplify/ui-react": "6.11.2", + "@cloudscape-design/components": "3.0.1048", + "@reduxjs/toolkit": "2.8.2", + "@types/node": "24.2.0", + "@types/react": "18.3.23", + "@types/react-dom": "18.3.7", + "aws-amplify": "6.15.5", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-redux": "9.2.0", + "react-router-dom": "7.8.1", + "zod": "3.25.76" + }, + "devDependencies": { + "@testing-library/jest-dom": "6.7.0", + "@testing-library/react": "16.3.0", + "@testing-library/user-event": "14.6.1", + "@types/react": "18.3.23", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react-swc": "3.11.0", + "@vitest/coverage-v8": "3.2.4", + "eslint": "9.33.0", + "jsdom": "26.1.0", + "msw": "2.10.5", + "prettier": "3.6.2", + "typescript": "5.9.2", + "vite": "7.1.11", + "vitest": "3.2.4" + }, + "msw": { + "workerDirectory": "public" + } +} diff --git a/source/webui/public/aws-exports.template.json b/source/webui/public/aws-exports.template.json new file mode 100644 index 00000000..f01ec9f2 --- /dev/null +++ b/source/webui/public/aws-exports.template.json @@ -0,0 +1,27 @@ +{ + "API": { + "endpoints": [ + { + "name": "", + "endpoint": "" + } + ] + }, + "loggingLevel": "INFO", + "Auth": { + "region": "", + "userPoolId": "", + "userPoolWebClientId": "", + "mandatorySignIn": true, + "oauth": { + "domain": "", + "scope": [ + ], + "redirectSignIn": "", + "redirectSignOut": "", + "responseType": "", + "clientId": "" + } + }, + "ticketingEnabled": "false" +} diff --git a/source/webui/public/aws-logo.svg b/source/webui/public/aws-logo.svg new file mode 100644 index 00000000..e7e5b6a6 --- /dev/null +++ b/source/webui/public/aws-logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/source/webui/public/cognito-login-banner.png b/source/webui/public/cognito-login-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..3758a2f4115f85fa200a4a1f05d83a426cd97fdc GIT binary patch literal 6639 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 DT3glC literal 0 HcmV?d00001 diff --git a/source/webui/public/cognito-managed-login-branding.json b/source/webui/public/cognito-managed-login-branding.json new file mode 100644 index 00000000..2e3eff98 --- /dev/null +++ b/source/webui/public/cognito-managed-login-branding.json @@ -0,0 +1,473 @@ +{ + "ManagedLoginBranding": { + "ManagedLoginBrandingId": "cd531850-5375-4592-85bc-1baa4a312e35", + "UserPoolId": "us-east-1_L1xcUdqmw", + "UseCognitoProvidedValues": false, + "Settings": { + "components": { + "secondaryButton": { + "lightMode": { + "hover": { + "backgroundColor": "f2f8fdff", + "borderColor": "033160ff", + "textColor": "033160ff" + }, + "defaults": { + "backgroundColor": "ffffffff", + "borderColor": "0972d3ff", + "textColor": "0972d3ff" + }, + "active": { + "backgroundColor": "d3e7f9ff", + "borderColor": "033160ff", + "textColor": "033160ff" + } + }, + "darkMode": { + "hover": { + "backgroundColor": "192534ff", + "borderColor": "89bdeeff", + "textColor": "89bdeeff" + }, + "defaults": { + "backgroundColor": "0f1b2aff", + "borderColor": "539fe5ff", + "textColor": "539fe5ff" + }, + "active": { + "backgroundColor": "354150ff", + "borderColor": "89bdeeff", + "textColor": "89bdeeff" + } + } + }, + "form": { + "lightMode": { + "backgroundColor": "ffffffff", + "borderColor": "c6c6cdff" + }, + "borderRadius": 15.0, + "backgroundImage": { + "enabled": true + }, + "logo": { + "location": "START", + "position": "TOP", + "enabled": true, + "formInclusion": "IN" + }, + "darkMode": { + "backgroundColor": "0f1b2aff", + "borderColor": "424650ff" + } + }, + "alert": { + "lightMode": { + "error": { + "backgroundColor": "fff7f7ff", + "borderColor": "d91515ff" + } + }, + "borderRadius": 12.0, + "darkMode": { + "error": { + "backgroundColor": "1a0000ff", + "borderColor": "eb6f6fff" + } + } + }, + "favicon": { + "enabledTypes": [ + "ICO", + "SVG" + ] + }, + "pageBackground": { + "image": { + "enabled": true + }, + "lightMode": { + "color": "ffffffff" + }, + "darkMode": { + "color": "0f1b2aff" + } + }, + "pageText": { + "lightMode": { + "bodyColor": "414d5cff", + "headingColor": "000716ff", + "descriptionColor": "414d5cff" + }, + "darkMode": { + "bodyColor": "b6bec9ff", + "headingColor": "d1d5dbff", + "descriptionColor": "b6bec9ff" + } + }, + "phoneNumberSelector": { + "displayType": "TEXT" + }, + "primaryButton": { + "lightMode": { + "hover": { + "backgroundColor": "033160ff", + "textColor": "ffffffff" + }, + "defaults": { + "backgroundColor": "0972d3ff", + "textColor": "ffffffff" + }, + "active": { + "backgroundColor": "033160ff", + "textColor": "ffffffff" + }, + "disabled": { + "backgroundColor": "ffffffff", + "borderColor": "ffffffff" + } + }, + "darkMode": { + "hover": { + "backgroundColor": "89bdeeff", + "textColor": "000716ff" + }, + "defaults": { + "backgroundColor": "539fe5ff", + "textColor": "000716ff" + }, + "active": { + "backgroundColor": "539fe5ff", + "textColor": "000716ff" + }, + "disabled": { + "backgroundColor": "ffffffff", + "borderColor": "ffffffff" + } + } + }, + "pageFooter": { + "lightMode": { + "borderColor": "d5dbdbff", + "background": { + "color": "fafafaff" + } + }, + "backgroundImage": { + "enabled": false + }, + "logo": { + "location": "START", + "enabled": false + }, + "darkMode": { + "borderColor": "424650ff", + "background": { + "color": "0f141aff" + } + } + }, + "pageHeader": { + "lightMode": { + "borderColor": "d5dbdbff", + "background": { + "color": "fafafaff" + } + }, + "backgroundImage": { + "enabled": false + }, + "logo": { + "location": "START", + "enabled": false + }, + "darkMode": { + "borderColor": "424650ff", + "background": { + "color": "0f141aff" + } + } + }, + "idpButton": { + "standard": { + "lightMode": { + "hover": { + "backgroundColor": "f2f8fdff", + "borderColor": "033160ff", + "textColor": "033160ff" + }, + "defaults": { + "backgroundColor": "ffffffff", + "borderColor": "424650ff", + "textColor": "424650ff" + }, + "active": { + "backgroundColor": "d3e7f9ff", + "borderColor": "033160ff", + "textColor": "033160ff" + } + }, + "darkMode": { + "hover": { + "backgroundColor": "192534ff", + "borderColor": "89bdeeff", + "textColor": "89bdeeff" + }, + "defaults": { + "backgroundColor": "0f1b2aff", + "borderColor": "c6c6cdff", + "textColor": "c6c6cdff" + }, + "active": { + "backgroundColor": "354150ff", + "borderColor": "89bdeeff", + "textColor": "89bdeeff" + } + } + }, + "custom": {} + } + }, + "componentClasses": { + "dropDown": { + "lightMode": { + "hover": { + "itemBackgroundColor": "f4f4f4ff", + "itemBorderColor": "7d8998ff", + "itemTextColor": "000716ff" + }, + "defaults": { + "itemBackgroundColor": "ffffffff" + }, + "match": { + "itemBackgroundColor": "414d5cff", + "itemTextColor": "0972d3ff" + } + }, + "borderRadius": 8.0, + "darkMode": { + "hover": { + "itemBackgroundColor": "081120ff", + "itemBorderColor": "5f6b7aff", + "itemTextColor": "e9ebedff" + }, + "defaults": { + "itemBackgroundColor": "192534ff" + }, + "match": { + "itemBackgroundColor": "d1d5dbff", + "itemTextColor": "89bdeeff" + } + } + }, + "input": { + "lightMode": { + "defaults": { + "backgroundColor": "ffffffff", + "borderColor": "7d8998ff" + }, + "placeholderColor": "5f6b7aff" + }, + "borderRadius": 8.0, + "darkMode": { + "defaults": { + "backgroundColor": "0f1b2aff", + "borderColor": "5f6b7aff" + }, + "placeholderColor": "8d99a8ff" + } + }, + "inputDescription": { + "lightMode": { + "textColor": "5f6b7aff" + }, + "darkMode": { + "textColor": "8d99a8ff" + } + }, + "buttons": { + "borderRadius": 8.0 + }, + "optionControls": { + "lightMode": { + "defaults": { + "backgroundColor": "ffffffff", + "borderColor": "7d8998ff" + }, + "selected": { + "backgroundColor": "0972d3ff", + "foregroundColor": "ffffffff" + } + }, + "darkMode": { + "defaults": { + "backgroundColor": "0f1b2aff", + "borderColor": "7d8998ff" + }, + "selected": { + "backgroundColor": "539fe5ff", + "foregroundColor": "000716ff" + } + } + }, + "statusIndicator": { + "lightMode": { + "success": { + "backgroundColor": "f2fcf3ff", + "borderColor": "037f0cff", + "indicatorColor": "037f0cff" + }, + "pending": { + "indicatorColor": "AAAAAAAA" + }, + "warning": { + "backgroundColor": "fffce9ff", + "borderColor": "8d6605ff", + "indicatorColor": "8d6605ff" + }, + "error": { + "backgroundColor": "fff7f7ff", + "borderColor": "d91515ff", + "indicatorColor": "d91515ff" + } + }, + "darkMode": { + "success": { + "backgroundColor": "001a02ff", + "borderColor": "29ad32ff", + "indicatorColor": "29ad32ff" + }, + "pending": { + "indicatorColor": "AAAAAAAA" + }, + "warning": { + "backgroundColor": "1d1906ff", + "borderColor": "e0ca57ff", + "indicatorColor": "e0ca57ff" + }, + "error": { + "backgroundColor": "1a0000ff", + "borderColor": "eb6f6fff", + "indicatorColor": "eb6f6fff" + } + } + }, + "divider": { + "lightMode": { + "borderColor": "ebebf0ff" + }, + "darkMode": { + "borderColor": "232b37ff" + } + }, + "idpButtons": { + "icons": { + "enabled": true + } + }, + "focusState": { + "lightMode": { + "borderColor": "0972d3ff" + }, + "darkMode": { + "borderColor": "539fe5ff" + } + }, + "inputLabel": { + "lightMode": { + "textColor": "000716ff" + }, + "darkMode": { + "textColor": "d1d5dbff" + } + }, + "link": { + "lightMode": { + "hover": { + "textColor": "033160ff" + }, + "defaults": { + "textColor": "0972d3ff" + } + }, + "darkMode": { + "hover": { + "textColor": "89bdeeff" + }, + "defaults": { + "textColor": "539fe5ff" + } + } + } + }, + "categories": { + "form": { + "sessionTimerDisplay": "NONE", + "instructions": { + "enabled": false + }, + "languageSelector": { + "enabled": false + }, + "displayGraphics": true, + "location": { + "horizontal": "CENTER", + "vertical": "CENTER" + } + }, + "auth": { + "federation": { + "interfaceStyle": "BUTTON_LIST", + "order": [] + }, + "authMethodOrder": [ + [ + { + "display": "BUTTON", + "type": "FEDERATED" + }, + { + "display": "INPUT", + "type": "USERNAME_PASSWORD" + } + ] + ] + }, + "global": { + "colorSchemeMode": "LIGHT", + "pageHeader": { + "enabled": false + }, + "pageFooter": { + "enabled": false + }, + "spacingDensity": "REGULAR" + }, + "signUp": { + "acceptanceElements": [ + { + "enforcement": "NONE", + "textKey": "en" + } + ] + } + } + }, + "Assets": [ + { + "Category": "FORM_LOGO", + "ColorMode": "DYNAMIC", + "Extension": "PNG", + "Bytes": "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" + }, + { + "Category": "FORM_LOGO", + "ColorMode": "LIGHT", + "Extension": "PNG", + "Bytes": "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" + } + ], + "CreationDate": "2025-10-15T16:09:23.221000-04:00", + "LastModifiedDate": "2025-10-15T16:52:04.051000-04:00" + } +} diff --git a/source/webui/public/favicon.ico b/source/webui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1141f0a685c6a1436d7b5f115fd2e6bf77933a07 GIT binary patch literal 1150 zcmdUtElUJZ6o%i`1&xcrCTv@SMOH*qFo{Jlt;Mh+2wTinCkXxoPZJG-O%O3IX!bW~ zv?wNvaD490I3AQW>|$_+XU^w)?w!k(D!jFt_}0~_N|TbNffSiMkNNxO*uT~=Z_B(6 z4cI9Q7J5=L#^x>JYw&_5b~(g1_?tgwnV*NAPi?Yu5cRQk7Un$vqF?M&d-^B(g(3Gp zjDDWAmj+n>5=|?d?-udZw>k0rU#MNcdQW3s#ovWn=)!iGwMKjbk40}@GM^&15BCu7 zN%Zfi?ZHH$t@LcV_vYiU0jKZ=QJ4HzFj$4OzkJ>JjXCofdJZ9jM%fm7%H-eW;u@u1VSt(m|nE literal 0 HcmV?d00001 diff --git a/source/webui/public/logo.png b/source/webui/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1141f0a685c6a1436d7b5f115fd2e6bf77933a07 GIT binary patch literal 1150 zcmdUtElUJZ6o%i`1&xcrCTv@SMOH*qFo{Jlt;Mh+2wTinCkXxoPZJG-O%O3IX!bW~ zv?wNvaD490I3AQW>|$_+XU^w)?w!k(D!jFt_}0~_N|TbNffSiMkNNxO*uT~=Z_B(6 z4cI9Q7J5=L#^x>JYw&_5b~(g1_?tgwnV*NAPi?Yu5cRQk7Un$vqF?M&d-^B(g(3Gp zjDDWAmj+n>5=|?d?-udZw>k0rU#MNcdQW3s#ovWn=)!iGwMKjbk40}@GM^&15BCu7 zN%Zfi?ZHH$t@LcV_vYiU0jKZ=QJ4HzFj$4OzkJ>JjXCofdJZ9jM%fm7%H-eW;u@u1VSt(m|nE literal 0 HcmV?d00001 diff --git a/source/webui/public/manifest.json b/source/webui/public/manifest.json new file mode 100644 index 00000000..e5b8c80a --- /dev/null +++ b/source/webui/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "ASR", + "name": "Automated Security Response on AWS", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/source/webui/public/mockServiceWorker.js b/source/webui/public/mockServiceWorker.js new file mode 100644 index 00000000..723b0714 --- /dev/null +++ b/source/webui/public/mockServiceWorker.js @@ -0,0 +1,344 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.10.5' +const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + */ +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @returns {Promise} + */ +async function getResponse(event, client, requestId) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/source/webui/src/App.tsx b/source/webui/src/App.tsx new file mode 100644 index 00000000..08a1f41b --- /dev/null +++ b/source/webui/src/App.tsx @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import '@aws-amplify/ui-react/styles.css'; +import { useContext } from 'react'; +import { AppRoutes } from './AppRoutes.tsx'; +import { useDispatch } from 'react-redux'; +import { UserContext } from './contexts/UserContext.tsx'; +import { Spinner } from '@cloudscape-design/components'; +import { useLocation } from 'react-router-dom'; + +export const AppComponent = () => { + const dispatch = useDispatch(); + const { user } = useContext(UserContext); + const location = useLocation(); + + /** + * Load base data here that should be available on app start up for all pages. + * Other data will only load once the user navigates to pages that require it. + */ + + // Allow callback page to render even without user + if (!user && location.pathname !== '/callback') { + return ( + <> + +

+ + ); + } + + return ; +}; diff --git a/source/webui/src/AppRoutes.tsx b/source/webui/src/AppRoutes.tsx new file mode 100644 index 00000000..5e40ba89 --- /dev/null +++ b/source/webui/src/AppRoutes.tsx @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Route, Routes, Navigate } from 'react-router-dom'; +import Layout from './Layout.tsx'; +import { Container, ContentLayout, Header } from '@cloudscape-design/components'; +import { FindingsOverviewPage } from './pages/findings/FindingsOverviewPage.tsx'; +import { RemediationHistoryOverviewPage } from './pages/history/RemediationHistoryOverviewPage.tsx'; +import { UsersOverviewPage } from './pages/users/UsersOverviewPage.tsx'; +import { InviteUsersPage } from './pages/users/invite/InviteUsersPage.tsx'; +import { ProtectedRoute } from './components/ProtectedRoute.tsx'; +import { CallbackPage } from './pages/callback/CallbackPage.tsx'; + +export const AppRoutes = () => ( + + } /> + }> + } /> + } /> + } /> + + + + } /> + + + + } /> + {/*} />*/} + {/*} />*/} + {/*} />*/} + + {/* Add more child routes that use the same Layout here */} + + Error}> + Page not found 😿}> + + } + /> + + + {/* Add another set of routes with a different layout here */} + +); diff --git a/source/webui/src/Layout.tsx b/source/webui/src/Layout.tsx new file mode 100644 index 00000000..27a3f427 --- /dev/null +++ b/source/webui/src/Layout.tsx @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useContext } from 'react'; +import { AppLayout, Flashbar } from '@cloudscape-design/components'; +import SideNavigationBar from './components/navigation/SideNavigationBar.tsx'; +import { NotificationContext } from './contexts/NotificationContext.tsx'; +import { Outlet } from 'react-router-dom'; +import { Breadcrumbs } from './components/navigation/Breadcrumbs.tsx'; +import TopNavigationBar from './components/navigation/TopNavigationBar.tsx'; + +export default function Layout() { + const { notifications } = useContext(NotificationContext); + + return ( + <> +
+ +
+
+ + +
+ } + contentType={'dashboard'} + breadcrumbs={} + navigation={} + notifications={} + stickyNotifications={true} + toolsHide={true} + ariaLabels={{ + navigation: 'Navigation drawer', + navigationClose: 'Close navigation drawer', + navigationToggle: 'Open navigation drawer', + notifications: 'Notifications', + }} + /> + + + ); +} diff --git a/source/webui/src/__tests__/App.test.tsx b/source/webui/src/__tests__/App.test.tsx new file mode 100644 index 00000000..8c25144e --- /dev/null +++ b/source/webui/src/__tests__/App.test.tsx @@ -0,0 +1,350 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { AuthUser } from 'aws-amplify/auth'; +import { http, HttpResponse } from 'msw'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { AppComponent } from '../App.tsx'; +import { NotificationContext, NotificationContextProvider } from '../contexts/NotificationContext.tsx'; +import { ConfigContextProvider } from '../contexts/ConfigContext.tsx'; +import { UserContext } from '../contexts/UserContext.tsx'; +import { ApiEndpoints } from '../store/solutionApi.ts'; +import { setupStore } from '../store/store.ts'; +import { MOCK_SERVER_URL, server } from './server.ts'; +import { generateTestFindings } from './test-data-factory.ts'; + +// Mock AWS Amplify styles +vi.mock('@aws-amplify/ui-react/styles.css', () => ({})); + +describe('App Component', () => { + describe('when no user is logged in', () => { + it('should show loading spinner and redirect message', () => { + const store = setupStore(); + + render( + + + Promise.resolve(), + signInWithRedirect: () => Promise.resolve(), + checkUser: () => Promise.resolve(), + }} + > + + + + + + + + , + ); + + const redirectMessage = screen.getByText(/Redirecting to login/i); + expect(redirectMessage).toBeInTheDocument(); + // Check that the spinner component is rendered (it's a CloudScape component) + expect(document.querySelector('.awsui_root_1612d_152xz_183')).toBeInTheDocument(); + }); + + it('should not render the main application content', () => { + const store = setupStore(); + + render( + + + Promise.resolve(), + signInWithRedirect: () => Promise.resolve(), + checkUser: () => Promise.resolve(), + }} + > + + + + + + + + , + ); + + expect(screen.queryByTestId('main-content')).not.toBeInTheDocument(); + expect(screen.queryByText(/Automated Security Response on AWS/i)).not.toBeInTheDocument(); + }); + }); + + describe('when a user is logged in', () => { + const userEmail = 'john.doe@example.com'; + const userContext = { + user: { + username: window.crypto.randomUUID(), + userId: window.crypto.randomUUID(), + } as AuthUser, + email: userEmail, + groups: ['AdminGroup'], + signOut: vi.fn().mockResolvedValue(undefined), + signInWithRedirect: vi.fn().mockResolvedValue(undefined), + checkUser: vi.fn().mockResolvedValue(undefined), + }; + + beforeEach(() => { + // Mock API responses + const findings = generateTestFindings(3); + server.use( + http.get(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, () => { + return HttpResponse.json( + { Findings: findings, NextToken: null }, + { + status: 200, + headers: [['Access-Control-Allow-Origin', '*']], + }, + ); + }), + ); + }); + + const renderAppWithUser = (initialRoute = '/') => { + const store = setupStore(); + return render( + + + + + + + + + + + , + ); + }; + + it('should render complete application layout, navigation, and user interactions', async () => { + // Render the app with a logged-in user + renderAppWithUser(); + + // Verify main application layout + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + expect(screen.queryByText(/Redirecting to login/i)).not.toBeInTheDocument(); + + // AND: Verify top navigation with user menu + const userButton = screen.getByRole('button', { name: userEmail }); + expect(userButton).toBeInTheDocument(); + + // Verify sidebar navigation links + expect(screen.getAllByText(/Automated Security Response on AWS/i)).toHaveLength(2); + expect(screen.getAllByRole('link', { name: /Findings/i })).toHaveLength(2); + expect(screen.getByRole('link', { name: /Execution History/i })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /Invite Users/i })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /View Users/i })).toBeInTheDocument(); + + // Click on user menu + await userEvent.click(userButton); + + // Verify sign out option appears + const signOutButton = await screen.findByRole('menuitem', { name: /Sign Out/i }); + expect(signOutButton).toBeInTheDocument(); + expect(userContext.signOut).not.toHaveBeenCalled(); + + // Click sign out + await userEvent.click(signOutButton); + + // Verify signOut was called + expect(userContext.signOut).toHaveBeenCalled(); + }); + + it('should handle routing and navigation correctly', async () => { + // Render app at root route + renderAppWithUser('/'); + + // Verify introduction page is displayed by default + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + + // Navigate to findings page using the sidebar navigation link + const sidebarFindingsLinks = screen.getAllByRole('link', { name: /Findings/i }); + await userEvent.click(sidebarFindingsLinks[0]); + + // Verify findings page loads + const heading = await screen.findByRole('heading', { name: /Findings to Remediate/i }); + expect(heading).toBeInTheDocument(); + }); + + it('should handle direct navigation and error routes', () => { + // Direct navigation to findings route + renderAppWithUser('/findings'); + + // Verify app renders correctly + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + + // Navigation to unknown route + renderAppWithUser('/unknown-route'); + + // Verify 404 page is displayed + const errorHeading = screen.getByRole('heading', { name: /Error/i }); + const notFoundMessage = screen.getByRole('heading', { name: /Page not found/i }); + expect(errorHeading).toBeInTheDocument(); + expect(notFoundMessage).toBeInTheDocument(); + }); + + it('should handle notifications and user context variations', () => { + // App with notifications + const notificationContext = { + notifications: [ + { + header: 'Remediation in progress', + content: 'A remediation is currently running for finding ABC-123', + type: 'info' as const, + }, + ], + setNotifications: vi.fn(), + }; + + const store = setupStore(); + + render( + + + + + + + + + + + , + ); + + // Verify notifications are displayed + expect(screen.getByText(/Remediation in progress/i)).toBeInTheDocument(); + expect(screen.getByText(/A remediation is currently running for finding ABC-123/i)).toBeInTheDocument(); + + // Verify app renders with notification context + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + }); + + it('should handle user context variations and Redux integration', () => { + // User context with null email + const userContextWithNullEmail = { + ...userContext, + email: null, + }; + + const store = setupStore(); + + render( + + + + + + + + + + + , + ); + + // Verify app renders correctly + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + + // Verify username is displayed when email is not available + const userButton = screen.getByRole('button', { name: userContext.user.username }); + expect(userButton).toBeInTheDocument(); + + // Verify Redux integration works without errors + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + }); + + it('should only show ViewUsers page to AdminGroup and DelegatedAdminGroup users', async () => { + // ARRANGE - Test AdminGroup access + const adminUserContext = { + ...userContext, + groups: ['AdminGroup'], + }; + + const { unmount } = render( + + + + + + + + + + + , + ); + + // ACT & ASSERT - AdminGroup can access users page + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: /Page not found/i })).not.toBeInTheDocument(); + + unmount(); + + // ARRANGE - Test DelegatedAdminGroup access + const delegatedAdminUserContext = { + ...userContext, + groups: ['DelegatedAdminGroup'], + }; + + const { unmount: unmount2 } = render( + + + + + + + + + + + , + ); + + // ACT & ASSERT - DelegatedAdminGroup can access users page + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: /Page not found/i })).not.toBeInTheDocument(); + + unmount2(); + + // ARRANGE - Test AccountOperatorGroup access (should be denied) + const operatorUserContext = { + ...userContext, + groups: ['AccountOperatorGroup'], + }; + + render( + + + + + + + + + + + , + ); + + // ACT & ASSERT - AccountOperatorGroup is redirected to home page + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: /Page not found/i })).not.toBeInTheDocument(); + }); + }); +}); diff --git a/source/webui/src/__tests__/components/navigation/create-breadcrumbs.test.ts b/source/webui/src/__tests__/components/navigation/create-breadcrumbs.test.ts new file mode 100644 index 00000000..9d8f29b8 --- /dev/null +++ b/source/webui/src/__tests__/components/navigation/create-breadcrumbs.test.ts @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { createBreadcrumbs } from '../../../components/navigation/create-breadcrumbs.ts'; + +it('generates the Home breadcrumb for the empty path', () => { + // WHEN + const result = createBreadcrumbs(''); + + // THEN + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ text: 'Home', href: '/findings' }); +}); + +it('generates breadcrumbs for multiple path elements', () => { + // WHEN + const result = createBreadcrumbs('/invite/foo'); + + // THEN + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ text: 'Home', href: '/findings' }); + expect(result[1]).toEqual({ text: 'Invite', href: '/invite' }); + expect(result[2]).toEqual({ text: 'foo', href: '/invite/foo' }); +}); + +it('uses "Details" as label for uuids', () => { + // GIVEN + const findingId = window.crypto.randomUUID(); + + // WHEN + const result = createBreadcrumbs(`/findings/${findingId}`); + + // THEN + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ text: 'Home', href: '/findings' }); + expect(result[1]).toEqual({ text: 'Findings', href: '/findings' }); + expect(result[2]).toEqual({ text: 'Details', href: `/findings/${findingId}` }); +}); diff --git a/source/webui/src/__tests__/contexts/UserContext.test.tsx b/source/webui/src/__tests__/contexts/UserContext.test.tsx new file mode 100644 index 00000000..1ba7513f --- /dev/null +++ b/source/webui/src/__tests__/contexts/UserContext.test.tsx @@ -0,0 +1,422 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { render, screen, waitFor, act } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { + AuthUser, + fetchUserAttributes, + getCurrentUser, + signInWithRedirect, + signOut, + fetchAuthSession, +} from 'aws-amplify/auth'; +import { Hub } from 'aws-amplify/utils'; + +import { UserContext, UserContextProvider } from '../../contexts/UserContext.tsx'; +import { rootReducer } from '../../store/store.ts'; +import { solutionApi } from '../../store/solutionApi.ts'; +import { useContext } from 'react'; + +// Mock AWS Amplify +vi.mock('aws-amplify/auth', () => ({ + getCurrentUser: vi.fn(), + fetchUserAttributes: vi.fn(), + fetchAuthSession: vi.fn(), + signOut: vi.fn(), + signInWithRedirect: vi.fn(), +})); + +vi.mock('aws-amplify/utils', () => ({ + Hub: { + listen: vi.fn(), + }, +})); + +const mockGetCurrentUser = vi.mocked(getCurrentUser); +const mockFetchUserAttributes = vi.mocked(fetchUserAttributes); +const mockFetchAuthSession = vi.mocked(fetchAuthSession); +const mockSignOut = vi.mocked(signOut); +const mockSignInWithRedirect = vi.mocked(signInWithRedirect); +const mockHubListen = vi.mocked(Hub.listen); + +const mockUser: AuthUser = { + username: 'testuser', + userId: 'test-user-id', +} as AuthUser; + +const TestComponent = () => { + const context = useContext(UserContext); + return ( +
+
{context.user?.username || 'null'}
+
{context.email || 'null'}
+
{context.groups?.join(',') || 'null'}
+ + +
+ ); +}; + +const renderWithProvider = () => { + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(solutionApi.middleware), + }); + + return render( + + + + + , + ); +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('UserContext', () => { + it('initializes with default values', () => { + // ARRANGE + mockGetCurrentUser.mockRejectedValue(new Error('Not authenticated')); + + // ACT + renderWithProvider(); + + // ASSERT + expect(screen.getByTestId('user')).toHaveTextContent('null'); + expect(screen.getByTestId('email')).toHaveTextContent('null'); + expect(screen.getByTestId('groups')).toHaveTextContent('null'); + }); + + it('loads user successfully and fetches groups', async () => { + // ARRANGE + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({ email: 'test@example.com' }); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup', 'DelegatedAdminGroup'], + }, + }, + }, + } as any); + + // ACT + renderWithProvider(); + + // ASSERT + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + + await waitFor(() => { + expect(screen.getByTestId('email')).toHaveTextContent('test@example.com'); + }); + + await waitFor(() => { + expect(screen.getByTestId('groups')).toHaveTextContent('AdminGroup,DelegatedAdminGroup'); + }); + }); + + it('handles getCurrentUser failure and triggers sign in redirect', async () => { + // ARRANGE + mockGetCurrentUser.mockRejectedValue(new Error('Not authenticated')); + mockSignInWithRedirect.mockResolvedValue(); + + // ACT + renderWithProvider(); + + // ASSERT + await waitFor(() => { + expect(mockSignInWithRedirect).toHaveBeenCalled(); + }); + + expect(screen.getByTestId('user')).toHaveTextContent('null'); + }); + + it('handles fetchUserAttributes failure gracefully', async () => { + // ARRANGE + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockRejectedValue(new Error('Failed to fetch attributes')); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + } as any); + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + // ACT + renderWithProvider(); + + // ASSERT + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + + expect(screen.getByTestId('email')).toHaveTextContent('null'); + expect(consoleSpy).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + + it('handles sign in redirect failure gracefully', async () => { + // ARRANGE + mockGetCurrentUser.mockRejectedValue(new Error('Not authenticated')); + mockSignInWithRedirect.mockRejectedValue(new Error('Sign in failed')); + const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); + + // ACT + renderWithProvider(); + + // ASSERT + await waitFor(() => { + expect(mockSignInWithRedirect).toHaveBeenCalled(); + }); + + expect(consoleSpy).toHaveBeenCalledWith('Sign in error:', expect.any(Error)); + + consoleSpy.mockRestore(); + }); + + it('calls signOut function correctly', async () => { + // ARRANGE + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({ email: 'test@example.com' }); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + } as any); + mockSignOut.mockResolvedValue(); + + // ACT + renderWithProvider(); + + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + + const signOutButton = screen.getByTestId('signOut'); + signOutButton.click(); + + // ASSERT + expect(mockSignOut).toHaveBeenCalled(); + }); + + it('calls signInWithRedirect function correctly', async () => { + // ARRANGE + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({ email: 'test@example.com' }); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + } as any); + mockSignInWithRedirect.mockResolvedValue(); + + // ACT + renderWithProvider(); + + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + + const signInButton = screen.getByTestId('signIn'); + signInButton.click(); + + // ASSERT + expect(mockSignInWithRedirect).toHaveBeenCalled(); + }); + + it('handles Hub auth events correctly', async () => { + // ARRANGE + let hubCallback: ((data: any) => void) | null = null; + // @ts-ignore - mock implementation + mockHubListen.mockImplementation((channel, callback) => { + if (channel === 'auth') { + hubCallback = callback; + } + }); + + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({ email: 'test@example.com' }); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + } as any); + + // ACT + renderWithProvider(); + + // Wait for initial load + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + + // Simulate signedOut event + await act(async () => { + if (hubCallback) { + hubCallback({ payload: { event: 'signedOut' } }); + } + }); + + // ASSERT + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('null'); + }); + }); + + it('handles signInWithRedirect Hub event', async () => { + // ARRANGE + let hubCallback: ((data: any) => void) | null = null; + // @ts-ignore - mock implementation + mockHubListen.mockImplementation((channel, callback) => { + if (channel === 'auth') { + hubCallback = callback; + } + }); + + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({ email: 'test@example.com' }); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + } as any); + + // ACT + renderWithProvider(); + + // Simulate signInWithRedirect event + await act(async () => { + if (hubCallback) { + hubCallback({ payload: { event: 'signInWithRedirect' } }); + } + }); + + // ASSERT + await waitFor(() => { + expect(mockGetCurrentUser).toHaveBeenCalled(); + }); + }); + + it('handles unknown Hub events gracefully', async () => { + // ARRANGE + let hubCallback: ((data: any) => void) | null = null; + // @ts-ignore - mock implementation + mockHubListen.mockImplementation((channel, callback) => { + if (channel === 'auth') { + hubCallback = callback; + } + }); + + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({ email: 'test@example.com' }); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + } as any); + + // ACT + renderWithProvider(); + + // Simulate unknown event + await act(async () => { + if (hubCallback) { + hubCallback({ payload: { event: 'unknownEvent' } }); + } + }); + + // ASSERT - Should not crash or change state + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + }); + + it('handles fetchAuthSession with no groups', async () => { + // ARRANGE + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({ email: 'test@example.com' }); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: {}, + }, + }, + } as any); + + // ACT + renderWithProvider(); + + // ASSERT + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + + await waitFor(() => { + expect(screen.getByTestId('email')).toHaveTextContent('test@example.com'); + }); + + expect(screen.getByTestId('groups')).toHaveTextContent('null'); + }); + + it('handles missing email in user attributes', async () => { + // ARRANGE + mockGetCurrentUser.mockResolvedValue(mockUser); + mockFetchUserAttributes.mockResolvedValue({}); + mockFetchAuthSession.mockResolvedValue({ + tokens: { + accessToken: { + payload: { + 'cognito:groups': ['AdminGroup'], + }, + }, + }, + } as any); + + // ACT + renderWithProvider(); + + // ASSERT + await waitFor(() => { + expect(screen.getByTestId('user')).toHaveTextContent('testuser'); + }); + + expect(screen.getByTestId('email')).toHaveTextContent('null'); + }); +}); diff --git a/source/webui/src/__tests__/pages/CallbackPage.test.tsx b/source/webui/src/__tests__/pages/CallbackPage.test.tsx new file mode 100644 index 00000000..b92630dc --- /dev/null +++ b/source/webui/src/__tests__/pages/CallbackPage.test.tsx @@ -0,0 +1,138 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { act, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '@testing-library/react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; + +import { CallbackPage } from '../../pages/callback/CallbackPage.tsx'; +import { UserContext } from '../../contexts/UserContext.tsx'; +import { mockUserContext } from '../test-data-factory.ts'; +import { vi } from 'vitest'; + +const MockHomePage = () => ( +
+

Home Page

+
+); + +const renderCallbackPage = (searchParams = '', userContextOverrides = {}) => { + const contextValue = { + ...mockUserContext, + ...userContextOverrides, + }; + + return render( + + + + } /> + } /> + + + , + ); +}; + +describe('CallbackPage', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('displays error when authentication fails with error parameter', () => { + // ARRANGE & ACT + renderCallbackPage('?error=access_denied&error_description=User denied access'); + + // ASSERT + expect(screen.getByRole('heading', { name: 'Automated Security Response on AWS' })).toBeInTheDocument(); + expect(screen.getByText('Sign-in failed')).toBeInTheDocument(); + expect(screen.getByText('User denied access')).toBeInTheDocument(); + expect( + screen.getByText(/Please ensure you have been invited by an existing Admin or Delegated Admin user/), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Try Again' })).toBeInTheDocument(); + }); + + it('displays generic error message when only error parameter is present', () => { + // ARRANGE & ACT + renderCallbackPage('?error=invalid_request'); + + // ASSERT + expect(screen.getByText('Sign-in failed')).toBeInTheDocument(); + expect(screen.getByText('An authentication error occurred.')).toBeInTheDocument(); + }); + + it('displays loading state when no error and user is not authenticated + shows failsafe button after 10 seconds in loading state', () => { + // ARRANGE & ACT + vi.useFakeTimers(); + renderCallbackPage('', { user: null }); + + // ASSERT + expect(screen.getByRole('heading', { name: 'Automated Security Response on AWS' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Signing you in...' })).toBeInTheDocument(); + // Initially no failsafe button + expect(screen.queryByRole('button', { name: 'Continue to Application' })).not.toBeInTheDocument(); + + // ACT - Fast-forward 10 seconds + act(() => { + vi.advanceTimersByTime(10000); + }); + + // ASSERT - Failsafe button appears + expect(screen.getByRole('button', { name: 'Continue to Application' })).toBeInTheDocument(); + }); + + it('navigates to home when user is authenticated and no error', async () => { + // ARRANGE & ACT + renderCallbackPage('', { user: { email: 'test@example.com' } }); + + // ASSERT + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Home Page' })).toBeInTheDocument(); + }); + }); + + it('handles try again button click', async () => { + // ARRANGE + renderCallbackPage('?error=access_denied'); + + // ACT + const tryAgainButton = screen.getByRole('button', { name: 'Try Again' }); + await userEvent.click(tryAgainButton); + + // ASSERT + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Home Page' })).toBeInTheDocument(); + }); + }); + + it('handles continue to application button click', async () => { + // ARRANGE + vi.useFakeTimers(); + renderCallbackPage('', { user: null }); + act(() => { + vi.advanceTimersByTime(10000); + }); + vi.useRealTimers(); + + // ACT + const continueButton = screen.getByRole('button', { name: 'Continue to Application' }); + await userEvent.click(continueButton); + + // ASSERT + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Home Page' })).toBeInTheDocument(); + }); + }); + + it('redirects to base page when user is authenticated', async () => { + // ARRANGE & ACT + renderCallbackPage('', { user: { email: 'test@example.com' } }); + + // ASSERT + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Home Page' })).toBeInTheDocument(); + }); + }); +}); diff --git a/source/webui/src/__tests__/pages/FindingsPage.test.tsx b/source/webui/src/__tests__/pages/FindingsPage.test.tsx new file mode 100644 index 00000000..9b6d410d --- /dev/null +++ b/source/webui/src/__tests__/pages/FindingsPage.test.tsx @@ -0,0 +1,711 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { http } from 'msw'; +import { ok } from '../../mocks/handlers.ts'; +import { ApiEndpoints } from '../../store/solutionApi.ts'; +import { MOCK_SERVER_URL, server } from '../server.ts'; +import { generateTestFindings } from '../test-data-factory.ts'; +import { renderAppContent } from '../test-utils.tsx'; + +it('renders an empty table', async () => { + // GIVEN the backend returns no findings + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: [], NextToken: null }))); + + // WHEN rendering the /findings route + renderAppContent({ + initialRoute: '/findings', + }); + + // THEN + const withinMain = within(screen.getByTestId('main-content')); + expect(withinMain.getByRole('heading', { name: 'Findings to Remediate (0)' })).toBeInTheDocument(); + expect(await withinMain.findByText(/no findings to display/i)).toBeInTheDocument(); +}); + +it('renders a table with findings', async () => { + // GIVEN the backend returns 5 findings + const findings = generateTestFindings(5, { suppressed: false, remediationStatus: 'NOT_STARTED' }); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: findings, NextToken: null }))); + + // WHEN + renderAppContent({ + initialRoute: '/findings', + }); + + // THEN expect 5 findings plus a header row in the table + const withinMain = within(screen.getByTestId('main-content')); + const loadingIndicator = await withinMain.findByText('Loading findings'); + await waitForElementToBeRemoved(loadingIndicator); + + const heading = await withinMain.findByRole('heading', { name: `Findings to Remediate (5)` }); + expect(heading).toBeInTheDocument(); + + const table = await withinMain.findByRole('table'); + const rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(findings.length + 1); + + const finding1NameCell = await within(table).findByRole('cell', { name: findings[0].findingDescription }); + expect(finding1NameCell).toBeInTheDocument(); +}); + +it('shows Actions dropdown with correct options when findings are selected', async () => { + // GIVEN the backend returns findings with mixed suppressed status and selectable remediation status + const findings = [ + ...generateTestFindings(2, { suppressed: false, remediationStatus: 'NOT_STARTED' }), + ...generateTestFindings(2, { suppressed: true, remediationStatus: 'NOT_STARTED' }), + ]; + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: findings, NextToken: null }))); + + // WHEN rendering the /findings route + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // THEN the Actions dropdown should be disabled initially + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + expect(actionsButton).toBeDisabled(); + + // WHEN selecting a finding + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); // Select first finding (skip header checkbox) + + // THEN the Actions dropdown should be enabled + expect(actionsButton).toBeEnabled(); + + // WHEN clicking the Actions dropdown + await userEvent.click(actionsButton); + + // THEN it should show all action options + const dropdown = await screen.findByRole('menu'); + expect(within(dropdown).getByText('Remediate')).toBeInTheDocument(); + expect(within(dropdown).getByText('Remediate & Generate Ticket')).toBeInTheDocument(); + expect(within(dropdown).getByText('Suppress')).toBeInTheDocument(); + expect(within(dropdown).getByText('Unsuppress')).toBeInTheDocument(); +}); + +it('enables Suppress action only for unsuppressed findings', async () => { + // GIVEN the backend returns unsuppressed findings with selectable remediation status + const findings = generateTestFindings(3, { suppressed: false, remediationStatus: 'NOT_STARTED' }); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: findings, NextToken: null }))); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // WHEN selecting an unsuppressed finding + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + // WHEN clicking the Actions dropdown + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + // THEN Suppress should be enabled and Unsuppress should be disabled + const dropdown = await screen.findByRole('menu'); + const suppressOption = within(dropdown).getByText('Suppress'); + const unsuppressOption = within(dropdown).getByText('Unsuppress'); + + // Check if the options are clickable (enabled) or not (disabled) + expect(suppressOption).toBeInTheDocument(); + expect(unsuppressOption).toBeInTheDocument(); + +}); + +it('enables Unsuppress action only for suppressed findings', async () => { + // GIVEN the backend returns suppressed findings + const findings = generateTestFindings(3, { suppressed: true, remediationStatus: 'NOT_STARTED' }); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: findings, NextToken: null }))); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // WHEN toggling to show suppressed findings + const showSuppressedToggle = await withinMain.findByText('Show suppressed findings'); + await userEvent.click(showSuppressedToggle); + + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // WHEN selecting a suppressed finding + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + // WHEN clicking the Actions dropdown + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + // THEN Unsuppress should be enabled and Suppress should be disabled + const dropdown = await screen.findByRole('menu'); + const suppressOption = within(dropdown).getByText('Suppress'); + const unsuppressOption = within(dropdown).getByText('Unsuppress'); + + // Check if the options are present - the actual disabled state testing is complex with CloudScape + // The important thing is that the dropdown shows the correct options and the logic works + expect(suppressOption).toBeInTheDocument(); + expect(unsuppressOption).toBeInTheDocument(); +}); + +it('shows confirmation modal when Unsuppress action is selected', async () => { + // GIVEN the backend returns suppressed findings + const findings = generateTestFindings(2, { suppressed: true, remediationStatus: 'NOT_STARTED' }); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: findings, NextToken: null }))); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // WHEN toggling to show suppressed findings + const showSuppressedToggle = await withinMain.findByText('Show suppressed findings'); + await userEvent.click(showSuppressedToggle); + + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // WHEN selecting findings and clicking Unsuppress + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + await userEvent.click(checkboxes[2]); + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const unsuppressOption = within(dropdown).getByText('Unsuppress'); + await userEvent.click(unsuppressOption); + + // THEN a confirmation modal should appear + const modal = await screen.findByRole('dialog'); + expect(within(modal).getByText('Confirm Unsuppress Action')).toBeInTheDocument(); + expect(within(modal).getByText(/are you sure you want to unsuppress 2 findings/i)).toBeInTheDocument(); + expect(within(modal).getByText(/unsuppressed findings will be visible in the default view/i)).toBeInTheDocument(); + expect(within(modal).getByRole('button', { name: 'Unsuppress' })).toBeInTheDocument(); + expect(within(modal).getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); +}); + +it('executes Unsuppress action when confirmed', async () => { + // GIVEN the backend returns suppressed findings and will accept unsuppress action + const findings = generateTestFindings(1, { suppressed: true, remediationStatus: 'NOT_STARTED' }); + let unsuppressActionCalled = false; + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: findings, NextToken: null })), + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS + '/action', async ({ request }) => { + const body = await request.json() as any; + if (body.actionType === 'Unsuppress') { + unsuppressActionCalled = true; + expect(body.findingIds).toEqual([findings[0].findingId]); + } + return await ok({}); + }) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // WHEN toggling to show suppressed findings + const showSuppressedToggle = await withinMain.findByText('Show suppressed findings'); + await userEvent.click(showSuppressedToggle); + + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // WHEN selecting a finding and confirming unsuppress + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const unsuppressOption = within(dropdown).getByText('Unsuppress'); + await userEvent.click(unsuppressOption); + + const modal = await screen.findByRole('dialog'); + const confirmButton = within(modal).getByRole('button', { name: 'Unsuppress' }); + await userEvent.click(confirmButton); + + // THEN the unsuppress action should be called and modal should be dismissed + await waitFor(() => { + expect(unsuppressActionCalled).toBe(true); + }); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); +}); + +it('cancels Unsuppress action when Cancel is clicked', async () => { + // GIVEN the backend returns suppressed findings + const findings = generateTestFindings(1, { suppressed: true, remediationStatus: 'NOT_STARTED' }); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: findings, NextToken: null }))); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // WHEN toggling to show suppressed findings + const showSuppressedToggle = await withinMain.findByText('Show suppressed findings'); + await userEvent.click(showSuppressedToggle); + + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // WHEN selecting a finding and clicking Unsuppress, then Cancel + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const unsuppressOption = within(dropdown).getByText('Unsuppress'); + await userEvent.click(unsuppressOption); + + const modal = await screen.findByRole('dialog'); + const cancelButton = within(modal).getByRole('button', { name: 'Cancel' }); + await userEvent.click(cancelButton); + + // THEN the modal should be dismissed + expect(modal).not.toBeInTheDocument(); + + // AND the finding should still be selected + expect(checkboxes[1]).toBeChecked(); +}); + +it('shows suppressed findings when toggle is enabled', async () => { + // GIVEN the backend returns mixed findings + const unsuppressedFindings = generateTestFindings(2, { suppressed: false }); + const suppressedFindings = generateTestFindings(2, { suppressed: true }); + const allFindings = [...unsuppressedFindings, ...suppressedFindings]; + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => await ok({ Findings: allFindings, NextToken: null }))); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // THEN initially only unsuppressed findings should be visible + let table = await withinMain.findByRole('table'); + let rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(3); // 2 unsuppressed + header + + // WHEN toggling to show suppressed findings + const showSuppressedToggle = await withinMain.findByText('Show suppressed findings'); + await userEvent.click(showSuppressedToggle); + + // THEN all findings should be visible + table = await withinMain.findByRole('table'); + rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(5); // 4 findings + header +}); + +it('renders loading state initially', async () => { + // GIVEN the backend is slow to respond + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => { + await new Promise(resolve => setTimeout(resolve, 100)); + return await ok({ Findings: [], NextToken: null }); + }) + ); + + // WHEN rendering the /findings route + renderAppContent({ + initialRoute: '/findings', + }); + + // THEN loading indicator should be visible + const withinMain = within(screen.getByTestId('main-content')); + expect(await withinMain.findByText('Loading findings')).toBeInTheDocument(); +}); + +it('handles search error gracefully', async () => { + // GIVEN the backend returns an error + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => { + return new Response(JSON.stringify({ message: 'Internal server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + }) + ); + + // WHEN rendering the /findings route + renderAppContent({ + initialRoute: '/findings', + }); + + // THEN error message should be displayed + const withinMain = within(screen.getByTestId('main-content')); + expect(await withinMain.findByText(/failed to load findings/i)).toBeInTheDocument(); +}); + +it('handles sorting changes', async () => { + const findings = generateTestFindings(3, { suppressed: false }); + let lastSearchRequest: any = null; + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async ({ request }) => { + lastSearchRequest = await request.json(); + return await ok({ Findings: findings, NextToken: null }); + }) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // WHEN clicking on a sortable column header + const table = await withinMain.findByRole('table'); + const securityHubHeader = await within(table).findByText('Security Hub Updated Time'); + await userEvent.click(securityHubHeader); + + // THEN the sort order should change + await waitFor(() => { + expect(lastSearchRequest.SortCriteria[0].SortOrder).toBe('asc'); + }); +}); + +it('shows confirmation modals for different actions', async () => { + const findings = generateTestFindings(3, { suppressed: false, remediationStatus: 'NOT_STARTED' }); + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => + await ok({ Findings: findings, NextToken: null }) + ) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + + // Test Suppress action modal + await userEvent.click(checkboxes[1]); + let actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + let dropdown = await screen.findByRole('menu'); + await userEvent.click(within(dropdown).getByText('Suppress')); + + let modal = await screen.findByRole('dialog'); + expect(within(modal).getByText('Confirm Suppress Action')).toBeInTheDocument(); + expect(within(modal).getByText(/are you sure you want to suppress 1 finding/i)).toBeInTheDocument(); + expect(within(modal).getByText(/suppressed findings will be hidden from the default view/i)).toBeInTheDocument(); + + // Cancel and test Remediate action modal + await userEvent.click(within(modal).getByRole('button', { name: 'Cancel' })); + await userEvent.click(checkboxes[2]); // Select second finding too + + actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + dropdown = await screen.findByRole('menu'); + await userEvent.click(within(dropdown).getByText('Remediate')); + + modal = await screen.findByRole('dialog'); + expect(within(modal).getByText('Confirm Remediation')).toBeInTheDocument(); + expect(within(modal).getByText(/are you sure you want to remediate 2 findings/i)).toBeInTheDocument(); + expect(within(modal).getByText(/automatically make changes to your aws resources/i)).toBeInTheDocument(); + + // Cancel and test Remediate & Generate Ticket action modal + await userEvent.click(within(modal).getByRole('button', { name: 'Cancel' })); + await userEvent.click(checkboxes[2]); // Deselect second finding + + actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + dropdown = await screen.findByRole('menu'); + await userEvent.click(within(dropdown).getByText('Remediate & Generate Ticket')); + + modal = await screen.findByRole('dialog'); + expect(within(modal).getByText('Confirm Remediation with Ticket')).toBeInTheDocument(); + expect(within(modal).getByText(/remediate 1 finding and generate tickets/i)).toBeInTheDocument(); + expect(within(modal).getByText(/create tracking tickets/i)).toBeInTheDocument(); +}); + +it('executes Suppress action when confirmed', async () => { + const findings = generateTestFindings(1, { suppressed: false, remediationStatus: 'NOT_STARTED' }); + let suppressActionCalled = false; + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => + await ok({ Findings: findings, NextToken: null }) + ), + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS + '/action', async ({ request }) => { + const body = await request.json() as any; + if (body.actionType === 'Suppress') { + suppressActionCalled = true; + expect(body.findingIds).toBeDefined(); + expect(Array.isArray(body.findingIds)).toBe(true); + expect(body.findingIds).toHaveLength(1); + expect(body.findingIds[0]).toBe(findings[0].findingId); + } + return await ok({}); + }) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // Select finding and confirm suppress + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const suppressOption = within(dropdown).getByText('Suppress'); + await userEvent.click(suppressOption); + + const modal = await screen.findByRole('dialog'); + const confirmButton = within(modal).getByRole('button', { name: 'Suppress' }); + await userEvent.click(confirmButton); + + // THEN the suppress action should be called + await waitFor(() => { + expect(suppressActionCalled).toBe(true); + }); +}); + +it('executes RemediateAndGenerateTicket action when confirmed', async () => { + const findings = generateTestFindings(1, { suppressed: false, remediationStatus: 'NOT_STARTED' }); + let remediateTicketActionCalled = false; + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => + await ok({ Findings: findings, NextToken: null }) + ), + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS + '/action', async ({ request }) => { + const body = await request.json() as any; + if (body.actionType === 'RemediateAndGenerateTicket') { + remediateTicketActionCalled = true; + expect(body.findingIds).toEqual([findings[0].findingId]); + } + return await ok({}); + }) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // Select finding and confirm remediate with ticket + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const remediateTicketOption = within(dropdown).getByText('Remediate & Generate Ticket'); + await userEvent.click(remediateTicketOption); + + const modal = await screen.findByRole('dialog'); + const confirmButton = within(modal).getByRole('button', { name: 'Remediate & Create Ticket' }); + await userEvent.click(confirmButton); + + // THEN the remediate and ticket action should be called + await waitFor(() => { + expect(remediateTicketActionCalled).toBe(true); + }); +}); + +it('handles action execution errors', async () => { + const findings = generateTestFindings(1, { suppressed: false, remediationStatus: 'NOT_STARTED' }); + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => + await ok({ Findings: findings, NextToken: null }) + ), + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS + '/action', async () => { + return new Response(JSON.stringify({ message: 'Action failed' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + }) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // Select finding and confirm suppress + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const suppressOption = within(dropdown).getByText('Suppress'); + await userEvent.click(suppressOption); + + const modal = await screen.findByRole('dialog'); + const confirmButton = within(modal).getByRole('button', { name: 'Suppress' }); + await userEvent.click(confirmButton); + + // THEN error message should be displayed + expect(await withinMain.findByText(/failed to suppress findings/i)).toBeInTheDocument(); +}); + +it('refreshes findings when refresh button is clicked', async () => { + const findings = generateTestFindings(3, { suppressed: false }); + let requestCount = 0; + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => { + requestCount++; + return await ok({ Findings: findings, NextToken: null }); + }) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // WHEN clicking refresh button + const refreshButton = await withinMain.findByLabelText('Refresh findings'); + await userEvent.click(refreshButton); + + // THEN a new request should be made + await waitFor(() => { + expect(requestCount).toBe(2); + }); +}); + +it('shows finding IDs in confirmation modal', async () => { + const findings = generateTestFindings(7, { suppressed: false, remediationStatus: 'NOT_STARTED' }); // More than 5 to test truncation + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => + await ok({ Findings: findings, NextToken: null }) + ) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // Select all findings + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[0]); // Select all checkbox + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const suppressOption = within(dropdown).getByText('Suppress'); + await userEvent.click(suppressOption); + + // THEN modal should show finding IDs with truncation + const modal = await screen.findByRole('dialog'); + expect(within(modal).getByText('Selected finding IDs:')).toBeInTheDocument(); + + // Should show truncation message for more than 5 findings + expect(within(modal).getByText('... and 2 more finding(s)')).toBeInTheDocument(); + + // Verify the modal shows the confirmation message + expect(within(modal).getByText(/are you sure you want to suppress 7 findings/i)).toBeInTheDocument(); +}); + +it('handles View History button click in success message', async () => { + const findings = generateTestFindings(1, { suppressed: false, remediationStatus: 'NOT_STARTED' }); + + server.use( + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS, async () => + await ok({ Findings: findings, NextToken: null }) + ), + http.post(MOCK_SERVER_URL + ApiEndpoints.FINDINGS + '/action', async () => await ok({})) + ); + + renderAppContent({ + initialRoute: '/findings', + }); + + const withinMain = within(screen.getByTestId('main-content')); + await waitForElementToBeRemoved(await withinMain.findByText('Loading findings')); + + // Execute a successful remediate action + const table = await withinMain.findByRole('table'); + const checkboxes = await within(table).findAllByRole('checkbox'); + await userEvent.click(checkboxes[1]); + + const actionsButton = await withinMain.findByRole('button', { name: 'Actions' }); + await userEvent.click(actionsButton); + + const dropdown = await screen.findByRole('menu'); + const remediateOption = within(dropdown).getByText('Remediate'); + await userEvent.click(remediateOption); + + const modal = await screen.findByRole('dialog'); + const confirmButton = within(modal).getByRole('button', { name: 'Remediate' }); + await userEvent.click(confirmButton); + + // Wait for success message with View History button + await withinMain.findByText(/successfully sent 1 finding for remediation/i); + const viewHistoryButton = await withinMain.findByText('View History'); + + // WHEN clicking View History button + await userEvent.click(viewHistoryButton); + + await waitFor(() => { + expect(screen.queryByText('Findings to Remediate')).not.toBeInTheDocument(); + }); +}); diff --git a/source/webui/src/__tests__/pages/InviteUsersPage.test.tsx b/source/webui/src/__tests__/pages/InviteUsersPage.test.tsx new file mode 100644 index 00000000..2094c3d5 --- /dev/null +++ b/source/webui/src/__tests__/pages/InviteUsersPage.test.tsx @@ -0,0 +1,616 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { screen, within, act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { http } from 'msw'; +import { render } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { configureStore } from '@reduxjs/toolkit'; +import createWrapper from '@cloudscape-design/components/test-utils/dom'; + +import { MOCK_SERVER_URL, server } from '../server.ts'; +import { ApiEndpoints } from '../../store/solutionApi.ts'; +import { ok } from '../../mocks/handlers.ts'; +import { mockUserContext } from '../test-data-factory.ts'; +import { InviteUsersPage } from '../../pages/users/invite/InviteUsersPage.tsx'; +import { UserContext } from '../../contexts/UserContext.tsx'; +import { NotificationContextProvider } from '../../contexts/NotificationContext.tsx'; +import { rootReducer } from '../../store/store.ts'; +import { solutionApi } from '../../store/solutionApi.ts'; + +const renderInviteUsersPage = () => { + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(solutionApi.middleware), + }); + + const renderResult = render( + + + + +
+ +
+
+
+
+
, + ); + + return { + store, + container: renderResult.container, + }; +}; + +describe('InviteUsersPage', () => { + it('renders initial page state', () => { + // ACT + renderInviteUsersPage(); + + // ASSERT + // Form structure + expect(screen.getByRole('heading', { name: 'Invite Users' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Invitation Details' })).toBeInTheDocument(); + expect(screen.getByLabelText('Email')).toBeInTheDocument(); + expect(screen.getByLabelText('Permission Type')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument(); + + // Description text + expect(screen.getByText(/Send an access invitation for additional users/)).toBeInTheDocument(); + expect(screen.getByText(/Let us know who the invitation should be sent to/)).toBeInTheDocument(); + expect(screen.getByText(/What level of access should this user have/)).toBeInTheDocument(); + + // Initial state + expect(screen.getByRole('button', { name: 'Submit' })).toBeDisabled(); + }); + + it('enables submit button when email and permission type are filled', async () => { + // ARRANGE + const user = userEvent.setup(); + + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'test@example.com'); + + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + // ASSERT + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled(); + }); + }); + + it('shows owned accounts field when account operator is selected', async () => { + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const selectWrapper = wrapper.findSelect(); + + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('account-operator'); + }); + + // ASSERT + await waitFor(() => { + expect(screen.getByLabelText('Owned Accounts')).toBeInTheDocument(); + }); + expect(screen.getByText(/Enter a comma-separated list of Account IDs/)).toBeInTheDocument(); + }); + + it('hides owned accounts field when delegated admin is selected', async () => { + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const selectWrapper = wrapper.findSelect(); + + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('account-operator'); + }); + + // Verify field appears + await waitFor(() => { + expect(screen.getByLabelText('Owned Accounts')).toBeInTheDocument(); + }); + + // Switch to delegated admin + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + // ASSERT + await waitFor(() => { + expect(screen.queryByLabelText('Owned Accounts')).not.toBeInTheDocument(); + }); + }); + + it('validates account IDs and shows error for invalid format', async () => { + // ARRANGE + const user = userEvent.setup(); + + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const selectWrapper = wrapper.findSelect(); + + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('account-operator'); + }); + + const ownedAccountsField = screen.getByLabelText('Owned Accounts'); + await user.type(ownedAccountsField, 'invalid-account-id'); + + // ASSERT + await waitFor(() => { + expect(screen.getByText(/Invalid account IDs/)).toBeInTheDocument(); + }); + expect(ownedAccountsField).toHaveAttribute('aria-invalid', 'true'); + }); + + it('accepts valid account IDs', async () => { + // ARRANGE + const user = userEvent.setup(); + + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const selectWrapper = wrapper.findSelect(); + + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('account-operator'); + }); + + const ownedAccountsField = screen.getByLabelText('Owned Accounts'); + await user.type(ownedAccountsField, '123456789012, 012345678901'); + + // ASSERT + await waitFor(() => { + expect(screen.queryByText(/Invalid account IDs/)).not.toBeInTheDocument(); + }); + expect(ownedAccountsField).not.toHaveAttribute('aria-invalid', 'true'); + }); + + it('disables submit button when account operator has invalid account IDs', async () => { + // ARRANGE + const user = userEvent.setup(); + + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'test@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('account-operator'); + }); + + const ownedAccountsField = screen.getByLabelText('Owned Accounts'); + await user.type(ownedAccountsField, 'invalid'); + + // ASSERT + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Submit' })).toBeDisabled(); + }); + }); + + it('successfully invites delegated admin user', async () => { + // ARRANGE + const user = userEvent.setup(); + server.use(http.post(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, async () => await ok({}))); + + // ACT + const { container, store } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'test@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + const submitButton = screen.getByRole('button', { name: 'Submit' }); + await user.click(submitButton); + + // ASSERT + await waitFor(() => { + expect(emailInput).toHaveValue(''); + }); + + await waitFor(() => { + const state = store.getState(); + expect(state.notifications.notifications).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'success', + content: 'User invitation sent successfully to test@example.com', + }), + ]), + ); + }); + + // ACT - second successful invitation + await user.type(emailInput, 'test2@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + await user.click(submitButton); + + // ASSERT - notification should be re-rendered + await waitFor(() => { + expect(emailInput).toHaveValue(''); + }); + + await waitFor(() => { + const state = store.getState(); + expect(state.notifications.notifications).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'success', + content: 'User invitation sent successfully to test2@example.com', + }), + ]), + ); + }); + }); + + it('successfully invites account operator user with account IDs', async () => { + // ARRANGE + const user = userEvent.setup(); + server.use(http.post(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, async () => await ok({}))); + + // ACT + const { container, store } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'operator@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('account-operator'); + }); + + const ownedAccountsField = screen.getByLabelText('Owned Accounts'); + await user.type(ownedAccountsField, '123456789012, 012345678901'); + + const submitButton = screen.getByRole('button', { name: 'Submit' }); + await user.click(submitButton); + + // ASSERT + await waitFor(() => { + expect(emailInput).toHaveValue(''); + }); + + await waitFor(() => { + const state = store.getState(); + expect(state.notifications.notifications).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'success', + content: 'User invitation sent successfully to operator@example.com', + }), + ]), + ); + }); + }); + + it('shows loading state during submission', async () => { + // ARRANGE + const user = userEvent.setup(); + let resolveRequest: (value: Response) => void; + const requestPromise = new Promise((resolve) => { + resolveRequest = resolve; + }); + + server.use(http.post(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, async () => requestPromise)); + + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'test@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + const submitButton = screen.getByRole('button', { name: 'Submit' }); + await user.click(submitButton); + + // ASSERT + await waitFor(() => { + expect(submitButton).toHaveAttribute('aria-disabled', 'true'); + }); + + // Clean up + resolveRequest!(await ok({})); + }); + + it('displays error notification when invitation fails', async () => { + // ARRANGE + const user = userEvent.setup(); + server.use( + http.post(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, async () => { + return new Response(JSON.stringify({ message: 'User already exists' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + }), + ); + + // ACT + const { container, store } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'existing@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + const submitButton = screen.getByRole('button', { name: 'Submit' }); + await user.click(submitButton); + + // ASSERT + await waitFor(() => { + const state = store.getState(); + const errorNotification = state.notifications.notifications.find((n) => n.type === 'error'); + expect(errorNotification).toBeDefined(); + expect(errorNotification?.content).toContain('Failed to invite user'); + }); + }); + + it('handles API error with unknown error message', async () => { + // ARRANGE + const user = userEvent.setup(); + server.use( + http.post(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, async () => { + return new Response(null, { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + }), + ); + + // ACT + const { container, store } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'test@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + const submitButton = screen.getByRole('button', { name: 'Submit' }); + await user.click(submitButton); + + // ASSERT + await waitFor(() => { + const state = store.getState(); + const errorNotification = state.notifications.notifications.find((n) => n.type === 'error'); + expect(errorNotification).toBeDefined(); + expect(errorNotification?.content).toContain('Failed to invite user'); + }); + }); + + it('does not submit when form is incomplete', async () => { + // ARRANGE + const user = userEvent.setup(); + const mockPost = vi.fn(); + server.use(http.post(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, mockPost)); + + // ACT + renderInviteUsersPage(); + + const submitButton = screen.getByRole('button', { name: 'Submit' }); + + // Try to submit with empty form + await user.click(submitButton); + + // ASSERT + expect(mockPost).not.toHaveBeenCalled(); + expect(submitButton).toBeDisabled(); + }); + + it('enables submit for account operator without owned accounts', async () => { + // ARRANGE + const user = userEvent.setup(); + + // ACT + const { container } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'test@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('account-operator'); + }); + + // ASSERT + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled(); + }); + }); + + it('shows failed to invite user notification on API failure', async () => { + // ARRANGE + const user = userEvent.setup(); + server.use( + http.post( + `${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, + () => { + return Response.json({ message: 'first failure' }, { status: 400 }); + }, + { once: true }, + ), + http.post( + `${MOCK_SERVER_URL}${ApiEndpoints.USERS}`, + () => { + return Response.json({ message: 'second failure' }, { status: 400 }); + }, + { once: true }, + ), + ); + + // ACT + const { container, store } = renderInviteUsersPage(); + const wrapper = createWrapper(container); + + const emailInput = screen.getByLabelText('Email'); + const selectWrapper = wrapper.findSelect(); + + await user.type(emailInput, 'test@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + const submitButton = screen.getByRole('button', { name: 'Submit' }); + await user.click(submitButton); + + // ASSERT + await waitFor(() => { + const state = store.getState(); + expect(state.notifications.notifications).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'error', + content: 'Failed to invite user: first failure', + }), + ]), + ); + }); + + // ACT - second failed invitation + await user.clear(emailInput); + await user.type(emailInput, 'test2@example.com'); + act(() => { + selectWrapper!.openDropdown(); + }); + act(() => { + selectWrapper!.selectOptionByValue('delegated-admin'); + }); + + await waitFor(() => { + expect(submitButton).toBeEnabled(); + }); + + await user.click(submitButton); + + // ASSERT + await waitFor(() => { + const state = store.getState(); + const notifications = state.notifications.notifications; + expect(notifications).toHaveLength(2); + expect(notifications[1]).toEqual( + expect.objectContaining({ + type: 'error', + content: 'Failed to invite user: second failure', + }) + ); + }); + }); + + it('shows appropriate description for delegated admin users', () => { + // ARRANGE + const delegatedAdminUserContext = { + ...mockUserContext, + groups: ['DelegatedAdminGroup'], + }; + + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(solutionApi.middleware), + }); + + // ACT + render( + + + + +
+ +
+
+
+
+
, + ); + + // ASSERT + expect(screen.getByText('Delegated Admins can only invite Account Operators')).toBeInTheDocument(); + }); +}); diff --git a/source/webui/src/__tests__/pages/RemediationHistoryPage.test.tsx b/source/webui/src/__tests__/pages/RemediationHistoryPage.test.tsx new file mode 100644 index 00000000..56605001 --- /dev/null +++ b/source/webui/src/__tests__/pages/RemediationHistoryPage.test.tsx @@ -0,0 +1,451 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { http } from 'msw'; +import { ok } from '../../mocks/handlers.ts'; +import { ApiEndpoints } from '../../store/solutionApi.ts'; +import { MOCK_SERVER_URL, server } from '../server.ts'; +import { generateTestRemediation, generateTestRemediations } from '../test-data-factory.ts'; +import { renderAppContent } from '../test-utils.tsx'; + +describe('RemediationHistoryPage', () => { + it('renders an empty table', async () => { + // GIVEN the backend returns no remediations + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: [], NextToken: null }))); + + // WHEN rendering the /history route + renderAppContent({ + initialRoute: '/history', + }); + + // THEN + const withinMain = within(screen.getByTestId('main-content')); + expect(withinMain.getByRole('heading', { name: 'Remediation History (0)' })).toBeInTheDocument(); + expect(await withinMain.findByText(/no history to display/i)).toBeInTheDocument(); + }); + + it('renders a table with remediation history', async () => { + // GIVEN the backend returns 5 remediations + const remediations = generateTestRemediations(5); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: remediations, NextToken: null }))); + + // WHEN + renderAppContent({ + initialRoute: '/history', + }); + + // THEN expect 5 remediations plus a header row in the table + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for the data to load (the refresh button should not be in loading state) + await withinMain.findByRole('button', { name: 'Refresh history' }); + + const heading = await withinMain.findByRole('heading', { name: `Remediation History (5)` }); + expect(heading).toBeInTheDocument(); + + const table = await withinMain.findByRole('table'); + const rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(remediations.length + 1); + + // Verify first remediation data is displayed + const firstRemediationFindingId = await within(table).findByRole('cell', { name: remediations[0].findingId }); + expect(firstRemediationFindingId).toBeInTheDocument(); + }); + + it('displays refresh button and allows refreshing data', async () => { + // GIVEN the backend returns different numbers of remediations on subsequent requests + let requestCount = 0; + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => { + requestCount++; + if (requestCount <= 1) { + return await ok({ + Remediations: generateTestRemediations(3), + NextToken: null + }); + } else { + return await ok({ + Remediations: generateTestRemediations(4), + NextToken: null + }); + } + })); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for the initial data to load by waiting for the counter to show 3 items + await withinMain.findByText('(3)'); + + // Wait for the refresh button to appear and not be in loading state + const refreshButton = await withinMain.findByRole('button', { name: 'Refresh history' }); + expect(refreshButton).toBeInTheDocument(); + expect(refreshButton).not.toHaveAttribute('aria-disabled', 'true'); + + expect(requestCount).toBe(1); + + // WHEN clicking the refresh button + await userEvent.click(refreshButton); + + // THEN it should make another request and the UI should update to show 4 items + await withinMain.findByText('(4)'); + expect(requestCount).toBe(2); + }); + + it('supports all filtering types and interactions', async () => { + // GIVEN the backend returns remediations with diverse data + const remediations = [ + ...generateTestRemediations(1, { + findingId: 'finding-123', + remediationStatus: 'SUCCESS', + accountId: '123456789012', + resourceId: 'resource-abc123', + lastUpdatedBy: 'user1@example.com', + resourceType: 'AWS::S3::Bucket' + }), + ...generateTestRemediations(1, { + findingId: 'finding-456', + remediationStatus: 'FAILED', + accountId: '123456789013', + resourceId: 'resource-def456', + lastUpdatedBy: 'user2@example.com', + resourceType: 'AWS::EC2::Instance' + }), + ]; + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: remediations, NextToken: null }))); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for the data to load by waiting for the table to appear + const table = await withinMain.findByRole('table'); + const filterInput = await withinMain.findByPlaceholderText('Search Remediations'); + + // Test Finding ID filtering + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Finding ID = finding-123'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Finding ID = finding-123'); + + // Test Status filtering + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Status = SUCCESS'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Status = SUCCESS'); + + // Test Account ID filtering + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Account = 123456789012'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Account = 123456789012'); + + // Test Resource ID filtering + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Resource ID : abc123'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Resource ID : abc123'); + + // Test Executed By filtering + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Executed By = user1@example.com'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Executed By = user1@example.com'); + + // Test Resource Type filtering + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Resource Type : S3'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Resource Type : S3'); + + expect(table).toBeInTheDocument(); + }); + + it('supports sorting by columns', async () => { + // GIVEN the backend returns remediations with different timestamps + const now = new Date(); + const remediations = [ + { + ...generateTestRemediation(), + findingId: 'finding-1', + lastUpdatedTime: new Date(now.getTime() - 3600000).toISOString() // 1 hour ago + }, + { + ...generateTestRemediation(), + findingId: 'finding-2', + lastUpdatedTime: new Date(now.getTime() - 7200000).toISOString() // 2 hours ago + }, + ]; + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: remediations, NextToken: null }))); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for the data to load by waiting for the counter to show 2 items + await withinMain.findByText('(2)'); + + // Wait for the table to appear + const table = await withinMain.findByRole('table'); + const timestampHeader = await within(table).findByText('Execution Timestamp'); + expect(timestampHeader).toBeInTheDocument(); + + // Verify that data is displayed in the table - wait for the actual data rows + const rows = await within(table).findAllByRole('row'); + expect(rows.length).toBe(3); // Header + 2 data rows + + // Verify that the finding IDs are present in the table + await within(table).findByText('finding-1'); + await within(table).findByText('finding-2'); + }); + + it('displays correct counter text for filtered results', async () => { + // GIVEN the backend returns remediations + const remediations = generateTestRemediations(5); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: remediations, NextToken: null }))); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for the data to load by waiting for the table to appear + await withinMain.findByRole('table'); + + // Check that the header counter shows the correct count (counter is in separate span) + expect(await withinMain.findByText('(5)')).toBeInTheDocument(); + + // WHEN applying a filter that reduces results + const filterInput = await withinMain.findByPlaceholderText('Search Remediations'); + await userEvent.type(filterInput, `Finding ID = ${remediations[0].findingId}`); + await userEvent.keyboard('{Enter}'); + + // THEN the filter input should contain the filter text (filtering functionality works) + expect(filterInput).toHaveValue(`Finding ID = ${remediations[0].findingId}`); + }); + + it('handles error states gracefully', async () => { + // GIVEN the backend returns an error + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => { + return new Response(JSON.stringify({ message: 'Internal Server Error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + })); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // THEN it should display an error message + const errorAlert = await withinMain.findByText(/Failed to load remediation history/i); + expect(errorAlert).toBeInTheDocument(); + }); + + it('clears filters when clear filters is used', async () => { + // GIVEN the backend returns remediations + const remediations = generateTestRemediations(5); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: remediations, NextToken: null }))); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for the data to load by waiting for the table to appear + await withinMain.findByRole('table'); + + // Check that the initial counter appears (counter is in separate span) + expect(await withinMain.findByText('(5)')).toBeInTheDocument(); + + // WHEN applying a filter + const filterInput = await withinMain.findByPlaceholderText('Search Remediations'); + await userEvent.type(filterInput, `Finding ID = ${remediations[0].findingId}`); + await userEvent.keyboard('{Enter}'); + + // THEN the filter should be applied + expect(filterInput).toHaveValue(`Finding ID = ${remediations[0].findingId}`); + + // WHEN clearing filters + await userEvent.clear(filterInput); + await userEvent.keyboard('{Enter}'); + + // THEN the filter should be cleared + expect(filterInput).toHaveValue(''); + }); + + it('supports infinite scroll functionality with pagination', async () => { + // GIVEN the backend returns paginated results + let requestCount = 0; + const firstBatch = generateTestRemediations(3); + const secondBatch = generateTestRemediations(2); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async (req) => { + const body = await req.request.json() as any; + requestCount++; + + if (requestCount === 1) { + // First request - return first batch with NextToken + return await ok({ + Remediations: firstBatch, + NextToken: 'next-token-123' + }); + } else if (requestCount === 2 && body.NextToken === 'next-token-123') { + // Second request with NextToken - return second batch + return await ok({ + Remediations: secondBatch, + NextToken: null + }); + } + return await ok({ Remediations: [], NextToken: null }); + })); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for initial data to load by checking the heading + const initialHeading = await withinMain.findByRole('heading', { name: 'Remediation History (3+)' }); + expect(initialHeading).toBeInTheDocument(); + + // THEN should show initial data with + indicator for more data + expect(requestCount).toBe(1); + + const table = await withinMain.findByRole('table'); + const rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(4); + }); + + it('handles load more errors gracefully', async () => { + // GIVEN the backend returns data initially but fails on load more + let requestCount = 0; + const firstBatch = generateTestRemediations(3); + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async (req) => { + const body = await req.request.json() as any; + requestCount++; + + if (requestCount === 1) { + // First request succeeds + return await ok({ + Remediations: firstBatch, + NextToken: 'next-token-123' + }); + } else if (body.NextToken) { + // Load more request fails + return new Response(JSON.stringify({ message: 'Load more failed' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + return await ok({ Remediations: [], NextToken: null }); + })); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // Wait for initial data to load + await withinMain.findByText('(3+)'); + + // THEN should show initial data with + indicator for more data + expect(requestCount).toBe(1); + + // Verify the table shows the initial 3 items + const table = await withinMain.findByRole('table'); + const rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(4); + + }); + + it('supports different filter operators', async () => { + // GIVEN the backend returns remediations with diverse data + const remediations = [ + ...generateTestRemediations(1, { + findingId: 'finding-abc-123', + accountId: '111111111111', + resourceId: 'resource-test-456' + }), + ...generateTestRemediations(1, { + findingId: 'finding-xyz-789', + accountId: '222222222222', + resourceId: 'resource-prod-123' + }), + ]; + + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: remediations, NextToken: null }))); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + const filterInput = await withinMain.findByPlaceholderText('Search Remediations'); + + // Test != operator + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Account != 111111111111'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Account != 111111111111'); + + // Test !: operator (does not contain) + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Resource ID !: test'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Resource ID !: test'); + + // Test : operator (contains) + await userEvent.clear(filterInput); + await userEvent.type(filterInput, 'Finding ID : abc'); + await userEvent.keyboard('{Enter}'); + expect(filterInput).toHaveValue('Finding ID : abc'); + }); + + it('handles non-array allHistory gracefully', async () => { + // GIVEN the backend returns invalid data structure + server.use(http.post(MOCK_SERVER_URL + ApiEndpoints.REMEDIATIONS, async () => await ok({ Remediations: null, NextToken: null }))); + + // WHEN rendering the page + renderAppContent({ + initialRoute: '/history', + }); + + const withinMain = within(screen.getByTestId('main-content')); + + // THEN should handle gracefully and show empty state + expect(await withinMain.findByText(/no history to display/i)).toBeInTheDocument(); + }); + +}); diff --git a/source/webui/src/__tests__/pages/UsersPage.test.tsx b/source/webui/src/__tests__/pages/UsersPage.test.tsx new file mode 100644 index 00000000..8fa4f9c2 --- /dev/null +++ b/source/webui/src/__tests__/pages/UsersPage.test.tsx @@ -0,0 +1,273 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { screen, within, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { http } from 'msw'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { configureStore } from '@reduxjs/toolkit'; + +import { MOCK_SERVER_URL, server } from '../server.ts'; +import { ApiEndpoints, solutionApi } from '../../store/solutionApi.ts'; +import { ok } from '../../mocks/handlers.ts'; +import { User } from '@data-models'; +import { generateTestUsers, mockCurrentUser, mockUserContext } from '../test-data-factory.ts'; +import { UsersOverviewPage } from '../../pages/users/UsersOverviewPage.tsx'; +import { UserContext } from '../../contexts/UserContext.tsx'; +import { NotificationContextProvider } from '../../contexts/NotificationContext.tsx'; +import { rootReducer } from '../../store/store.ts'; + +const renderUsersPage = () => { + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(solutionApi.middleware), + }); + + return render( + + + + +
+ +
+
+
+
+
, + ); +}; + +beforeEach(() => { + // ARRANGE - Mock current user endpoint + server.use( + http.get(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}/current%40example.com`, async () => await ok(mockCurrentUser)), + ); +}); + +describe('UsersOverviewPage', () => { + it('renders an empty users table', async () => { + // ARRANGE + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok([]))); + + // ACT + renderUsersPage(); + + // ASSERT + const withinMain = within(screen.getByTestId('main-content')); + expect(withinMain.getByRole('heading', { name: 'Users (0)' })).toBeInTheDocument(); + expect(await withinMain.findByText(/no users to display/i)).toBeInTheDocument(); + }); + + it('renders a table with users', async () => { + // ARRANGE + const users = generateTestUsers(3); + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok(users))); + + // ACT + renderUsersPage(); + + // ASSERT + const withinMain = within(screen.getByTestId('main-content')); + const heading = await withinMain.findByRole('heading', { name: `Users (3)` }); + expect(heading).toBeInTheDocument(); + + const table = await withinMain.findByRole('table'); + const rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(users.length + 1); + + const user1EmailCell = await within(table).findByRole('cell', { name: users[0].email }); + expect(user1EmailCell).toBeInTheDocument(); + }); + + it('displays loading state', async () => { + // ARRANGE + const users = generateTestUsers(1); + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok(users))); + + // ACT + renderUsersPage(); + + // ASSERT + const withinMain = within(screen.getByTestId('main-content')); + expect(await withinMain.findByRole('heading', { name: 'Users (1)' })).toBeInTheDocument(); + }); + + it('filters users by email', async () => { + // ARRANGE + const users = generateTestUsers(5); + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok(users))); + + // ACT + renderUsersPage(); + + const withinMain = within(screen.getByTestId('main-content')); + await withinMain.findByRole('heading', { name: 'Users (5)' }); + + const searchInput = await withinMain.findByPlaceholderText('Search by User ID...'); + await userEvent.type(searchInput, 'user0'); + + // ASSERT + const table = await withinMain.findByRole('table'); + const rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(2); // header + 1 matching user + expect(await within(table).findByRole('cell', { name: 'user0@example.com' })).toBeInTheDocument(); + }); + + it('clears filter when clear button is clicked', async () => { + // ARRANGE + const users = generateTestUsers(3); + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok(users))); + + // ACT + renderUsersPage(); + + const withinMain = within(screen.getByTestId('main-content')); + await withinMain.findByRole('heading', { name: 'Users (3)' }); + + const searchInput = await withinMain.findByPlaceholderText('Search by User ID...'); + await userEvent.type(searchInput, 'nonexistent'); + + const clearButton = await withinMain.findByRole('button', { name: 'Clear filter' }); + await userEvent.click(clearButton); + + // ASSERT + const table = await withinMain.findByRole('table'); + const rows = await within(table).findAllByRole('row'); + expect(rows).toHaveLength(4); // header + 3 users + }); + + it('refreshes users when refresh button is clicked', async () => { + // ARRANGE + let callCount = 0; + server.use( + http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => { + callCount++; + return await ok(generateTestUsers(callCount)); + }), + ); + + // ACT + renderUsersPage(); + + const withinMain = within(screen.getByTestId('main-content')); + expect(await withinMain.findByRole('heading', { name: 'Users (1)' })).toBeInTheDocument(); + + const refreshButton = withinMain + .getAllByRole('button') + .find((button) => button.querySelector('svg path[d*="M15 8c0 3.87"]')); + expect(refreshButton).toBeInTheDocument(); + await userEvent.click(refreshButton!); + + // ASSERT + expect(await withinMain.findByRole('heading', { name: 'Users (2)' })).toBeInTheDocument(); + expect(callCount).toBe(2); + }); + + it('displays correct status badges', async () => { + // ARRANGE + const users: User[] = [ + { + email: 'confirmed@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Confirmed', + type: 'admin', + }, + { + email: 'invited@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Invited', + type: 'admin', + }, + ]; + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok(users))); + + // ACT + renderUsersPage(); + + const withinMain = within(screen.getByTestId('main-content')); + await withinMain.findByRole('heading', { name: 'Users (2)' }); + + // ASSERT + const table = await withinMain.findByRole('table'); + expect(await within(table).findByText('Confirmed')).toBeInTheDocument(); + expect(await within(table).findByText('Invited')).toBeInTheDocument(); + }); + + it('displays formatted invitation timestamp', async () => { + // ARRANGE + const testDate = new Date('2023-01-01T12:00:00Z'); + const users: User[] = [ + { + email: 'test@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: testDate.toISOString(), + status: 'Confirmed', + type: 'admin', + }, + ]; + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok(users))); + + // ACT + renderUsersPage(); + + const withinMain = within(screen.getByTestId('main-content')); + await withinMain.findByRole('heading', { name: 'Users (1)' }); + + // ASSERT + const table = await withinMain.findByRole('table'); + expect(await within(table).findByText(testDate.toLocaleString())).toBeInTheDocument(); + }); + + it('shows manage user button', async () => { + // ARRANGE + server.use(http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => await ok([]))); + + // ACT + renderUsersPage(); + + const withinMain = within(screen.getByTestId('main-content')); + + // ASSERT + expect(await withinMain.findByRole('button', { name: 'Manage User' })).toBeInTheDocument(); + }); + + it('displays error notification when users API fails', async () => { + // ARRANGE + server.use( + http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => { + return new Response(JSON.stringify({ message: 'API Error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + }), + ); + + // ACT + renderUsersPage(); + + // ASSERT + expect(await screen.findByText(/Failed to load users/i)).toBeInTheDocument(); + }); + + it('handles users API error with unknown error message', async () => { + // ARRANGE + server.use( + http.get(MOCK_SERVER_URL + ApiEndpoints.USERS, async () => { + return new Response(null, { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + }), + ); + + // ACT + renderUsersPage(); + + // ASSERT + expect(await screen.findByText(/Failed to load users.*Cannot read properties of null/i)).toBeInTheDocument(); + }); +}); diff --git a/source/webui/src/__tests__/pages/UsersTable.test.tsx b/source/webui/src/__tests__/pages/UsersTable.test.tsx new file mode 100644 index 00000000..d35ee14c --- /dev/null +++ b/source/webui/src/__tests__/pages/UsersTable.test.tsx @@ -0,0 +1,1091 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { render } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import createWrapper, { TableWrapper } from '@cloudscape-design/components/test-utils/dom'; +import { Provider } from 'react-redux'; +import { http } from 'msw'; +import UsersTable from '../../pages/users/users-table/UsersTable'; +import { User } from '@data-models'; +import { generateTestUsers } from '../test-data-factory'; +import { setupStore } from '../../store/store'; +import { server, MOCK_SERVER_URL } from '../server'; +import { ApiEndpoints } from '../../store/solutionApi'; +import { ok } from '../../mocks/handlers'; + +const mockRefresh = vi.fn(); + +const findRowByUserType = (table: TableWrapper | null, userType: string): number => { + const rows = table?.findRows(); + const index = rows?.findIndex((row: any) => row.getElement().textContent?.includes(userType)); + assert( + index !== undefined && index !== -1, + `could not find row for userType ${userType}, check that mock users are generated correctly.`, + ); + + return index + 1; // rows in cloudscape findRowSelectionArea are 1-indexed +}; + +const renderWithProvider = (component: React.ReactElement) => { + const store = setupStore(); + return { + store, + ...render({component}), + }; +}; + +describe('UsersTable', () => { + beforeEach(() => { + // ARRANGE - Reset mocks + mockRefresh.mockClear(); + }); + + it('should handle null users prop without crashing', () => { + // ARRANGE + const nullUsers = null as any; + + // ACT + const renderResult = () => + renderWithProvider(); + + // ASSERT + expect(renderResult).not.toThrow(); + }); + + it('should handle empty users array', () => { + // ARRANGE + const emptyUsers: User[] = []; + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const header = wrapper.findHeader(); + expect(header?.getElement()).toHaveTextContent('Users'); + expect(header?.getElement()).toHaveTextContent('(0)'); + const table = wrapper.findTable(); + expect(table?.getElement()).toHaveTextContent('No users to display.'); + }); + + it('should display correct counter for users array', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const header = wrapper.findHeader(); + expect(header?.getElement()).toHaveTextContent('Users'); + expect(header?.getElement()).toHaveTextContent('(1)'); + }); + + it('should display loading state', () => { + // ARRANGE + const users = generateTestUsers(3); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const table = wrapper.findTable(); + expect(table?.getElement()).toHaveTextContent('Loading users'); + }); + + it('should display users in table', () => { + // ARRANGE + const users = generateTestUsers(2); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const table = wrapper.findTable(); + expect(table).toBeTruthy(); + expect(table?.getElement()).toHaveTextContent(users[0].email); + expect(table?.getElement()).toHaveTextContent(users[1].email); + expect(table?.getElement()).toHaveTextContent('Confirmed'); + expect(table?.getElement()).toHaveTextContent('Invited'); + }); + + it('should filter users by email', () => { + // ARRANGE + const users = generateTestUsers(3); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + const textFilter = table?.findTextFilter(); + textFilter?.findInput().setInputValue('user0'); + + // ASSERT + const rows = table?.findRows(); + expect(rows).toHaveLength(1); // 1 matching user + expect(table?.getElement()).toHaveTextContent('user0@example.com'); + expect(table?.getElement()).not.toHaveTextContent('delegated1@example.com'); + expect(textFilter?.getElement()).toHaveTextContent('1 match'); + }); + + it('should show no matches message when filter has no results & clear filter when button is clicked', () => { + // ARRANGE + const users = generateTestUsers(2); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + const textFilter = table?.findTextFilter(); + textFilter?.findInput().setInputValue('nonexistent'); + + // ASSERT + expect(table?.getElement()).toHaveTextContent('No matches'); + expect(table?.getElement()).toHaveTextContent("We can't find a match."); + + const clearButton = wrapper.findButton('[data-testid="clear-filter-button"]'); + expect(clearButton).toBeTruthy(); + + // ACT + clearButton?.click(); + + // ASSERT + const rows = table?.findRows(); + expect(rows).toHaveLength(2); // 2 users + }); + + it('should call onRefresh when refresh button is clicked', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const refreshButton = wrapper.findButton('[data-testid="refresh-button"]'); + refreshButton?.click(); + + // ASSERT + expect(mockRefresh).toHaveBeenCalledTimes(1); + }); + + it('should display manage user button', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + expect(manageButton).toBeTruthy(); + }); + + it('should display correct column headers', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const table = wrapper.findTable(); + expect(table?.getElement()).toHaveTextContent('User ID'); + expect(table?.getElement()).toHaveTextContent('Status'); + expect(table?.getElement()).toHaveTextContent('Permission Type'); + expect(table?.getElement()).toHaveTextContent('Invited By'); + expect(table?.getElement()).toHaveTextContent('Invitation Timestamp'); + }); + + it('should format invitation timestamp correctly', () => { + // ARRANGE + const testDate = new Date('2023-01-01T12:00:00Z'); + const users: User[] = [ + { + email: 'test@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: testDate.toISOString(), + status: 'Confirmed', + type: 'admin', + }, + ]; + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const table = wrapper.findTable(); + expect(table?.getElement()).toHaveTextContent(testDate.toLocaleString()); + }); + + it('should paginate results with default page size of 20', () => { + // ARRANGE + const users = generateTestUsers(25); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const table = wrapper.findTable(); + const rows = table!.findRows(); + expect(rows).toHaveLength(20); + + const pagination = wrapper.findPagination(); + expect(pagination).toBeTruthy(); + }); + + it('should navigate to next page when next button is clicked', () => { + // ARRANGE + const users = generateTestUsers(25); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const pagination = wrapper.findPagination(); + const nextButton = pagination!.findNextPageButton(); + nextButton!.click(); + + // ASSERT + const table = wrapper.findTable(); + const rows = table!.findRows(); + expect(rows).toHaveLength(5); // 5 remaining users on page 2 + }); + + it('should respect page size preference changes', () => { + // ARRANGE + const users = generateTestUsers(15); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const preferences = wrapper.findCollectionPreferences(); + preferences!.findTriggerButton().click(); + + const modal = preferences!.findModal(); + const radioGroup = modal!.findContent()!.findRadioGroup(); + radioGroup!.findInputByValue('10')!.click(); + + const buttons = modal!.findFooter()!.findAllButtons(); + const confirmButton = buttons.find((button) => button.getElement().textContent === 'Confirm'); + confirmButton!.click(); + + // ASSERT + const updatedWrapper = createWrapper(document.body); + const table = updatedWrapper.findTable(); + const rows = table!.findRows(); + expect(rows).toHaveLength(10); + }); + + it('should show correct pagination info for multiple pages', () => { + // ARRANGE + const users = generateTestUsers(50); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const pagination = wrapper.findPagination(); + expect(pagination!.findCurrentPage().getElement()).toHaveTextContent('1'); + expect(pagination!.findPageNumbers()).toHaveLength(3); // Pages 1, 2, 3 + }); + + it('should disable previous button on first page', () => { + // ARRANGE + const users = generateTestUsers(25); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const pagination = wrapper.findPagination(); + const prevButton = pagination!.findPreviousPageButton(); + expect(prevButton!.getElement()).toBeDisabled(); + }); + + it('should disable next button on last page', () => { + // ARRANGE + const users = generateTestUsers(25); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const pagination = wrapper.findPagination(); + const nextButton = pagination!.findNextPageButton(); + nextButton!.click(); // Go to page 2 (last page) + + // ASSERT + expect(nextButton!.getElement()).toBeDisabled(); + }); + + it('should disable manage user button when no user is selected', () => { + // ARRANGE + const users = generateTestUsers(2); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + // ASSERT + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + expect(manageButton?.getElement()).toBeDisabled(); + }); + + it('should enable manage user button when user is selected', () => { + // ARRANGE + const users = generateTestUsers(2); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + // ASSERT + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + expect(manageButton?.getElement()).not.toBeDisabled(); + }); + + it('should open modal when manage user button is clicked', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + // ASSERT + const modal = wrapper.findModal('[data-testid="manage-user-modal"]'); + expect(modal?.isVisible()).toBe(true); + }); + + it('should display account operator modal content correctly', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + // ASSERT + const modal = wrapper.findModal('[data-testid="manage-user-modal"]'); + expect(modal?.getElement()).toHaveTextContent('Permission Type'); + expect(modal?.getElement()).toHaveTextContent('Account Operator'); + expect(modal?.getElement()).toHaveTextContent('Owned Accounts'); + expect(modal?.getElement()).toHaveTextContent('Remove User'); + const cancelButton = wrapper.findButton('[data-testid="cancel-manage-user-button"]'); + const saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + expect(cancelButton).toBeTruthy(); + expect(saveButton).toBeTruthy(); + const textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + expect(textarea?.getTextareaValue()).toBe('123456789012, 123456789013'); + }); + + it('should display delegated admin modal content correctly', () => { + // ARRANGE + const users = generateTestUsers(2); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + const rowIndex = findRowByUserType(table, 'delegated-admin'); + table!.findRowSelectionArea(rowIndex)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + // ASSERT + const modal = wrapper.findModal('[data-testid="manage-user-modal"]'); + expect(modal?.getElement()).toHaveTextContent('Permission Type'); + expect(modal?.getElement()).toHaveTextContent('Delegated Admin'); + expect(modal?.getElement()).toHaveTextContent('Remove User'); + expect(modal?.getElement()).not.toHaveTextContent('Owned Accounts'); + const cancelButton = wrapper.findButton('[data-testid="cancel-manage-user-button"]'); + const saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + const closeButton = wrapper.findButton('[data-testid="close-manage-user-button"]'); + expect(cancelButton).toBeFalsy(); + expect(saveButton).toBeFalsy(); + expect(closeButton).toBeTruthy(); + }); + + it('should close manage user modal when cancel button is clicked', async () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + const rowIndex = findRowByUserType(table, 'account-operator'); + table!.findRowSelectionArea(rowIndex)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const modalWrapper = wrapper.findModal('[data-testid="manage-user-modal"]')!; + + const cancelManageUserButton = wrapper.findButton('[data-testid="cancel-manage-user-button"]'); + + cancelManageUserButton?.click(); + + expect(modalWrapper.isVisible()).toBe(false); + }); + + it('should close manage user modal when close button is clicked for delegated admin', async () => { + // ARRANGE + const users = generateTestUsers(2); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + const rowIndex = findRowByUserType(table, 'delegated-admin'); + table!.findRowSelectionArea(rowIndex)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const modalWrapper = wrapper.findModal('[data-testid="manage-user-modal"]')!; + + const closeManageUserButton = wrapper.findButton('[data-testid="close-manage-user-button"]'); + + closeManageUserButton?.click(); + + // ASSERT + expect(modalWrapper.isVisible()).toBe(false); + }); + + it('should validate account IDs and show error for invalid input', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + const modal = wrapper.findModal('[data-testid="manage-user-modal"]'); + const saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + + // ACT - Test empty account IDs + textarea?.setTextareaValue(''); + + // ASSERT - Validate error message for empty input + expect(modal?.getElement()).toHaveTextContent('Please enter at least one account ID.'); + expect(saveButton?.getElement()).toBeDisabled(); + + // ACT - invalid account ID + textarea?.setTextareaValue('invalid-account-id'); + + // ASSERT + expect(modal?.getElement()).toHaveTextContent( + 'Invalid account IDs. Each account ID must be exactly 12 digits separated by commas.', + ); + expect(saveButton?.getElement()).toBeDisabled(); + }); + + it('should enable save button for valid account IDs', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + textarea?.setTextareaValue('123456789012, 123456789013'); + + // ASSERT + const saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + expect(saveButton?.getElement()).not.toBeDisabled(); + }); + + it('should close manage user modal without saving when account IDs are unchanged', async () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + const rowIndex = findRowByUserType(table, 'account-operator'); + table!.findRowSelectionArea(rowIndex)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const modalWrapper = wrapper.findModal('[data-testid="manage-user-modal"]')!; + + const saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + saveButton?.click(); + + expect(modalWrapper.isVisible()).toBe(false); + }); + + it('should send new account IDs from form field in API request when updating user', async () => { + // ARRANGE + const users = generateTestUsers(1); + const newAccountIds = ['999999999999', '888888888888']; + let capturedRequest: any = null; + + server.use( + http.put(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, async ({ request }) => { + capturedRequest = await request.json(); + return await ok({}); + }), + ); + + // ACT + const { store } = renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + textarea?.setTextareaValue(newAccountIds.join(', ')); + + const saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + saveButton?.click(); + + // ASSERT + await vi.waitFor(() => { + expect(capturedRequest).toBeDefined(); + expect(capturedRequest.accountIds).toEqual(newAccountIds); + }); + await vi.waitFor(() => { + const state = store.getState(); + const successNotification = state.notifications.notifications.find((n) => n.type === 'success'); + expect(successNotification).toBeDefined(); + }); + }); + + it('should update account IDs form when user data changes', () => { + // ARRANGE + const initialUsers = generateTestUsers(1); + const updatedUsers = [ + { + ...initialUsers[0], + accountIds: ['999999999999', '888888888888'], + }, + ]; + + // ACT + const store = setupStore(); + const { rerender } = render( + + + , + ); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + // Simulate data refresh + rerender( + + + , + ); + + // ASSERT + const textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + expect(textarea?.getTextareaValue()).toBe('999999999999, 888888888888'); + }); + + it('should display error alert when user update fails', async () => { + // ARRANGE + const users = generateTestUsers(1); + let capturedRequest: any = null; + server.use( + http.put( + `${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, + async ({ request }) => { + capturedRequest = await request.json(); + return Response.json({ message: 'first failure' }, { status: 400 }); + }, + { once: true }, + ), + http.put( + `${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, + async ({ request }) => { + capturedRequest = await request.json(); + return Response.json({ message: 'second failure' }, { status: 400 }); + }, + { once: true }, + ), + ); + + // ACT + const { store } = renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + let manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const modal = wrapper.findModal('[data-testid="manage-user-modal"]')!; + + let textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + textarea?.setTextareaValue('999999999999'); + + let saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + saveButton?.click(); + + // ASSERT + await vi.waitFor(() => { + expect(capturedRequest).toBeDefined(); + expect(capturedRequest.accountIds).toEqual(['999999999999']); + }); + await vi.waitFor(() => { + const state = store.getState(); + const notifications = state.notifications.notifications; + expect(notifications).toHaveLength(1); + expect(notifications[0]).toEqual( + expect.objectContaining({ + type: 'error', + content: 'Failed to update user: first failure', + }), + ); + }); + expect(modal.isVisible()).toBe(false); + + // ACT - second user update failure + + table!.findRowSelectionArea(1)!.click(); + + manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + + manageButton?.click(); + + textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + textarea?.setTextareaValue('123456789012,012345678901'); + + saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + + saveButton?.click(); + + await vi.waitFor(() => { + expect(capturedRequest).toBeDefined(); + expect(capturedRequest.accountIds).toEqual(['123456789012', '012345678901']); + }); + + // ASSERT - notification is re-rendered + await vi.waitFor(() => { + const state = store.getState(); + const notifications = state.notifications.notifications; + + expect(notifications).toHaveLength(2); + expect(notifications[1]).toEqual( + expect.objectContaining({ + type: 'error', + content: 'Failed to update user: second failure', + }), + ); + }); + }); + + it('should handle network error during user update', async () => { + // ARRANGE + const users = generateTestUsers(1); + let capturedRequest: any = null; + server.use( + http.put(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, async ({ request }) => { + capturedRequest = await request.json(); + return Response.error(); + }), + ); + + // ACT + const { store } = renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const modal = wrapper.findModal('[data-testid="manage-user-modal"]')!; + + const textarea = wrapper.findTextarea('[data-testid="owned-accounts-form-field"]'); + textarea?.setTextareaValue('999999999999'); + + const saveButton = wrapper.findButton('[data-testid="manage-user-save-button"]'); + saveButton?.click(); + + // ASSERT + await vi.waitFor(() => { + expect(capturedRequest).toBeDefined(); + expect(capturedRequest.accountIds).toEqual(['999999999999']); + }); + await vi.waitFor(() => { + const state = store.getState(); + const errorNotification = state.notifications.notifications.find((n) => n.type === 'error'); + expect(errorNotification).toBeDefined(); + expect(errorNotification?.content).toContain('Failed to update user'); + }); + expect(modal.isVisible()).toBe(false); + }); + + it('should open delete confirmation modal and hide manage user modal', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const manageModal = wrapper.findModal('[data-testid="manage-user-modal"]'); + expect(manageModal?.isVisible()).toBe(true); + + const deleteButton = manageModal?.findContent()?.findButton(); + deleteButton?.click(); + + // ASSERT + const deleteModal = wrapper + .findAllModals() + .find( + (modal) => + modal.getElement().textContent?.includes('Delete User') && + modal.getElement().textContent?.includes('Are you sure'), + ); + expect(deleteModal?.isVisible()).toBe(true); + expect(deleteModal?.getElement()).toHaveTextContent( + 'Are you sure you want to delete user user0@example.com? This action cannot be undone.', + ); + expect(manageModal?.isVisible()).toBe(false); + }); + + it('should close delete confirmation modal when Cancel button is clicked', () => { + // ARRANGE + const users = generateTestUsers(1); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const manageModal = wrapper.findModal('[data-testid="manage-user-modal"]'); + const deleteButton = manageModal?.findContent()?.findButton(); + deleteButton?.click(); + + const deleteModal = wrapper + .findAllModals() + .find( + (modal) => + modal.getElement().textContent?.includes('Delete User') && + modal.getElement().textContent?.includes('Are you sure'), + ); + const cancelButton = deleteModal + ?.findFooter() + ?.findAllButtons() + .find((button) => button.getElement().textContent === 'Cancel'); + cancelButton?.click(); + + // ASSERT + expect(deleteModal?.isVisible()).toBe(false); + expect(manageModal?.isVisible()).toBe(true); + }); + + it('should successfully delete user and close both modals', async () => { + // ARRANGE + const users = generateTestUsers(1); + server.use(http.delete(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, async () => await ok({}))); + + // ACT + const { store } = renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const manageModal = wrapper.findModal('[data-testid="manage-user-modal"]'); + const deleteButton = manageModal?.findContent()?.findButton(); + deleteButton?.click(); + + const deleteModal = wrapper + .findAllModals() + .find( + (modal) => + modal.getElement().textContent?.includes('Delete User') && + modal.getElement().textContent?.includes('Are you sure'), + ); + const confirmDeleteButton = deleteModal + ?.findFooter() + ?.findAllButtons() + .find((button) => button.getElement().textContent === 'Delete'); + confirmDeleteButton?.click(); + + // ASSERT + await vi.waitFor(() => { + const state = store.getState(); + const successNotification = state.notifications.notifications.find((n) => n.type === 'success'); + expect(successNotification).toBeDefined(); + expect(successNotification?.id).toBe('user-delete-success'); + }); + expect(mockRefresh).toHaveBeenCalledTimes(1); + expect(deleteModal?.isVisible()).toBe(false); + expect(manageModal?.isVisible()).toBe(false); + }); + + it('should show loading state on delete button during deletion', () => { + // ARRANGE + const users = generateTestUsers(1); + let resolveDelete: () => void; + const deletePromise = new Promise((resolve) => { + resolveDelete = resolve; + }); + server.use( + http.delete(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, async () => { + await deletePromise; + return await ok({}); + }), + ); + + // ACT + renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const manageModal = wrapper.findModal('[data-testid="manage-user-modal"]'); + const deleteButton = manageModal?.findContent()?.findButton(); + deleteButton?.click(); + + const deleteModal = wrapper + .findAllModals() + .find( + (modal) => + modal.getElement().textContent?.includes('Delete User') && + modal.getElement().textContent?.includes('Are you sure'), + ); + const confirmDeleteButton = deleteModal + ?.findFooter() + ?.findAllButtons() + .find((button) => button.getElement().textContent === 'Delete'); + confirmDeleteButton?.click(); + + // ASSERT + expect(confirmDeleteButton?.getElement()).toHaveAttribute('aria-disabled', 'true'); + + resolveDelete!(); + }); + + it('should handle delete error and close both modals', async () => { + // ARRANGE + const users = generateTestUsers(1); + server.use( + http.delete( + `${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, + async () => { + return Response.json({ message: 'first failure' }, { status: 400 }); + }, + { once: true }, + ), + http.delete( + `${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, + async () => { + return Response.json({ message: 'second failure' }, { status: 400 }); + }, + { once: true }, + ), + ); + + // ACT + const { store } = renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + let manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + let manageModal = wrapper.findModal('[data-testid="manage-user-modal"]'); + let deleteButton = manageModal?.findContent()?.findButton(); + deleteButton?.click(); + + let deleteModal = wrapper + .findAllModals() + .find( + (modal) => + modal.getElement().textContent?.includes('Delete User') && + modal.getElement().textContent?.includes('Are you sure'), + ); + let confirmDeleteButton = deleteModal + ?.findFooter() + ?.findAllButtons() + .find((button) => button.getElement().textContent === 'Delete'); + confirmDeleteButton?.click(); + + // ASSERT + await vi.waitFor(() => { + const state = store.getState(); + const notifications = state.notifications.notifications; + expect(notifications).toHaveLength(1); + expect(notifications[0]).toEqual( + expect.objectContaining({ + type: 'error', + content: 'Failed to delete user: first failure', + }), + ); + }); + expect(deleteModal?.isVisible()).toBe(false); + expect(manageModal?.isVisible()).toBe(false); + + // ACT - second deletion attempt + + table!.findRowSelectionArea(1)!.click(); + + manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + manageModal = wrapper.findModal('[data-testid="manage-user-modal"]'); + deleteButton = manageModal?.findContent()?.findButton(); + deleteButton?.click(); + + deleteModal = wrapper + .findAllModals() + .find( + (modal) => + modal.getElement().textContent?.includes('Delete User') && + modal.getElement().textContent?.includes('Are you sure'), + ); + confirmDeleteButton = deleteModal + ?.findFooter() + ?.findAllButtons() + .find((button) => button.getElement().textContent === 'Delete'); + confirmDeleteButton?.click(); + + // ASSERT + await vi.waitFor(() => { + const state = store.getState(); + const notifications = state.notifications.notifications; + expect(notifications).toHaveLength(2); + expect(notifications[1]).toEqual( + expect.objectContaining({ + type: 'error', + content: 'Failed to delete user: second failure', + }), + ); + }); + }); + + it('should handle delete network error and close both modals', async () => { + // ARRANGE + const users = generateTestUsers(1); + server.use( + http.delete(`${MOCK_SERVER_URL}${ApiEndpoints.USERS}/:email`, async () => { + return Response.error(); + }), + ); + + // ACT + const { store } = renderWithProvider(); + const wrapper = createWrapper(document.body); + + const table = wrapper.findTable(); + table!.findRowSelectionArea(1)!.click(); + + const manageButton = wrapper.findButton('[data-testid="manage-user-button"]'); + manageButton?.click(); + + const manageModal = wrapper.findModal('[data-testid="manage-user-modal"]'); + const deleteButton = manageModal?.findContent()?.findButton(); + deleteButton?.click(); + + const deleteModal = wrapper + .findAllModals() + .find( + (modal) => + modal.getElement().textContent?.includes('Delete User') && + modal.getElement().textContent?.includes('Are you sure'), + ); + const confirmDeleteButton = deleteModal + ?.findFooter() + ?.findAllButtons() + .find((button) => button.getElement().textContent === 'Delete'); + confirmDeleteButton?.click(); + + // ASSERT + await vi.waitFor(() => { + const state = store.getState(); + const errorNotification = state.notifications.notifications.find((n) => n.type === 'error'); + expect(errorNotification).toBeDefined(); + expect(errorNotification?.content).toContain('Failed to delete user'); + }); + expect(deleteModal?.isVisible()).toBe(false); + expect(manageModal?.isVisible()).toBe(false); + }); +}); diff --git a/source/webui/src/__tests__/server.ts b/source/webui/src/__tests__/server.ts new file mode 100644 index 00000000..7d5a5cf4 --- /dev/null +++ b/source/webui/src/__tests__/server.ts @@ -0,0 +1,9 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { setupServer } from 'msw/node'; +import { handlers } from '../mocks/handlers.ts'; + +// configures a mock server for unit tests. +// call server.use() in test to set up handlers. +export const MOCK_SERVER_URL = 'http://localhost:3001/'; +export const server = setupServer(...handlers(MOCK_SERVER_URL)); diff --git a/source/webui/src/__tests__/test-data-factory.ts b/source/webui/src/__tests__/test-data-factory.ts new file mode 100644 index 00000000..fd87ec16 --- /dev/null +++ b/source/webui/src/__tests__/test-data-factory.ts @@ -0,0 +1,119 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { add, sub } from 'date-fns'; +import { FindingApiResponse, RemediationHistoryApiResponse, User } from '@data-models'; +import { + randomAccountId, + randomAlias, + randomRemediationStatus, + randomSeverity, + randomWord, +} from './test-data-random-utils'; + +export const mockCurrentUser: User = { + email: 'current@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Confirmed', + type: 'admin', +}; + +export const mockUserContext = { + user: { username: 'testuser' } as any, + email: 'current@example.com', + groups: ['AdminGroup'], + signOut: () => Promise.resolve(), + checkUser: () => Promise.resolve(), + signInWithRedirect: () => Promise.resolve(), +}; + +// Functions to generate random test data for unit test and early stage UI development +export function generateTestRemediation(data?: Partial): RemediationHistoryApiResponse { + const id = window.crypto.randomUUID(); + return { + executionId: id, + findingId: id, + lastUpdatedTime: sub(new Date(), { + hours: Math.random() * 100, + minutes: Math.random() * 60, + }).toISOString(), + accountId: randomAccountId(), + remediationStatus: randomRemediationStatus(), + region: randomWord(5, 10), + resourceId: randomWord(30, 40), + resourceType: randomWord(10, 15), + resourceTypeNormalized: randomWord(10, 15), + findingType: randomWord(10, 15), + lastUpdatedBy: randomAlias(), + severity: randomSeverity(), + consoleLink: `https://console.aws.amazon.com/states/home?region=${randomWord(5, 10)}#/executions/details/${id}`, + ...data, + }; +} + +export function generateTestRemediations( + length: number, + data?: Partial, +): Array { + return Array.from({ length }).map(() => generateTestRemediation(data)); +} + +export function generateTestFinding(data?: Partial): FindingApiResponse { + const id = window.crypto.randomUUID(); + const creationTime = sub(new Date(), { + days: Math.floor(Math.random() * 30), + hours: Math.floor(Math.random() * 24), + }).toISOString(); + + return { + findingId: id, + findingDescription: randomWord(20, 100), + accountId: randomAccountId(), + resourceId: randomWord(30, 40), + resourceType: randomWord(8, 15), + resourceTypeNormalized: randomWord(8, 15), + findingType: randomWord(10, 15), + region: randomWord(5, 10), + severity: randomSeverity(), + remediationStatus: randomRemediationStatus(), + suppressed: Math.random() > 0.8, // 20% chance of being suppressed + creationTime: creationTime, + securityHubUpdatedAtTime: add(new Date(creationTime), { + hours: Math.floor(Math.random() * 24), + }).toISOString(), + lastUpdatedTime: add(new Date(creationTime), { + hours: Math.floor(Math.random() * 48), + }).toISOString(), + consoleLink: `https://console.aws.amazon.com/securityhub/home?region=${randomWord(5, 10)}#/findings/${id}`, + ...data, + }; +} + +export function generateTestFindings(length: number, data?: Partial): Array { + return Array.from({ length }).map(() => generateTestFinding(data)); +} + +export function generateTestUsers(count: number): User[] { + const users: User[] = []; + for (let i = 0; i < count; i++) { + if (i % 2 === 0) { + users.push({ + email: `user${i}@example.com`, + accountIds: ['123456789012', '123456789013'], + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Confirmed', + type: 'account-operator', + }); + } else { + users.push({ + email: `delegated${i}@example.com`, + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Invited', + type: 'delegated-admin', + }); + } + } + return users; +} diff --git a/source/webui/src/__tests__/test-data-random-utils.ts b/source/webui/src/__tests__/test-data-random-utils.ts new file mode 100644 index 00000000..176d3dbb --- /dev/null +++ b/source/webui/src/__tests__/test-data-random-utils.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Collection of functions to generate domain-independent quasi random data. + */ +const alphabet = 'abcdefghijklmnopqrstuvwxyz'; +const getRandomLetter = () => alphabet[randomInteger(alphabet.length)]; + +export function blindText(targetWordCount: number): string { + return Array.from({ length: targetWordCount }, randomWord).join(' '); +} + +export function randomWord(minLength = 5, maxLength = 10): string { + const difference = Math.abs(maxLength - minLength); + const wordLength = randomInteger(difference) + minLength; + + const word = Array.from({ length: wordLength }, getRandomLetter).join(''); + return word.charAt(0).toUpperCase() + word.slice(1); +} + +export function randomAlias() { + return randomWord().toLowerCase() + '@'; +} + +export function randomSentence(minLength = 1, maxLength = 5): string { + const difference = Math.abs(maxLength - minLength); + const numberOfWords = randomInteger(difference) + minLength; + const words = Array.from({ length: numberOfWords }, randomWord); + return words.join(' '); +} + +export function randomInteger(max: number) { + return Math.floor(Math.random() * max); +} + +export function randomDigit(max = 10) { + return randomInteger(max); +} + +export function randomAccountId() { + const safeTestAccountIds = ['111111111111', '222222222222', '333333333333', '123456789012']; + return safeTestAccountIds[Math.floor(Math.random() * safeTestAccountIds.length)]; +} + +const severityLevels = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFORMATIONAL'] as const; +const remediationStatuses = ['SUCCESS', 'FAILED', 'IN_PROGRESS', 'NOT_STARTED'] as const; + +export function randomRemediationStatus() { + return remediationStatuses[Math.floor(Math.random() * remediationStatuses.length)]; +} + +export function shuffle(array: T[]): T[] { + return array.sort(() => 0.5 - Math.random()); +} + +export function randomSeverity() { + return severityLevels[Math.floor(Math.random() * severityLevels.length)]; +} diff --git a/source/webui/src/__tests__/test-utils.tsx b/source/webui/src/__tests__/test-utils.tsx new file mode 100644 index 00000000..c9863ab2 --- /dev/null +++ b/source/webui/src/__tests__/test-utils.tsx @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { DEFAULT_INITIAL_STATE } from '../store/types.ts'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { NotificationContextProvider } from '../contexts/NotificationContext.tsx'; +import { ConfigContextProvider } from '../contexts/ConfigContext.tsx'; +import { MemoryRouter } from 'react-router-dom'; +import { AppRoutes } from '../AppRoutes.tsx'; +import { render } from '@testing-library/react'; +import { rootReducer, RootState } from '../store/store.ts'; +import { solutionApi } from '../store/solutionApi.ts'; + +/* + * Render a page within the context of a Router, redux store and NotificationContext. + * + * This function provides setup for component tests that + * - interact with the store state, + * -navigate between pages + * and/or + * - emit notifications. + */ +export function renderAppContent(props?: { + preloadedState?: Partial; + initialRoute: string; + config?: { ticketingEnabled: boolean }; +}) { + const store = configureStore({ + reducer: rootReducer, + preloadedState: props?.preloadedState ?? {}, + middleware: getDefaultMiddleware => getDefaultMiddleware().concat(solutionApi.middleware), + }); + + const defaultConfig = { ticketingEnabled: true }; + const config = props?.config ?? defaultConfig; + + const renderResult = render( + + + + + + + + + , + ); + return { + renderResult, + store, + }; +} diff --git a/source/webui/src/components/ActionsDropdown.tsx b/source/webui/src/components/ActionsDropdown.tsx new file mode 100644 index 00000000..007c09c5 --- /dev/null +++ b/source/webui/src/components/ActionsDropdown.tsx @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ButtonDropdown } from '@cloudscape-design/components'; +import { FindingApiResponse } from '@data-models'; +import { useConfig } from '../contexts/ConfigContext'; + +interface ActionsDropdownProps { + selectedItems: readonly FindingApiResponse[]; + onRemediate: (items: readonly FindingApiResponse[]) => void; + onRemediateAndGenerateTicket: (items: readonly FindingApiResponse[]) => void; + onSuppress: (items: readonly FindingApiResponse[]) => void; + onUnsuppress: (items: readonly FindingApiResponse[]) => void; +} + +export const ActionsDropdown = ({ + selectedItems, + onRemediate, + onRemediateAndGenerateTicket, + onSuppress, + onUnsuppress +}: ActionsDropdownProps) => { + const { ticketingEnabled } = useConfig(); + const isDisabled = selectedItems.length === 0; + const hasSuppressedItems = selectedItems.some(item => item.suppressed); + const hasUnsuppressedItems = selectedItems.some(item => !item.suppressed); + const hasInProgressOrSuccessItems = selectedItems.some(item => + item.remediationStatus === 'IN_PROGRESS' || item.remediationStatus === 'SUCCESS' + ); + + const dropdownItems = [ + { + id: 'remediate', + text: 'Remediate', + disabled: isDisabled || hasInProgressOrSuccessItems + }, + { + id: 'remediate-ticket', + text: 'Remediate & Generate Ticket', + disabled: isDisabled || hasInProgressOrSuccessItems || !ticketingEnabled + }, + { + id: 'suppress', + text: 'Suppress', + disabled: isDisabled || !hasUnsuppressedItems || hasInProgressOrSuccessItems + }, + { + id: 'unsuppress', + text: 'Unsuppress', + disabled: isDisabled || !hasSuppressedItems || hasInProgressOrSuccessItems + } + ]; + + const handleItemClick = ({ detail }: { detail: { id: string } }) => { + switch (detail.id) { + case 'remediate': + onRemediate(selectedItems); + break; + case 'remediate-ticket': + onRemediateAndGenerateTicket(selectedItems); + break; + case 'suppress': + onSuppress(selectedItems); + break; + case 'unsuppress': + onUnsuppress(selectedItems); + break; + } + }; + + return ( +
+ + Actions + +
+ ); +}; diff --git a/source/webui/src/components/EmptyTableState.tsx b/source/webui/src/components/EmptyTableState.tsx new file mode 100644 index 00000000..94347443 --- /dev/null +++ b/source/webui/src/components/EmptyTableState.tsx @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ReactNode } from 'react'; +import Box from '@cloudscape-design/components/box'; + +export const EmptyTableState = ({ + title, + subtitle, + action, +}: { + title: string; + subtitle: string; + action?: ReactNode; +}) => { + return ( + + + {title} + + + {subtitle} + + {action} + + ); +}; diff --git a/source/webui/src/components/ProtectedRoute.tsx b/source/webui/src/components/ProtectedRoute.tsx new file mode 100644 index 00000000..1092baf0 --- /dev/null +++ b/source/webui/src/components/ProtectedRoute.tsx @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useContext } from 'react'; +import { Navigate } from 'react-router-dom'; +import { UserContext } from '../contexts/UserContext.tsx'; +import { canAccessUsers } from '../utils/userPermissions.ts'; + +interface ProtectedRouteProps { + children: React.ReactNode; + requireUsersAccess?: boolean; +} + +export const ProtectedRoute = ({ children, requireUsersAccess = false }: ProtectedRouteProps) => { + const { groups } = useContext(UserContext); + + if (requireUsersAccess && !canAccessUsers(groups)) { + return ; + } + + return <>{children}; +}; diff --git a/source/webui/src/components/navigation/Breadcrumbs.tsx b/source/webui/src/components/navigation/Breadcrumbs.tsx new file mode 100644 index 00000000..8afbe869 --- /dev/null +++ b/source/webui/src/components/navigation/Breadcrumbs.tsx @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { BreadcrumbGroup, BreadcrumbGroupProps } from '@cloudscape-design/components'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { createBreadcrumbs } from './create-breadcrumbs.ts'; + +export const Breadcrumbs = () => { + const location = useLocation(); + const navigate = useNavigate(); + const path = location.pathname; + + const breadCrumbItems = createBreadcrumbs(path); + + return ( + + ); +}; diff --git a/source/webui/src/components/navigation/SideNavigationBar.tsx b/source/webui/src/components/navigation/SideNavigationBar.tsx new file mode 100644 index 00000000..a166672e --- /dev/null +++ b/source/webui/src/components/navigation/SideNavigationBar.tsx @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SideNavigation, SideNavigationProps } from '@cloudscape-design/components'; +import { NavigateFunction, useLocation, useNavigate } from 'react-router-dom'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { UserContext } from '../../contexts/UserContext.tsx'; +import { canAccessUsers } from '../../utils/userPermissions.ts'; + +export default function SideNavigationBar() { + const navigate: NavigateFunction = useNavigate(); + const [activeHref, setActiveHref] = useState('/'); + const { groups } = useContext(UserContext); + + const navigationItems: SideNavigationProps['items'] = [ + { + type: 'section-group', + title: 'Remediate', + items: [ + { type: 'link', text: 'Findings', href: '/findings' }, + { type: 'link', text: 'Execution History', href: '/history' }, + ], + }, + { type: 'divider' }, + ...(canAccessUsers(groups) + ? [ + { + type: 'section-group' as const, + title: 'Access Control', + items: [ + { type: 'link' as const, text: 'Invite Users', href: '/invite' }, + { type: 'link' as const, text: 'View Users', href: '/users' }, + ], + }, + { type: 'divider' as const }, + ] + : []), + { + type: 'link', + external: true, + href: 'https://docs.aws.amazon.com/solutions/latest/automated-security-response-on-aws/solution-overview.html', + text: 'Documentation', + }, + ]; + + // follow the given router link and update the store with active path + const handleFollow = useCallback( + (event: Readonly): void => { + if (event.detail.external || !event.detail.href) return; + + event.preventDefault(); + + const path = event.detail.href; + navigate(path); + }, + [navigate], + ); + + const location = useLocation(); + useEffect(() => { + const pathParts = location.pathname.split('/'); + const topLevelPath = pathParts.length > 1 ? `/${pathParts[1]}` : '/'; + setActiveHref(topLevelPath); + }, [location]); + + const navHeader: SideNavigationProps.Header = { + href: '/', + text: 'Automated Security Response on AWS', + }; + + return ; +} diff --git a/source/webui/src/components/navigation/TopNavigationBar.tsx b/source/webui/src/components/navigation/TopNavigationBar.tsx new file mode 100644 index 00000000..f8114f19 --- /dev/null +++ b/source/webui/src/components/navigation/TopNavigationBar.tsx @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { TopNavigation, TopNavigationProps } from '@cloudscape-design/components'; +import { useContext } from 'react'; +import { UserContext } from '../../contexts/UserContext.tsx'; + +export default function TopNavigationBar() { + const { user, email, signOut } = useContext(UserContext); + + const solutionIdentity: TopNavigationProps.Identity = { + href: '/', + logo: { src: '/aws-logo.svg', alt: 'AWS' }, + }; + + const i18nStrings: TopNavigationProps.I18nStrings = { + overflowMenuTitleText: 'All', + overflowMenuTriggerText: 'More', + }; + + const utilities: TopNavigationProps.Utility[] = [ + { + type: 'menu-dropdown', + text: email ?? user?.username ?? 'User', + iconName: 'user-profile', + items: [ + { + id: 'documentation', + text: 'Documentation', + href: 'https://docs.aws.amazon.com/solutions/latest/automated-security-response-on-aws/solution-overview.html', + external: true, + externalIconAriaLabel: ' (opens in new tab)', + }, + { + id: 'signout', + text: 'Sign Out', + }, + ], + onItemClick: async (event) => { + if (event.detail.id === 'signout') { + await signOut(); + } + }, + }, + ]; + + return ; +} diff --git a/source/webui/src/components/navigation/create-breadcrumbs.ts b/source/webui/src/components/navigation/create-breadcrumbs.ts new file mode 100644 index 00000000..588b5ae9 --- /dev/null +++ b/source/webui/src/components/navigation/create-breadcrumbs.ts @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// build an array of breadcrumb items, one for each element of the given path +import { BreadcrumbGroupProps } from '@cloudscape-design/components'; + +export const createBreadcrumbs = (path: string): BreadcrumbGroupProps.Item[] => { + const pathElements: string[] = path.split('/'); + + return pathElements.map((currentElement, index) => { + const previousPathElementsPlusCurrent = pathElements.slice(0, index + 1); + let href = `${previousPathElementsPlusCurrent.join('/')}`; + // Make Home breadcrumb point to /findings + if (currentElement === '' && index === 0) { + href = '/findings'; + } + + return { text: getLabelForPathElement(currentElement), href }; + }); +}; + +// Mapping of router path to breadcrumb label +const pathLabels: Record = { + '': 'Home', + home: 'Home', + history: 'Execution History', + findings: 'Findings', + users: 'View Users', + invite: 'Invite', +}; + +function getLabelForPathElement(pathElement: string): string { + const pathLabel = pathLabels[pathElement]; + if (pathLabel) return pathLabel; + + // 'Details' is supposed to be used for the uuids that are part of the route + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(pathElement)) return 'Details'; + + return pathElement; +} diff --git a/source/webui/src/contexts/ConfigContext.tsx b/source/webui/src/contexts/ConfigContext.tsx new file mode 100644 index 00000000..9b78dcad --- /dev/null +++ b/source/webui/src/contexts/ConfigContext.tsx @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { createContext, useContext, ReactNode } from 'react'; + +interface ConfigContextType { + ticketingEnabled: boolean; +} + +const ConfigContext = createContext(undefined); + +interface ConfigContextProviderProps { + children: ReactNode; + config: ConfigContextType; +} + +export const ConfigContextProvider: React.FC = ({ children, config }) => { + return ( + + {children} + + ); +}; + +export const useConfig = (): ConfigContextType => { + const context = useContext(ConfigContext); + if (context === undefined) { + throw new Error('useConfig must be used within a ConfigContextProvider'); + } + return context; +}; diff --git a/source/webui/src/contexts/NotificationContext.tsx b/source/webui/src/contexts/NotificationContext.tsx new file mode 100644 index 00000000..8aa47570 --- /dev/null +++ b/source/webui/src/contexts/NotificationContext.tsx @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { createContext, ReactNode, useEffect, useState } from 'react'; +import { FlashbarProps } from '@cloudscape-design/components'; +import { useDispatch, useSelector } from 'react-redux'; +import { deleteNotification, selectNotifications } from '../store/notificationsSlice.ts'; + +/** + * NotificationContext provides the notifications to the global FlashBar + * and any component that needs to use them. + * + * The notifications are stored in the redux store, + * but NotificationContext adds the onDismiss method to each notification object + * which is not serializable and cannot be stored in redux. + */ +export type NotificationContextType = { + notifications: ReadonlyArray; +}; + +export const NotificationContext = createContext( + null as unknown as NotificationContextType, +); +export const NotificationContextProvider = (props: { children: ReactNode }) => { + const storeNotifications = useSelector(selectNotifications); + const dispatch = useDispatch(); + + const initialState: ReadonlyArray = []; + const [notifications, setNotifications] = useState(initialState); + + useEffect(() => { + setNotifications( + storeNotifications.map(it => { + return { + dismissible: true, + onDismiss: () => dispatch(deleteNotification({ id: it.id })), + ...it, + }; + }), + ); + }, [storeNotifications]); + + return ( + <> + + {props.children} + + + ); +}; diff --git a/source/webui/src/contexts/UserContext.tsx b/source/webui/src/contexts/UserContext.tsx new file mode 100644 index 00000000..a29415ad --- /dev/null +++ b/source/webui/src/contexts/UserContext.tsx @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { createContext, ReactNode, useEffect, useState } from 'react'; +import { + AuthUser, + fetchUserAttributes, + getCurrentUser, + signInWithRedirect, + signOut, + fetchAuthSession, +} from 'aws-amplify/auth'; +import { Hub } from 'aws-amplify/utils'; + +export const UserContext = createContext<{ + user: AuthUser | null; + email: string | null; + groups: string[] | null; + signOut: () => Promise; + signInWithRedirect: () => Promise; + checkUser: () => Promise; +}>({ + user: null, + email: null, + groups: [], + signOut: () => Promise.resolve(), + signInWithRedirect: () => Promise.resolve(), + checkUser: () => Promise.resolve(), +}); + +export const UserContextProvider = (props: { children: ReactNode }) => { + const [user, setUser] = useState(null); + const [groups, setGroups] = useState(null); + const [email, setEmail] = useState(null); + + useEffect(() => { + Hub.listen('auth', ({ payload }) => { + switch (payload.event) { + case 'signInWithRedirect': + checkUser(); + break; + case 'signedOut': + setUser(null); + break; + } + }); + + // Don't call checkUser immediately on callback page - let CallbackPage handle it + const isCallbackPage = window.location.pathname === '/callback'; + if (!isCallbackPage) { + checkUser(); + } + }, []); + + const checkUser = async () => { + try { + const responseUser: AuthUser | null = await getCurrentUser(); + setUser({ + ...responseUser, + }); + try { + const userAttributesOutput = await fetchUserAttributes(); + setEmail(userAttributesOutput.email ?? null); + + const authSession = await fetchAuthSession(); + const groups = authSession.tokens?.accessToken.payload['cognito:groups'] as string[]; + setGroups(groups); + } catch (e) { + console.log(e); + } + } catch (error) { + console.error(error); + setUser(null); + setEmail(null); + setGroups(null); + + const isCallbackPage = window.location.pathname === '/callback'; + if (!isCallbackPage) { + try { + await signInWithRedirect(); + } catch (signInError) { + console.debug('Sign in error:', signInError); + } + } + } + }; + + return ( + + {props.children} + + ); +}; diff --git a/source/webui/src/main.tsx b/source/webui/src/main.tsx new file mode 100644 index 00000000..68d2ee88 --- /dev/null +++ b/source/webui/src/main.tsx @@ -0,0 +1,100 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Amplify, ResourcesConfig } from 'aws-amplify'; +import { cognitoUserPoolsTokenProvider } from 'aws-amplify/auth/cognito'; +import { sessionStorage } from 'aws-amplify/utils'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { AppComponent } from './App.tsx'; +import { ConfigContextProvider } from './contexts/ConfigContext.tsx'; +import { NotificationContextProvider } from './contexts/NotificationContext.tsx'; +import { UserContextProvider } from './contexts/UserContext.tsx'; +import { startMockServer } from './mocks/browser.ts'; +import { setupStore } from './store/store.ts'; +import './styles.css'; + +/** + * Read the configuration .json file that was generated by the custom resource during deployment. + * If running in development mode, also enable mock-service-worker to intercept defined http requests. + */ +const getRuntimeConfig = async () => { + let runtimeConfig: any = {}; + try { + const response = await fetch('/aws-exports.json'); + runtimeConfig = await response.json(); + } catch (e) { + console.log(e); + } + + if (process.env.NODE_ENV === 'development') await startMockServer(runtimeConfig.API?.endpoints?.[0]?.endpoint); + + return runtimeConfig; +}; + +getRuntimeConfig().then((json) => { + const awsconfig: ResourcesConfig = { + Auth: { + Cognito: { + userPoolId: json.Auth?.userPoolId, + userPoolClientId: json.Auth?.userPoolWebClientId, + loginWith: { + oauth: { + domain: json.Auth?.oauth?.domain, + redirectSignIn: [json.Auth?.oauth?.redirectSignIn], + redirectSignOut: [json.Auth?.oauth?.redirectSignOut], + responseType: 'code', + scopes: json.Auth?.oauth?.scope, + providers: [], + }, + }, + }, + }, + API: { + REST: { + 'solution-api': { + endpoint: json.API?.endpoints?.[0]?.endpoint, + }, + }, + }, + }; + console.log(awsconfig); + Amplify.configure(awsconfig); + + // Configure session storage for auth tokens + cognitoUserPoolsTokenProvider.setKeyValueStorage(sessionStorage); + + const store = setupStore(); + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + + // if auth is not configured, render the frontend without authentication for local UI development. + // will not be able to connect to any backend / API. + const isAuthConfigured = !!awsconfig.Auth?.Cognito.userPoolId; + + // Extract configuration for the app + const appConfig = { + ticketingEnabled: json.ticketingEnabled === 'true' + }; + + root.render( + + + + + + {isAuthConfigured ? ( + + + + ) : ( + + )} + + + + + , + ); +}); diff --git a/source/webui/src/mocks/browser.ts b/source/webui/src/mocks/browser.ts new file mode 100644 index 00000000..306646c2 --- /dev/null +++ b/source/webui/src/mocks/browser.ts @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Amplify } from 'aws-amplify'; + +/** + * This function enables mock-service-worker (msw) in the browser, so you can do local frontend development against the mock handlers. + * + * Only if aws-exports.json file is NOT present or does NOT contain the API endpoint config, msw will be enabled. + * If the API config is present, requests will be sent to the API. + */ +export async function startMockServer(apiEndpoint: string) { + // if apiEndpoint is provided from aws-exports.json, do not enable mocking + const isBackendConfigured = !!apiEndpoint; + if (isBackendConfigured) { + console.log('🚫 MSW disabled - Backend API endpoint configured:', apiEndpoint); + return Promise.resolve(); + } + + console.log('🔧 MSW enabled - No backend API endpoint found, using mocks'); + + const { setupWorker } = await import('msw/browser'); + const { handlers } = await import('./handlers'); + + const worker = setupWorker(...handlers(apiEndpoint)); + // `worker.start()` returns a Promise that resolves + // once the Service Worker is up and ready to intercept requests. + return worker.start({ + onUnhandledRequest(request, print) { + // Print MSW unhandled request warning, to detect requests that are not handled by MSW + print.warning(); + }, + }); +} diff --git a/source/webui/src/mocks/handlers.ts b/source/webui/src/mocks/handlers.ts new file mode 100644 index 00000000..9ed677f4 --- /dev/null +++ b/source/webui/src/mocks/handlers.ts @@ -0,0 +1,302 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { FindingApiResponse, RemediationHistoryApiResponse } from '@data-models'; +import { delay, http, HttpResponse } from 'msw'; +import { generateTestFindings, generateTestRemediations } from '../__tests__/test-data-factory'; +import { ApiEndpoints } from '../store/solutionApi.ts'; + +// This file contains msw mocks of ASR's API endpoints. +// the mocks can be used for unit tests, as well as local development as long as no backend is available + +/** + * Return a 200 OK http response with the given payload. + * Delays the response by 200ms to simulate realistic latency and allow + * to test a loading spinner etc on the UI. + */ +export const ok = async (payload: object | object[], delayMilliseconds: number = 200) => { + await delay(delayMilliseconds); + return HttpResponse.json(payload, { + status: 200, + headers: [['Access-Control-Allow-Origin', '*']], + }); +}; + +const badRequest = async (payload: object | object[], delayMilliseconds: number = 200) => { + await delay(delayMilliseconds); + return HttpResponse.json(payload, { + status: 400, + headers: [['Access-Control-Allow-Origin', '*']], + }); +}; + +export const postFindingsHandler = (apiUrl: string) => + http.post(apiUrl + ApiEndpoints.FINDINGS, async ({ request }) => { + const searchRequest = (await request.json()) as any; + console.log('MSW: Handling POST /findings request:', searchRequest); + + let filteredFindings = [...mockFindings]; + + if (searchRequest.Filters?.StringFilters) { + searchRequest.Filters.StringFilters.forEach((filter: any) => { + filteredFindings = filteredFindings.filter((finding: FindingApiResponse) => { + const fieldValue = (finding as any)[filter.FieldName]; + if (!fieldValue) return false; + + const value = fieldValue.toString().toLowerCase(); + const filterValue = filter.Filter.Value.toLowerCase(); + + switch (filter.Filter.Comparison) { + case 'EQUALS': + return value === filterValue; + case 'NOT_EQUALS': + return value !== filterValue; + case 'CONTAINS': + return value.includes(filterValue); + case 'NOT_CONTAINS': + return !value.includes(filterValue); + default: + return true; + } + }); + }); + } + + const maxResults = searchRequest.MaxResults || 20; + let startIndex = 0; + + if (searchRequest.NextToken) { + try { + const decodedToken = atob(searchRequest.NextToken); + const tokenData = JSON.parse(decodedToken); + + if (tokenData.id && tokenData.securityHubUpdatedAtTime) { + const lastItemIndex = filteredFindings.findIndex((f) => f.findingId === tokenData.id); + startIndex = lastItemIndex >= 0 ? lastItemIndex + 1 : 0; + } else if (tokenData.startIndex !== undefined) { + startIndex = tokenData.startIndex; + } + + console.log('MSW: Parsed NextToken:', { tokenData, startIndex }); + } catch (error) { + console.warn('MSW: Invalid NextToken, starting from beginning:', error); + startIndex = 0; + } + } + + const endIndex = startIndex + maxResults; + const paginatedFindings = filteredFindings.slice(startIndex, endIndex); + + let nextToken: string | undefined; + if (endIndex < filteredFindings.length) { + const lastItem = paginatedFindings[paginatedFindings.length - 1]; + + const lastEvaluatedKey = { + id: lastItem.findingId, + securityHubUpdatedAtTime: lastItem.securityHubUpdatedAtTime, + FindingType: lastItem.findingType, + FindingId: lastItem.findingId, + 'securityHubUpdatedAtTime#findingId': `${lastItem.securityHubUpdatedAtTime}#${lastItem.findingId}`, + FINDING_CONSTANT: 'finding', + }; + + nextToken = btoa(JSON.stringify(lastEvaluatedKey)); + } + + return ok({ + Findings: paginatedFindings, + NextToken: nextToken, + }); + }); + +export const putFindingsHandler = (apiUrl: string) => + http.put(`${apiUrl + ApiEndpoints.FINDINGS}/{id}`, async ({ request }) => { + const findingUpdateRequest = (await request.json()) as any; + return ok({ id: window.crypto.randomUUID(), ...findingUpdateRequest }); + }); + +export const getRemediationsHandler = (apiUrl: string) => + http.get(apiUrl + ApiEndpoints.REMEDIATIONS, () => { + return ok(mockRemediations); + }); + +export const postRemediationHandler = (apiUrl: string) => + http.put(apiUrl + ApiEndpoints.REMEDIATIONS, async ({ request }) => { + const remediationCreateRequest = (await request.json()) as any; + return ok({ id: window.crypto.randomUUID(), ...remediationCreateRequest }); + }); + +export const postRemediationsSearchHandler = (apiUrl: string) => + http.post(apiUrl + ApiEndpoints.REMEDIATIONS, async ({ request }) => { + const searchRequest = (await request.json()) as any; + console.log('MSW: Handling POST /remediations request:', searchRequest); + + let filteredRemediations = [...mockRemediations]; + + if (searchRequest.Filters?.CompositeFilters) { + searchRequest.Filters.CompositeFilters.forEach((compositeFilter: any) => { + if (compositeFilter.StringFilters) { + compositeFilter.StringFilters.forEach((filter: any) => { + filteredRemediations = filteredRemediations.filter((remediation: RemediationHistoryApiResponse) => { + const fieldValue = (remediation as any)[filter.FieldName]; + if (!fieldValue) return false; + + const value = fieldValue.toString().toLowerCase(); + const filterValue = filter.Filter.Value.toLowerCase(); + + switch (filter.Filter.Comparison) { + case 'EQUALS': + return value === filterValue; + case 'NOT_EQUALS': + return value !== filterValue; + case 'CONTAINS': + return value.includes(filterValue); + case 'NOT_CONTAINS': + return !value.includes(filterValue); + default: + return true; + } + }); + }); + } + }); + } + + if (searchRequest.SortCriteria && searchRequest.SortCriteria.length > 0) { + const sortCriteria = searchRequest.SortCriteria[0]; + const sortField = sortCriteria.Field; + const sortOrder = sortCriteria.SortOrder; + + filteredRemediations.sort((a, b) => { + const aValue = (a as any)[sortField]; + const bValue = (b as any)[sortField]; + + let comparison = 0; + if (aValue < bValue) comparison = -1; + if (aValue > bValue) comparison = 1; + + return sortOrder === 'desc' ? -comparison : comparison; + }); + } + + const maxResults = searchRequest.MaxResults || 20; + let startIndex = 0; + + if (searchRequest.NextToken) { + try { + const decodedToken = atob(searchRequest.NextToken); + const tokenData = JSON.parse(decodedToken); + + if (tokenData.id && tokenData.lastUpdatedTime) { + const lastItemIndex = filteredRemediations.findIndex((r) => r.executionId === tokenData.id); + startIndex = lastItemIndex >= 0 ? lastItemIndex + 1 : 0; + } else if (tokenData.startIndex !== undefined) { + startIndex = tokenData.startIndex; + } + + console.log('MSW: Parsed NextToken:', { tokenData, startIndex }); + } catch (error) { + console.warn('MSW: Invalid NextToken, starting from beginning:', error); + startIndex = 0; + } + } + + const endIndex = startIndex + maxResults; + const paginatedRemediations = filteredRemediations.slice(startIndex, endIndex); + + let nextToken: string | undefined; + if (endIndex < filteredRemediations.length) { + const lastItem = paginatedRemediations[paginatedRemediations.length - 1]; + + const lastEvaluatedKey = { + executionId: lastItem.executionId, + lastUpdatedTime: lastItem.lastUpdatedTime, + findingId: lastItem.findingId, + 'lastUpdatedTime#findingId': `${lastItem.lastUpdatedTime}#${lastItem.findingId}`, + REMEDIATION_CONSTANT: 'remediation', + }; + + nextToken = btoa(JSON.stringify(lastEvaluatedKey)); + } + + return ok({ + Remediations: paginatedRemediations, + NextToken: nextToken, + }); + }); + +export const getUserSelfHandler = (apiUrl: string) => + http.get(apiUrl + ApiEndpoints.USERS, () => { + return ok({ alias: 'john_doe' }); + }); + +export const getUsersHandler = (apiUrl: string) => + http.get(apiUrl + ApiEndpoints.USERS, () => { + return ok([ + { + email: 'operator1@example.com', + accountIds: ['123456789012', '123456789013'], + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + userStatus: 'ACTIVE', + }, + { + email: 'delegated1@example.com', + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + userStatus: 'PENDING', + }, + ]); + }); + +export const getUserByIdHandler = (apiUrl: string) => + http.get(`${apiUrl + ApiEndpoints.USERS}/:id`, ({ params }) => { + const { id } = params; + const decodedId = decodeURIComponent(id as string); + const mockUsers = { + 'operator1@example.com': { + email: 'operator1@example.com', + type: 'account-operator', + accountIds: ['123456789012', '123456789013'], + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Confirmed', + }, + 'delegated1@example.com': { + email: 'delegated1@example.com', + type: 'delegated-admin', + invitedBy: 'admin@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Invited', + }, + 'admin@example.com': { + email: 'admin@example.com', + type: 'admin', + invitedBy: 'system@example.com', + invitationTimestamp: new Date().toISOString(), + status: 'Confirmed', + }, + }; + + const user = mockUsers[decodedId as keyof typeof mockUsers]; + return user ? ok(user) : badRequest({ error: 'User not found' }); + }); + +/** + * @param apiUrl the base url for http requests. only requests to this base url will be intercepted and handled by mock-service-worker. + */ +export const handlers = (apiUrl: string) => [ + getUserSelfHandler(apiUrl), + getUsersHandler(apiUrl), + getUserByIdHandler(apiUrl), + getRemediationsHandler(apiUrl), + postRemediationHandler(apiUrl), + postRemediationsSearchHandler(apiUrl), + postFindingsHandler(apiUrl), + putFindingsHandler(apiUrl), +]; + +export const mockRemediations: RemediationHistoryApiResponse[] = generateTestRemediations(100); + +// for each org, generate between 5 and 10 portfolios +export const mockFindings: FindingApiResponse[] = generateTestFindings(100); diff --git a/source/webui/src/pages/callback/CallbackPage.tsx b/source/webui/src/pages/callback/CallbackPage.tsx new file mode 100644 index 00000000..123ed08a --- /dev/null +++ b/source/webui/src/pages/callback/CallbackPage.tsx @@ -0,0 +1,101 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useContext, useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Container, Header, Alert, Spinner, Button, SpaceBetween, Box } from '@cloudscape-design/components'; +import { UserContext } from '../../contexts/UserContext.tsx'; + +const SolutionHeader = () =>
Automated Security Response on AWS
; + +export const CallbackPage = () => { + const { user, checkUser } = useContext(UserContext); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + const error = searchParams.get('error'); + const errorDescription = searchParams.get('error_description'); + const [showFailsafe, setShowFailsafe] = useState(false); + + useEffect(() => { + // Wait for Amplify to process the authorization code before checking user + if (!error && !errorDescription) { + // Give Amplify time to process the authorization code + const timer = setTimeout(() => { + checkUser(); + }, 1000); + + return () => clearTimeout(timer); + } + }, [checkUser, error, errorDescription]); + + useEffect(() => { + // If user is authenticated and no error, redirect to home + if (user && !error) { + navigate('/'); + } + }, [user, error, navigate]); + + useEffect(() => { + // Show failsafe redirect button after 10 seconds if no error is present + if (!error && !errorDescription) { + const timer = setTimeout(() => setShowFailsafe(true), 10000); + return () => clearTimeout(timer); + } + }, [error, errorDescription]); + + if (error || errorDescription) { + return ( + + + + + + {errorDescription || 'An authentication error occurred.'} + + + Please ensure you have been invited by an existing Admin or Delegated Admin user, and you are logging-in + with the same email address where you received the invitation. + + + + + + ); + } + + if (!user) { + return ( + + + + +
Signing you in...
+ + {showFailsafe && ( + + )} +
+
+
+ ); + } + + return null; +}; diff --git a/source/webui/src/pages/findings/FindingsOverviewPage.tsx b/source/webui/src/pages/findings/FindingsOverviewPage.tsx new file mode 100644 index 00000000..d411da2b --- /dev/null +++ b/source/webui/src/pages/findings/FindingsOverviewPage.tsx @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import FindingsTable from './findings-table/FindingsTable.tsx'; + +export const FindingsOverviewPage = () => { + return ; +}; diff --git a/source/webui/src/pages/findings/findings-table/FindingsTable.tsx b/source/webui/src/pages/findings/findings-table/FindingsTable.tsx new file mode 100644 index 00000000..36275cbe --- /dev/null +++ b/source/webui/src/pages/findings/findings-table/FindingsTable.tsx @@ -0,0 +1,937 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import CollectionPreferences, { + CollectionPreferencesProps, +} from '@cloudscape-design/components/collection-preferences'; +import Header from '@cloudscape-design/components/header'; +import PropertyFilter, { PropertyFilterProps } from '@cloudscape-design/components/property-filter'; +import Table from '@cloudscape-design/components/table'; + +import Alert from '@cloudscape-design/components/alert'; +import Box from '@cloudscape-design/components/box'; +import Button from '@cloudscape-design/components/button'; +import Modal from '@cloudscape-design/components/modal'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import Spinner from '@cloudscape-design/components/spinner'; +import Toggle from '@cloudscape-design/components/toggle'; +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { ActionsDropdown } from '../../../components/ActionsDropdown.tsx'; +import { EmptyTableState } from '../../../components/EmptyTableState.tsx'; +import { FindingApiResponse } from '@data-models'; +import { + useExecuteActionMutation, + useLazySearchFindingsQuery +} from '../../../store/findingsApiSlice.ts'; +import { CompositeFilter, SearchRequest, StringFilter } from '../../../store/types.ts'; +import { getErrorMessage } from '../../../utils/error.ts'; +import { createColumnDefinitions } from './createColumnDefinitions.tsx'; + +const getFilterCounterText = (count = 0) => `${count} ${count === 1 ? 'match' : 'matches'}`; +const getHeaderCounterText = (items: readonly FindingApiResponse[] = [], selectedItems: readonly FindingApiResponse[] = []) => { + return selectedItems && selectedItems.length > 0 ? `(${selectedItems.length}/${items.length})` : `(${items.length})`; +}; + +export interface FindingsTableProps { +} + +export default function FindingsTable() { + const navigate = useNavigate(); + useDispatch(); + + // State management + const [preferences, setPreferences] = useState({ + wrapLines: true, + stripedRows: false, + contentDensity: 'comfortable', + }); + const [selectedItems, setSelectedItems] = useState([]); + const [sortingColumn, setSortingColumn] = useState(() => { + const columns = createColumnDefinitions(navigate); + return columns.find(col => col.sortingField === 'securityHubUpdatedAtTime')!; + }); + const [sortingDescending, setSortingDescending] = useState(true); + const [filterTokens, setFilterTokens] = useState([]); + const [filterOperation, setFilterOperation] = useState<'and' | 'or'>('and'); + + const [allFindings, setAllFindings] = useState([]); + const [nextToken, setNextToken] = useState(); + const [hasMoreData, setHasMoreData] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [operationType, setOperationType] = useState<'initial' | 'refresh' | 'filter' | 'loadMore'>('initial'); + const [showSuppressed, setShowSuppressed] = useState(false); + + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [pendingAction, setPendingAction] = useState<{ + type: 'remediate' | 'remediateAndTicket' | 'suppress' | 'unsuppress'; + items: readonly FindingApiResponse[]; + } | null>(null); + + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + // Ref for scroll detection + const tableContainerRef = useRef(null); + const loadMoreTriggerRef = useRef(null); + + const [searchFindings, { data: searchResult, isLoading, error: searchError }] = useLazySearchFindingsQuery(); + const [executeAction, { isLoading: isExecutingAction }] = useExecuteActionMutation(); + + const getComparisonOperator = (operator: string): 'EQUALS' | 'NOT_EQUALS' | 'CONTAINS' | 'NOT_CONTAINS' => { + switch (operator) { + case '=': + return 'EQUALS'; + case '!=': + return 'NOT_EQUALS'; + case ':': + return 'CONTAINS'; + case '!:': + return 'NOT_CONTAINS'; + default: + return 'EQUALS'; + } + }; + + const unformatStatus = (formattedStatus: string) => { + // Convert formatted status back to uppercase with underscores for API call + const statusMap: { [key: string]: string } = { + 'Success': 'SUCCESS', + 'Failed': 'FAILED', + 'Not Started': 'NOT_STARTED', + 'In Progress': 'IN_PROGRESS' + }; + + return statusMap[formattedStatus] || formattedStatus.toUpperCase().replace(/\s+/g, '_'); + }; + + const convertTokensToFilters = (tokens: PropertyFilterProps.Token[], operation: string): SearchRequest['Filters'] => { + if (!tokens?.length) return undefined; + + const fieldGroups: { [fieldName: string]: StringFilter[] } = {}; + + tokens.forEach(token => { + const comparison = getComparisonOperator(token.operator || '='); + + // Convert formatted remediation status values back to raw values for API + let filterValue = token.value || ''; + if (token.propertyKey === 'remediationStatus') { + filterValue = unformatStatus(filterValue); + } + + const filter: StringFilter = { + FieldName: token.propertyKey || '', + Filter: { + Value: filterValue, + Comparison: comparison, + }, + }; + + const fieldName = token.propertyKey || ''; + if (!fieldGroups[fieldName]) { + fieldGroups[fieldName] = []; + } + fieldGroups[fieldName].push(filter); + }); + + // Convert field groups to CompositeFilters + // Each field group becomes a CompositeFilter with OR operator (same field = OR) + // Different CompositeFilters are combined with AND (different fields = AND) + const compositeFilters: CompositeFilter[] = Object.entries(fieldGroups).map(([fieldName, filters]) => ({ + Operator: 'OR' as const, // Same field filters use OR + StringFilters: filters, + })); + + return { + CompositeFilters: compositeFilters.length > 0 ? compositeFilters : undefined, + CompositeOperator: 'AND', + }; + }; + + const buildSearchRequest = (useNextToken: boolean = false): SearchRequest => { + const filters = convertTokensToFilters(filterTokens, filterOperation); + + const request: SearchRequest = { + Filters: filters, + SortCriteria: [{ + Field: sortingColumn.sortingField || 'securityHubUpdatedAtTime', + SortOrder: sortingDescending ? 'desc' : 'asc', + }], + }; + + // Add NextToken for loading more data + if (useNextToken && nextToken) { + request.NextToken = nextToken; + } + + return request; + }; + + // Initial load on component mount + useEffect(() => { + setOperationType('initial'); + const searchRequest = buildSearchRequest(false); + searchFindings(searchRequest); + }, []); + + // Reload when filters or sorting change + useEffect(() => { + setOperationType('filter'); + setAllFindings([]); + setNextToken(undefined); + setHasMoreData(false); + + const searchRequest = buildSearchRequest(false); + searchFindings(searchRequest); + }, [filterTokens, filterOperation, sortingColumn, sortingDescending]); + + // Update state when search results change + useEffect(() => { + if (searchResult) { + if (operationType === 'loadMore') { + setAllFindings(prev => { + const existingIds = new Set(prev.map(f => f.findingId)); + const newFindings = searchResult.Findings.filter(f => !existingIds.has(f.findingId)); + return [...prev, ...newFindings]; + }); + setIsLoadingMore(false); + } else { + // Replace findings (initial, refresh, or filter change) + setAllFindings(searchResult.Findings); + } + + setNextToken(searchResult.NextToken); + setHasMoreData(!!searchResult.NextToken); + + // Clear any previous search errors on successful response + setErrorMessage(null); + + if (operationType === 'refresh' || operationType === 'filter') { + setOperationType('initial'); + } + } + }, [searchResult, operationType]); + + // Handle search errors + useEffect(() => { + if (searchError) { + console.error('Failed to search findings:', searchError); + const errorMsg = getErrorMessage(searchError) || 'Please try again.'; + setErrorMessage(`Failed to load findings: ${errorMsg}`); + + setIsLoadingMore(false); + + if (operationType !== 'loadMore') { + setAllFindings([]); + setNextToken(undefined); + setHasMoreData(false); + setSelectedItems([]); + } + + if (operationType === 'refresh' || operationType === 'filter') { + setOperationType('initial'); + } + } + }, [searchError, operationType]); + + const findings = useMemo(() => { + if (!Array.isArray(allFindings)) { + return []; + } + + + if (showSuppressed) { + return allFindings; + } else { + return allFindings.filter(finding => !finding.suppressed); + } + }, [allFindings, showSuppressed]); + + const filteringProperties = [ + { + key: 'findingType', + operators: ['=', '!=', ':', '!:'], + propertyLabel: 'Finding Type', + groupValuesLabel: 'Finding Type values' + }, + { + key: 'accountId', + operators: ['=', '!=', ':', '!:'], + propertyLabel: 'Account', + groupValuesLabel: 'Account values' + }, + { + key: 'remediationStatus', + operators: ['=', '!='], + propertyLabel: 'Remediation Status', + groupValuesLabel: 'Remediation Status values' + }, + { + key: 'findingId', + operators: ['='], + propertyLabel: 'Finding ID', + groupValuesLabel: 'Finding ID values' + }, + { + key: 'resourceType', + operators: ['=', '!=', ':', '!:'], + propertyLabel: 'Resource Type', + groupValuesLabel: 'Resource Type values' + }, + { + key: 'resourceId', + operators: ['=', '!=', ':', '!:'], + propertyLabel: 'Resource ID', + groupValuesLabel: 'Resource ID values' + }, + { + key: 'severity', + operators: ['=', '!='], + propertyLabel: 'Severity', + groupValuesLabel: 'Severity values' + } + ]; + + const filteringOptions = useMemo(() => { + const options: { propertyKey: string; value: string }[] = []; + const uniqueValues = new Set(); + + const remediationStatusOptions = [ + 'Success', + 'Failed', + 'Not Started', + 'In Progress' + ]; + + remediationStatusOptions.forEach(status => { + options.push({ propertyKey: 'remediationStatus', value: status }); + }); + + const severityOptions = [ + 'INFORMATIONAL', + 'LOW', + 'MEDIUM', + 'HIGH', + 'CRITICAL' + ]; + + severityOptions.forEach(severity => { + options.push({ propertyKey: 'severity', value: severity }); + }); + + findings.forEach(finding => { + filteringProperties.forEach(prop => { + // Skip remediationStatus and severity as we're using fixed values + if (prop.key === 'remediationStatus' || prop.key === 'severity') return; + + const value = finding[prop.key as keyof FindingApiResponse]; + if (value && !uniqueValues.has(`${prop.key}:${value}`)) { + uniqueValues.add(`${prop.key}:${value}`); + options.push({ propertyKey: prop.key, value: String(value) }); + } + }); + }); + + return options; + }, [findings]); + + const collectionPreferencesProps = { + title: 'Preferences', + confirmLabel: 'Confirm', + cancelLabel: 'Cancel', + preferences: { + ...preferences, + contentDisplay: [ + { id: 'findingType', label: 'Finding Type', visible: preferences?.visibleContent?.includes('findingType') ?? true }, + { id: 'findingDescription', label: 'Finding Title', visible: preferences?.visibleContent?.includes('findingDescription') ?? true }, + { id: 'remediationStatus', label: 'Remediation Status', visible: preferences?.visibleContent?.includes('remediationStatus') ?? true }, + { id: 'accountId', label: 'Account', visible: preferences?.visibleContent?.includes('accountId') ?? true }, + { id: 'findingId', label: 'Finding ID', visible: preferences?.visibleContent?.includes('findingId') ?? true }, + { id: 'resourceType', label: 'Resource Type', visible: preferences?.visibleContent?.includes('resourceType') ?? true }, + { id: 'resourceId', label: 'Resource ID', visible: preferences?.visibleContent?.includes('resourceId') ?? true }, + { id: 'severity', label: 'Severity', visible: preferences?.visibleContent?.includes('severity') ?? true }, + { id: 'securityHubUpdatedAtTime', label: 'Security Hub Updated Time', visible: preferences?.visibleContent?.includes('securityHubUpdatedAtTime') ?? true }, + { id: 'consoleLink', label: 'Finding Link', visible: preferences?.visibleContent?.includes('consoleLink') ?? true }, + ...(showSuppressed ? [{ id: 'suppressed', label: 'Suppressed', visible: preferences?.visibleContent?.includes('suppressed') ?? true }] : []), + ], + }, + onConfirm: ({ detail }: any) => { + // Convert contentDisplay array to visibleContent array + const visibleContent = detail.contentDisplay + .filter((item: any) => item.visible) + .map((item: any) => item.id); + + setPreferences({ + ...preferences, + visibleContent, + }); + }, + contentDisplayPreference: { + title: 'Column preferences', + description: 'Choose which columns to display in the table', + options: [ + { id: 'findingType', label: 'Finding Type' }, + { id: 'findingDescription', label: 'Finding Title' }, + { id: 'remediationStatus', label: 'Remediation Status' }, + { id: 'accountId', label: 'Account' }, + { id: 'findingId', label: 'Finding ID' }, + { id: 'resourceType', label: 'Resource Type' }, + { id: 'resourceId', label: 'Resource ID' }, + { id: 'severity', label: 'Severity' }, + { id: 'securityHubUpdatedAtTime', label: 'Security Hub Updated Time' }, + { id: 'consoleLink', label: 'Finding Link' }, + ...(showSuppressed ? [{ id: 'suppressed', label: 'Suppressed' }] : []), + ], + }, + }; + + const allColumnDefinitions = useMemo(() => { + const columns = createColumnDefinitions(navigate); + // Add IDs to columns for preferences + return columns.map((col, index) => ({ + ...col, + id: [ + 'findingType', + 'findingDescription', + 'remediationStatus', + 'accountId', + 'findingId', + 'resourceType', + 'resourceId', + 'severity', + 'securityHubUpdatedAtTime', + 'consoleLink', + 'suppressed' + ][index] + })); + }, [navigate]); + + const columnDefinitions = useMemo(() => { + if (!preferences?.visibleContent) { + // Default: show all columns except suppressed + return allColumnDefinitions.filter(col => col.id !== 'suppressed'); + } + + return allColumnDefinitions.filter(col => preferences.visibleContent!.includes(col.id)); + }, [allColumnDefinitions, preferences?.visibleContent]); + + + const handleFilterChange = ({ detail }: any) => { + setFilterTokens(detail.tokens || []); + setFilterOperation(detail.operation || 'and'); + }; + + const handleSortingChange = (detail: any) => { + setSortingColumn(detail.sortingColumn); + setSortingDescending(detail.isDescending); + }; + + const loadMoreFindings = useCallback(async () => { + if (!hasMoreData || isLoadingMore || isLoading) return; + + setOperationType('loadMore'); + setIsLoadingMore(true); + + const searchRequest = buildSearchRequest(true); + searchFindings(searchRequest); + }, [hasMoreData, isLoadingMore, isLoading, searchFindings, buildSearchRequest]); + + // Intersection Observer for infinite scroll + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting && hasMoreData && !isLoadingMore && !isLoading) { + loadMoreFindings(); + } + }, + { + root: null, + rootMargin: '50px', // Trigger 50px before reaching the element + threshold: 0.1, + } + ); + + const currentTrigger = loadMoreTriggerRef.current; + if (currentTrigger) { + observer.observe(currentTrigger); + } + + return () => { + if (currentTrigger) { + observer.unobserve(currentTrigger); + } + }; + }, [hasMoreData, isLoadingMore, isLoading, loadMoreFindings]); + + // Alternative scroll-based detection for table container + useEffect(() => { + const handleScroll = () => { + const container = tableContainerRef.current; + if (!container || !hasMoreData || isLoadingMore || isLoading) return; + + const { scrollTop, scrollHeight, clientHeight } = container; + const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; + + // Trigger load more when 95% scrolled + if (scrollPercentage >= 0.95) { + loadMoreFindings(); + } + }; + + const container = tableContainerRef.current; + if (container) { + container.addEventListener('scroll', handleScroll, { passive: true }); + return () => container.removeEventListener('scroll', handleScroll); + } + }, [hasMoreData, isLoadingMore, isLoading, loadMoreFindings]); + + const handleRemediate = (items: readonly FindingApiResponse[]) => { + setPendingAction({ type: 'remediate', items }); + setShowConfirmModal(true); + }; + + const handleRemediateAndGenerateTicket = (items: readonly FindingApiResponse[]) => { + setPendingAction({ type: 'remediateAndTicket', items }); + setShowConfirmModal(true); + }; + + const handleSuppress = (items: readonly FindingApiResponse[]) => { + setPendingAction({ type: 'suppress', items }); + setShowConfirmModal(true); + }; + + const handleUnsuppress = (items: readonly FindingApiResponse[]) => { + setPendingAction({ type: 'unsuppress', items }); + setShowConfirmModal(true); + }; + + + // Generic suppress/unsuppress handler + const handleSuppressionAction = async ( + actionType: 'Suppress' | 'Unsuppress', + findingIds: string[] + ) => { + const suppressValue = actionType === 'Suppress'; + + const result = await executeAction({ + actionType, + findingIds, + }); + + if (!result.error) { + setAllFindings(prevFindings => + prevFindings.map(finding => + findingIds.includes(finding.findingId) + ? { ...finding, suppressed: suppressValue } + : finding + ) + ); + console.log(`Successfully ${actionType}ed ${pendingAction?.items.length} finding(s)`); + setErrorMessage(null); + setSuccessMessage(`Successfully ${actionType.toLowerCase()}ed ${pendingAction?.items.length} finding${pendingAction?.items.length === 1 ? '' : 's'}`); + } else { + console.error(`Failed to ${actionType} findings:`, result.error); + const errorMsg = getErrorMessage(result.error) || 'Please try again.'; + setErrorMessage(`Failed to ${actionType} findings: ${errorMsg}`); + } + }; + + const handleSuppressAction = async (findingIds: string[]) => { + await handleSuppressionAction('Suppress', findingIds); + }; + + const handleUnsuppressAction = async (findingIds: string[]) => { + await handleSuppressionAction('Unsuppress', findingIds); + }; + + const handleRemediationAction = async ( + actionType: 'Remediate' | 'RemediateAndGenerateTicket', + findingIds: string[] + ) => { + const result = await executeAction({ + actionType, + findingIds, + }); + + if (!result.error) { + setAllFindings(prevFindings => + prevFindings.map(finding => + findingIds.includes(finding.findingId) + ? { + ...finding, + remediationStatus: 'IN_PROGRESS' as const, + lastUpdatedTime: new Date().toISOString() + } + : finding + ) + ); + console.log(`Successfully initiated ${actionType} for ${pendingAction?.items.length} finding(s)`); + setErrorMessage(null); + setSuccessMessage(`Successfully sent ${pendingAction?.items.length} finding${pendingAction?.items.length === 1 ? '' : 's'} for Remediation`); + } else { + console.error(`Failed to execute ${actionType}:`, result.error); + const errorMsg = getErrorMessage(result.error) || 'Please try again.'; + setErrorMessage(`Failed to ${actionType}: ${errorMsg}`); + } + }; + + // Handle remediate action + const handleRemediateAction = async (findingIds: string[]) => { + await handleRemediationAction('Remediate', findingIds); + }; + + // Handle remediate and ticket action + const handleRemediateAndTicketAction = async (findingIds: string[]) => { + await handleRemediationAction('RemediateAndGenerateTicket', findingIds); + }; + + // Execute the confirmed action + const executeConfirmedAction = async () => { + if (!pendingAction || pendingAction.items.length === 0) return; + + try { + const findingIds = pendingAction.items.map(item => item.findingId); + + switch (pendingAction.type) { + case 'suppress': + await handleSuppressAction(findingIds); + break; + case 'unsuppress': + await handleUnsuppressAction(findingIds); + break; + case 'remediate': + await handleRemediateAction(findingIds); + break; + case 'remediateAndTicket': + await handleRemediateAndTicketAction(findingIds); + break; + } + + // Clear selection after action + setSelectedItems([]); + + } catch (error) { + console.error(`Failed to execute ${pendingAction.type} action:`, error); + } finally { + // Close modal and clear pending action + setShowConfirmModal(false); + setPendingAction(null); + } + }; + + // Cancel confirmation modal + const cancelConfirmation = () => { + setShowConfirmModal(false); + setPendingAction(null); + }; + + // Get modal content based on action type + const getModalContent = () => { + if (!pendingAction) return { title: '', message: '', actionButton: '' }; + + const count = pendingAction.items.length; + const itemText = count === 1 ? 'finding' : 'findings'; + + switch (pendingAction.type) { + case 'suppress': + return { + title: 'Confirm Suppress Action', + message: `Are you sure you want to suppress ${count} ${itemText}? Suppressed findings will be hidden from the default view but can be shown using the toggle.`, + actionButton: 'Suppress', + }; + case 'unsuppress': + return { + title: 'Confirm Unsuppress Action', + message: `Are you sure you want to unsuppress ${count} ${itemText}? Unsuppressed findings will be visible in the default view and available for remediation.`, + actionButton: 'Unsuppress', + }; + case 'remediate': + return { + title: 'Confirm Remediation', + message: `Are you sure you want to remediate ${count} ${itemText}? This will automatically make changes to your AWS resources to fix the security issues. Some changes may be irreversible.`, + actionButton: 'Remediate', + }; + case 'remediateAndTicket': + return { + title: 'Confirm Remediation with Ticket', + message: `Are you sure you want to remediate ${count} ${itemText} and generate tickets? This will automatically make changes to your AWS resources and create tracking tickets. Some changes may be irreversible.`, + actionButton: 'Remediate & Create Ticket', + }; + default: + return { title: '', message: '', actionButton: '' }; + } + }; + + const handleRefresh = () => { + setOperationType('refresh'); + setAllFindings([]); + setNextToken(undefined); + setHasMoreData(false); + setSelectedItems([]); + setErrorMessage(null); + setSuccessMessage(null); + setIsLoadingMore(false); + + const searchRequest = buildSearchRequest(false); + searchFindings(searchRequest); + }; + + return ( +
+ {successMessage && ( + + setSuccessMessage(null)} + action={ + successMessage.includes('Remediation') ? ( + + ) : undefined + } + > + {successMessage} + + + )} + + {errorMessage && ( + + setErrorMessage(null)} + header="Operation Failed" + > + {errorMessage} + + + )} + + {/* Header Section */} +
+
+ + {/* Single Integrated Search and Filter */} +
+
+ `Remove token ${token.propertyKey} ${token.operator} ${token.value}`, + enteredTextLabel: (text) => `Use: "${text}"` + }} + expandToViewport + /> +
+ +
+ + + { + setShowSuppressed(detail.checked); + + if (detail.checked) { + // Add suppressed column + const currentVisibleContent = preferences?.visibleContent || [ + 'findingType', 'findingDescription', 'remediationStatus', 'accountId', + 'findingId', 'resourceType', 'resourceId', 'severity', + 'securityHubUpdatedAtTime', 'consoleLink' + ]; + setPreferences({ + ...preferences, + visibleContent: [...currentVisibleContent, 'suppressed'] + }); + } else { + // Remove suppressed column + setPreferences({ + ...preferences, + visibleContent: (preferences?.visibleContent || []).filter(col => col !== 'suppressed') + }); + } + }} + checked={showSuppressed} + > + Show suppressed findings + + + + {/* Table Section with Infinite Scroll */} +
+ + items={findings} + loading={isLoading} + loadingText="Loading findings" + columnDefinitions={columnDefinitions} + selectedItems={selectedItems} + onSelectionChange={({ detail }) => setSelectedItems(detail.selectedItems)} + sortingColumn={sortingColumn} + sortingDescending={sortingDescending} + onSortingChange={({ detail }) => handleSortingChange(detail)} + stickyHeader + stripedRows={preferences?.stripedRows ?? false} + contentDensity={preferences?.contentDensity ?? 'comfortable'} + wrapLines={preferences?.wrapLines ?? true} + variant="full-page" + selectionType="multi" + isItemDisabled={(item) => + item.remediationStatus === 'IN_PROGRESS' || item.remediationStatus === 'SUCCESS' + } + ariaLabels={{ + selectionGroupLabel: 'Items selection', + tableLabel: 'Findings table', + allItemsSelectionLabel: ({ selectedItems }) => + `${selectedItems.length} ${selectedItems.length === 1 ? 'item' : 'items'} selected`, + itemSelectionLabel: ({ selectedItems }, item) => { + const isItemSelected = selectedItems.filter(i => i.findingId === item.findingId).length; + return `${item.findingDescription} is ${isItemSelected ? '' : 'not '}selected`; + } + }} + empty={} + /> + + {/* Invisible trigger element for intersection observer */} + {hasMoreData && ( +
+ )} + + {/* Loading More Indicator */} + {isLoadingMore && ( + +
+ + + Loading more findings... + +
+
+ )} + + + {/* End of Results Indicator */} + {!hasMoreData && findings.length > 0 && ( + + No more findings to load + + )} +
+ + + {showConfirmModal && pendingAction && ( + + + + + + + } + > + + + {getModalContent().message} + + {pendingAction.items.length > 0 && ( + + Selected finding IDs: +
    + {pendingAction.items.slice(0, 5).map((item) => ( +
  • + {item.findingId} +
  • + ))} + {pendingAction.items.length > 5 && ( +
  • + ... and {pendingAction.items.length - 5} more finding(s) +
  • + )} +
+
+ )} +
+
+ )} +
+ ); +} diff --git a/source/webui/src/pages/findings/findings-table/createColumnDefinitions.tsx b/source/webui/src/pages/findings/findings-table/createColumnDefinitions.tsx new file mode 100644 index 00000000..017a718a --- /dev/null +++ b/source/webui/src/pages/findings/findings-table/createColumnDefinitions.tsx @@ -0,0 +1,173 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { TableProps } from '@cloudscape-design/components/table'; + +import { Badge, Link, StatusIndicator } from '@cloudscape-design/components'; +import { NavigateFunction } from 'react-router-dom'; +import { FindingApiResponse } from '@data-models'; + +const getStatusIndicatorType = (status: string) => { + switch (status.toLowerCase()) { + case 'success': + return 'success'; + case 'failed': + return 'error'; + case 'in_progress': + return 'in-progress'; + case 'not_started': + default: + return 'pending'; + } +}; + +const getSeverityColor = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': + return 'severity-critical'; + case 'high': + return 'severity-high'; + case 'medium': + return 'severity-medium'; + case 'low': + return 'severity-low'; + case 'informational': + default: + return 'severity-neutral'; + } +}; + +const formatStatus = (status: string) => { + if (!status) return 'Unknown'; + + // Convert underscores to spaces and capitalize each word + return status + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/g, l => l.toUpperCase()); +}; + +const formatDateTime = (dateTimeString: string) => { + if (!dateTimeString) return '-'; + + try { + const date = new Date(dateTimeString); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short' + }); + } catch (error) { + console.error(`Error formatting date string "${dateTimeString}":`, error); + return dateTimeString; + } +}; + +export const createColumnDefinitions = (navigate: NavigateFunction): TableProps['columnDefinitions'] => [ + { + header: 'Finding Type', + cell: ({ findingType }) => findingType || '-', + minWidth: '150px', + }, + { + header: 'Finding Title', + cell: ({ findingDescription }) => findingDescription || '-', + minWidth: '300px', + }, + { + header: 'Remediation Status', + cell: ({ remediationStatus, findingId }) => { + const hasHistory = ['in_progress', 'failed', 'success'].includes(remediationStatus?.toLowerCase() || ''); + + if (hasHistory) { + return ( + + navigate('/history', { + state: { + filterTokens: [{ + propertyKey: 'findingId', + operator: '=', + value: findingId + }] + } + })} + style={{ + cursor: 'pointer', + textDecoration: 'none' + }} + onMouseEnter={(e) => (e.target as HTMLElement).style.textDecoration = 'underline'} + onMouseLeave={(e) => (e.target as HTMLElement).style.textDecoration = 'none'} + > + {formatStatus(remediationStatus)} + + + ); + } + + return ( + + {formatStatus(remediationStatus)} + + ); + }, + minWidth: '150px', + }, + { + header: 'Account', + cell: ({ accountId }) => accountId, + minWidth: '120px', + }, + { + header: 'Finding ID', + cell: ({ findingId }) => findingId || '-', + minWidth: '200px', + }, + { + header: 'Resource Type', + cell: ({ resourceType }) => resourceType || '-', + minWidth: '150px', + }, + { + header: 'Resource ID', + cell: ({ resourceId }) => resourceId || '-', + minWidth: '150px', + }, + { + header: 'Severity', + cell: ({ severity }) => ( + + {severity} + + ), + sortingField: 'severityNormalized', + minWidth: '100px', + }, + { + header: 'Security Hub Updated Time', + cell: ({ securityHubUpdatedAtTime }) => formatDateTime(securityHubUpdatedAtTime), + sortingField: 'securityHubUpdatedAtTime', + minWidth: '200px', + }, + { + header: 'Finding Link', + cell: ({ consoleLink }) => ( + + Security Hub + + ), + minWidth: '120px', + }, + { + header: 'Suppressed', + cell: ({ suppressed }) => ( + + {suppressed ? 'Yes' : 'No'} + + ), + minWidth: '100px', + }, +]; diff --git a/source/webui/src/pages/history/RemediationHistoryOverviewPage.tsx b/source/webui/src/pages/history/RemediationHistoryOverviewPage.tsx new file mode 100644 index 00000000..a7546bc3 --- /dev/null +++ b/source/webui/src/pages/history/RemediationHistoryOverviewPage.tsx @@ -0,0 +1,7 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import RemediationHistoryTable from './history-table/RemediationHistoryTable.tsx'; + +export const RemediationHistoryOverviewPage = () => { + return ; +}; diff --git a/source/webui/src/pages/history/history-table/RemediationHistoryTable.tsx b/source/webui/src/pages/history/history-table/RemediationHistoryTable.tsx new file mode 100644 index 00000000..ffc9038b --- /dev/null +++ b/source/webui/src/pages/history/history-table/RemediationHistoryTable.tsx @@ -0,0 +1,652 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import CollectionPreferences, { + CollectionPreferencesProps, +} from '@cloudscape-design/components/collection-preferences'; +import Header from '@cloudscape-design/components/header'; +import PropertyFilter, { PropertyFilterProps } from '@cloudscape-design/components/property-filter'; +import Table from '@cloudscape-design/components/table'; + +import Alert from '@cloudscape-design/components/alert'; +import Box from '@cloudscape-design/components/box'; +import Button from '@cloudscape-design/components/button'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import Spinner from '@cloudscape-design/components/spinner'; +import { useDispatch } from 'react-redux'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { EmptyTableState } from '../../../components/EmptyTableState.tsx'; +import { RemediationHistoryApiResponse } from '@data-models'; +import { useExportRemediationsMutation, useLazySearchRemediationsQuery } from '../../../store/remediationsSlice.ts'; +import { CompositeFilter, SearchRequest, StringFilter } from '../../../store/types.ts'; +import { getErrorMessage } from '../../../utils/error.ts'; +import { createHistoryColumnDefinitions } from './createHistoryColumnDefinitions.tsx'; + +const getFilterCounterText = (count = 0) => `${count} ${count === 1 ? 'match' : 'matches'}`; + +export default function RemediationHistoryTable() { + const navigate = useNavigate(); + const location = useLocation(); + useDispatch(); + + // State management + const [preferences, setPreferences] = useState({ + wrapLines: true, + stripedRows: false, + contentDensity: 'comfortable', + }); + const [sortingColumn, setSortingColumn] = useState(() => { + const columns = createHistoryColumnDefinitions(navigate); + return columns.find(col => col.sortingField === 'lastUpdatedTime')!; + }); + const [sortingDescending, setSortingDescending] = useState(true); + const [filterTokens, setFilterTokens] = useState([]); + const [filterOperation, setFilterOperation] = useState<'and' | 'or'>('and'); + + const [allHistory, setAllHistory] = useState([]); + const [nextToken, setNextToken] = useState(); + const [hasMoreData, setHasMoreData] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [operationType, setOperationType] = useState<'initial' | 'refresh' | 'filter' | 'loadMore'>('initial'); + + // Refs for scroll detection + const tableContainerRef = useRef(null); + const loadMoreTriggerRef = useRef(null); + + const [searchRemediations, { data: searchResult, isLoading: isSearchLoading, error: searchError }] = useLazySearchRemediationsQuery(); + const [exportRemediations, { isLoading: isExportLoading, error: exportError }] = useExportRemediationsMutation(); + + // Handle initial filter state from navigation + useEffect(() => { + const state = location.state as { filterTokens?: PropertyFilterProps.Token[] }; + if (state?.filterTokens) { + setFilterTokens(state.filterTokens); + } + }, [location.state]); + + const getComparisonOperator = (operator: string): 'EQUALS' | 'NOT_EQUALS' | 'CONTAINS' | 'NOT_CONTAINS' | 'GREATER_THAN_OR_EQUAL' | 'LESS_THAN_OR_EQUAL' => { + switch (operator) { + case '=': + return 'EQUALS'; + case '!=': + return 'NOT_EQUALS'; + case ':': + return 'CONTAINS'; + case '!:': + return 'NOT_CONTAINS'; + case '>=': + return 'GREATER_THAN_OR_EQUAL'; + case '<=': + return 'LESS_THAN_OR_EQUAL'; + default: + return 'EQUALS'; + } + }; + + const unformatStatus = (formattedStatus: string) => { + // Convert formatted status back to uppercase with underscores for API + const statusMap: { [key: string]: string } = { + 'Success': 'SUCCESS', + 'Failed': 'FAILED', + 'Not Started': 'NOT_STARTED', + 'In Progress': 'IN_PROGRESS' + }; + + return statusMap[formattedStatus] || formattedStatus.toUpperCase().replace(/\s+/g, '_'); + }; + + const convertTokensToFilters = (tokens: PropertyFilterProps.Token[]): SearchRequest['Filters'] => { + if (!tokens?.length) return undefined; + + const fieldGroups: { [fieldName: string]: StringFilter[] } = {}; + + tokens.forEach(token => { + const comparison = getComparisonOperator(token.operator || '='); + + // Convert formatted remediation status values back to raw values for API + let filterValue = token.value || ''; + if (token.propertyKey === 'remediationStatus') { + filterValue = unformatStatus(filterValue); + } + + const filter: StringFilter = { + FieldName: token.propertyKey || '', + Filter: { + Value: filterValue, + Comparison: comparison, + }, + }; + + const fieldName = token.propertyKey || ''; + if (!fieldGroups[fieldName]) { + fieldGroups[fieldName] = []; + } + fieldGroups[fieldName].push(filter); + }); + + const compositeFilters: CompositeFilter[] = Object.entries(fieldGroups).map(([, filters]) => ({ + Operator: 'OR' as const, + StringFilters: filters, + })); + + return { + CompositeFilters: compositeFilters.length > 0 ? compositeFilters : undefined, + CompositeOperator: 'AND', + }; + }; + + const buildSearchRequest = (useNextToken: boolean = false): SearchRequest => { + const filters = convertTokensToFilters(filterTokens); + + const request: SearchRequest = { + Filters: filters, + SortCriteria: [{ + Field: sortingColumn.sortingField || 'lastUpdatedTime', + SortOrder: sortingDescending ? 'desc' : 'asc', + }], + }; + + if (useNextToken && nextToken) { + request.NextToken = nextToken; + } + + return request; + }; + + // Initial load on component mount + useEffect(() => { + setOperationType('initial'); + const searchRequest = buildSearchRequest(false); + searchRemediations(searchRequest); + }, []); + + // Reload when filters or sorting change + useEffect(() => { + setOperationType('filter'); + setAllHistory([]); + setNextToken(undefined); + setHasMoreData(false); + + const searchRequest = buildSearchRequest(false); + searchRemediations(searchRequest); + }, [filterTokens, filterOperation, sortingColumn, sortingDescending]); + + // Update state when search results change + useEffect(() => { + if (searchResult) { + if (operationType === 'loadMore') { + setAllHistory(prev => { + const existingIds = new Set(prev.map(f => f.executionId)); + const newRemediations = searchResult.Remediations.filter(f => !existingIds.has(f.executionId)); + return [...prev, ...newRemediations]; + }); + setIsLoadingMore(false); + } else { + // Replace history (initial, refresh, or filter change) + setAllHistory(searchResult.Remediations); + } + + setNextToken(searchResult.NextToken); + setHasMoreData(!!searchResult.NextToken); + + // Clear any previous search errors on successful response + setErrorMessage(null); + + // Reset operation type after successful operation (but not for loadMore) + if (operationType === 'refresh' || operationType === 'filter') { + setOperationType('initial'); + } + } + }, [searchResult, operationType]); + + // Handle search errors + useEffect(() => { + if (searchError) { + console.error('Failed to search remediations:', searchError); + const errorMsg = getErrorMessage(searchError) || 'Please try again.'; + setErrorMessage(`Failed to load remediation history: ${errorMsg}`); + + setIsLoadingMore(false); + + // clear history when search fails + if (operationType !== 'loadMore') { + setAllHistory([]); + setNextToken(undefined); + setHasMoreData(false); + } + + // Reset operation type on error to prevent stuck states (but not for loadMore) + if (operationType === 'refresh' || operationType === 'filter') { + setOperationType('initial'); + } + } + }, [searchError, operationType]); + + // Handle export errors + useEffect(() => { + if (exportError) { + console.error('Failed to export remediations:', exportError); + const errorMsg = getErrorMessage(exportError) || 'Please try again.'; + setErrorMessage(`Failed to export remediation history: ${errorMsg}`); + } + }, [exportError]); + + const history = useMemo(() => { + if (!Array.isArray(allHistory)) { + return []; + } + + return allHistory; + }, [allHistory]); + + const filteringProperties = [ + { + key: 'findingId', + operators: ['='], + propertyLabel: 'Finding ID', + groupValuesLabel: 'Finding ID values' + }, + { + key: 'remediationStatus', + operators: ['=', '!='], + propertyLabel: 'Status', + groupValuesLabel: 'Status values' + }, + { + key: 'accountId', + operators: ['=', '!=', ':', '!:'], + propertyLabel: 'Account', + groupValuesLabel: 'Account values' + }, + { + key: 'resourceId', + operators: ['=', '!=', ':', '!:'], + propertyLabel: 'Resource ID', + groupValuesLabel: 'Resource ID values' + }, + { + key: 'lastUpdatedBy', + operators: ['=', '!=', ':', '!:'], + propertyLabel: 'Executed By', + groupValuesLabel: 'Executed By values' + }, + { + key: 'lastUpdatedTime', + operators: ['>=', '<='], + propertyLabel: 'Execution Timestamp', + groupValuesLabel: 'DateTime values (e.g., 2024-01-15T14:30)' + } + ]; + + const filteringOptions = useMemo(() => { + const options: { propertyKey: string; value: string }[] = []; + const uniqueValues = new Set(); + + // Add fixed formatted status values + const statusOptions = [ + 'Success', + 'Failed', + 'Not Started', + 'In Progress' + ]; + + statusOptions.forEach(status => { + options.push({ propertyKey: 'remediationStatus', value: status }); + uniqueValues.add(`remediationStatus:${status}`); + }); + + // Add timestamp format examples for better UX + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + const lastWeek = new Date(today); + lastWeek.setDate(lastWeek.getDate() - 7); + const lastMonth = new Date(today); + lastMonth.setMonth(lastMonth.getMonth() - 1); + + const timestampExamples = [ + today.toISOString().substring(0, 16), + yesterday.toISOString().substring(0, 16), + today.toISOString().split('T')[0], + yesterday.toISOString().split('T')[0], + ]; + + timestampExamples.forEach(value => { + if (!uniqueValues.has(`lastUpdatedTime:${value}`)) { + options.push({ propertyKey: 'lastUpdatedTime', value }); + uniqueValues.add(`lastUpdatedTime:${value}`); + } + }); + + // Add dynamic values for other fields (excluding remediationStatus and lastUpdatedTime) + if (Array.isArray(allHistory)) { + allHistory.forEach(item => { + filteringProperties.forEach(prop => { + if (prop.key === 'remediationStatus' || prop.key === 'lastUpdatedTime') return; // Skip these + + const value = item[prop.key as keyof RemediationHistoryApiResponse]; + if (value && !uniqueValues.has(`${prop.key}:${value}`)) { + uniqueValues.add(`${prop.key}:${value}`); + options.push({ propertyKey: prop.key, value: String(value) }); + } + }); + }); + } + + return options; + }, [allHistory]); + + const collectionPreferencesProps = { + title: 'Preferences', + confirmLabel: 'Confirm', + cancelLabel: 'Cancel', + preferences: { + ...preferences, + contentDisplay: [ + { id: 'findingId', label: 'Finding ID', visible: preferences?.visibleContent?.includes('findingId') ?? true }, + { id: 'status', label: 'Status', visible: preferences?.visibleContent?.includes('status') ?? true }, + { id: 'accountId', label: 'Account', visible: preferences?.visibleContent?.includes('accountId') ?? true }, + { id: 'resourceId', label: 'Resource ID', visible: preferences?.visibleContent?.includes('resourceId') ?? true }, + { id: 'executionTimestamp', label: 'Execution Timestamp', visible: preferences?.visibleContent?.includes('executionTimestamp') ?? true }, + { id: 'executedBy', label: 'Executed By', visible: preferences?.visibleContent?.includes('executedBy') ?? true }, + { id: 'viewExecution', label: 'View Execution', visible: preferences?.visibleContent?.includes('viewExecution') ?? true }, + ], + }, + onConfirm: ({ detail }: any) => { + const visibleContent = detail.contentDisplay + .filter((item: any) => item.visible) + .map((item: any) => item.id); + + setPreferences({ + ...preferences, + visibleContent, + }); + }, + contentDisplayPreference: { + title: 'Column preferences', + description: 'Choose which columns to display in the table', + options: [ + { id: 'findingId', label: 'Finding ID' }, + { id: 'status', label: 'Status' }, + { id: 'accountId', label: 'Account' }, + { id: 'resourceId', label: 'Resource ID' }, + { id: 'executionTimestamp', label: 'Execution Timestamp' }, + { id: 'executedBy', label: 'Executed By' }, + { id: 'viewExecution', label: 'View Execution' }, + ], + }, + }; + + const allColumnDefinitions = useMemo(() => { + const columns = createHistoryColumnDefinitions(navigate); + // Add IDs to columns for preferences + return columns.map((col, index) => ({ + ...col, + id: [ + 'findingId', + 'status', + 'accountId', + 'resourceId', + 'executionTimestamp', + 'executedBy', + 'viewExecution' + ][index] + })); + }, [navigate]); + + const columnDefinitions = useMemo(() => { + if (!preferences?.visibleContent) { + // Default: show all columns + return allColumnDefinitions; + } + + return allColumnDefinitions.filter(col => preferences.visibleContent!.includes(col.id)); + }, [allColumnDefinitions, preferences?.visibleContent]); + + const handleFilterChange = ({ detail }: any) => { + setFilterTokens(detail.tokens || []); + setFilterOperation(detail.operation || 'and'); + }; + + const handleSortingChange = (detail: any) => { + setSortingColumn(detail.sortingColumn); + setSortingDescending(detail.isDescending); + }; + + const loadMoreRemediations = useCallback(async () => { + if (!hasMoreData || isLoadingMore || isSearchLoading) return; + + setOperationType('loadMore'); + setIsLoadingMore(true); + + const searchRequest = buildSearchRequest(true); + searchRemediations(searchRequest); + }, [hasMoreData, isLoadingMore, isSearchLoading, searchRemediations, buildSearchRequest]); + + // Intersection Observer for infinite scroll + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting && hasMoreData && !isLoadingMore && !isSearchLoading) { + loadMoreRemediations(); + } + }, + { + root: null, + rootMargin: '50px', // Trigger 50px before reaching the element + threshold: 0.1, + } + ); + + const currentTrigger = loadMoreTriggerRef.current; + if (currentTrigger) { + observer.observe(currentTrigger); + } + + return () => { + if (currentTrigger) { + observer.unobserve(currentTrigger); + } + }; + }, [hasMoreData, isLoadingMore, isSearchLoading, loadMoreRemediations]); + + // Alternative scroll-based detection for table container + useEffect(() => { + const handleScroll = () => { + const container = tableContainerRef.current; + if (!container || !hasMoreData || isLoadingMore || isSearchLoading) return; + + const { scrollTop, scrollHeight, clientHeight } = container; + const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; + + // Trigger load more when 95% scrolled + if (scrollPercentage >= 0.95) { + loadMoreRemediations(); + } + }; + + const container = tableContainerRef.current; + if (container) { + container.addEventListener('scroll', handleScroll, { passive: true }); + return () => container.removeEventListener('scroll', handleScroll); + } + }, [hasMoreData, isLoadingMore, isSearchLoading, loadMoreRemediations]); + + const handleRefresh = () => { + setOperationType('refresh'); + setAllHistory([]); + setNextToken(undefined); + setHasMoreData(false); + setErrorMessage(null); + setIsLoadingMore(false); + + const searchRequest = buildSearchRequest(false); + searchRemediations(searchRequest); + }; + + const handleExport = async () => { + try { + const exportRequest = buildSearchRequest(false); + + const result = await exportRemediations(exportRequest).unwrap(); + if (result.downloadUrl) { + window.open(result.downloadUrl, '_blank'); + } + } catch (error) { + console.error('Export failed:', error); + const errorMsg = getErrorMessage(error) || 'Please try again.'; + setErrorMessage(`Failed to export remediation history: ${errorMsg}`); + } + }; + + + return ( +
+ + {/* Header Section */} +
+ + + } + description="View remediations executed in the past for all member accounts." + > + Remediation History +
+ + {errorMessage && ( + + setErrorMessage(null)} + header="Operation Failed" + > + {errorMessage} + + + )} + + {/* Search and Filter */} +
+
+ `Remove token ${token.propertyKey} ${token.operator} ${token.value}`, + enteredTextLabel: (text) => `Use: "${text}"` + }} + expandToViewport + /> +
+ +
+ + {/* Table Section with Infinite Scroll */} +
+ + items={history} + loading={isSearchLoading} + loadingText="Loading history" + columnDefinitions={columnDefinitions} + sortingColumn={sortingColumn} + sortingDescending={sortingDescending} + onSortingChange={({ detail }) => handleSortingChange(detail)} + stickyHeader + stripedRows={preferences?.stripedRows ?? false} + contentDensity={preferences?.contentDensity ?? 'comfortable'} + wrapLines={preferences?.wrapLines ?? true} + variant="full-page" + ariaLabels={{ + tableLabel: 'Remediation history table' + }} + empty={} + /> + + {/* Invisible trigger element for intersection observer */} + {hasMoreData && ( +
+ )} + + {/* Loading More Indicator */} + {isLoadingMore && ( + +
+ + + Loading more remediations... + +
+
+ )} + + {/* End of Results Indicator */} + {!hasMoreData && history.length > 0 && ( + + No more remediations to load + + )} +
+
+ ); +} diff --git a/source/webui/src/pages/history/history-table/createHistoryColumnDefinitions.tsx b/source/webui/src/pages/history/history-table/createHistoryColumnDefinitions.tsx new file mode 100644 index 00000000..4bdf47b3 --- /dev/null +++ b/source/webui/src/pages/history/history-table/createHistoryColumnDefinitions.tsx @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { TableProps } from '@cloudscape-design/components/table'; + +import { Link, StatusIndicator, Popover, Box } from '@cloudscape-design/components'; +import { NavigateFunction } from 'react-router-dom'; +import { RemediationHistoryApiResponse } from '@data-models'; + +const getStatusIndicatorType = (status: string) => { + switch (status.toLowerCase()) { + case 'success': + return 'success'; + case 'failed': + return 'error'; + case 'in_progress': + return 'in-progress'; + default: + return 'pending'; + } +}; + +const formatStatus = (status: string) => { + if (!status) return 'Unknown'; + + // Convert underscores to spaces and capitalize each word + return status + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/g, (l) => l.toUpperCase()); +}; + +const formatDateTime = (dateTimeString: string) => { + if (!dateTimeString) return '-'; + + try { + const date = new Date(dateTimeString); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', + }); + } catch (error) { + console.error(`Error formatting date string "${dateTimeString}":`, error); + return dateTimeString; + } +}; + +export const createHistoryColumnDefinitions = ( + navigate: NavigateFunction, +): TableProps['columnDefinitions'] => [ + { + header: 'Finding ID', + cell: ({ findingId }) => findingId || '-', + minWidth: '300px', + }, + { + header: 'Status', + cell: ({ remediationStatus, error }) => { + const statusIndicator = ( + + {formatStatus(remediationStatus)} + + ); + + if (error && remediationStatus === 'FAILED') { + return ( + + {error}}> + + {formatStatus(remediationStatus)} + + + + ); + } + + return statusIndicator; + }, + minWidth: '140px', + }, + { + header: 'Account', + cell: ({ accountId }) => accountId, + minWidth: '140px', + }, + { + header: 'Resource ID', + cell: ({ resourceId }) => resourceId || '-', + minWidth: '150px', + }, + { + header: 'Execution Timestamp', + cell: ({ lastUpdatedTime }) => formatDateTime(lastUpdatedTime), + sortingField: 'lastUpdatedTime', + minWidth: '200px', + }, + { + header: 'Executed By', + cell: ({ lastUpdatedBy }) => lastUpdatedBy || '-', + minWidth: '180px', + }, + { + header: 'View Execution', + cell: ({ consoleLink }) => ( + + Step Functions + + ), + minWidth: '140px', + }, +]; diff --git a/source/webui/src/pages/users/UsersOverviewPage.tsx b/source/webui/src/pages/users/UsersOverviewPage.tsx new file mode 100644 index 00000000..44b368ad --- /dev/null +++ b/source/webui/src/pages/users/UsersOverviewPage.tsx @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import UsersTable from './users-table/UsersTable.tsx'; +import { useGetUsersQuery } from '../../store/usersApiSlice.ts'; +import { useContext, useState } from 'react'; +import { UserContext } from '../../contexts/UserContext.tsx'; +import { Flashbar, FlashbarProps } from '@cloudscape-design/components'; +import { getErrorMessage } from '../../utils/error.ts'; + +export const UsersOverviewPage = () => { + const { groups } = useContext(UserContext); + const queryResult = useGetUsersQuery({ currentUserGroups: groups ?? [] }, { skip: !groups }); + const { data: users, error: usersError, refetch, isFetching } = queryResult; + const [resetPagination, setResetPagination] = useState(false); + + const handleRefresh = async () => { + setResetPagination(true); + await refetch(); + setResetPagination(false); + }; + const notifications: FlashbarProps.MessageDefinition[] = []; + + if (usersError) { + notifications.push({ + type: 'error', + content: `Failed to load users: ${getErrorMessage(usersError) || 'Unknown error'}`, + id: 'users-error', + }); + } + + return ( + <> + {notifications.length > 0 && } + + + ); +}; diff --git a/source/webui/src/pages/users/invite/InviteUsersPage.tsx b/source/webui/src/pages/users/invite/InviteUsersPage.tsx new file mode 100644 index 00000000..3d6f7940 --- /dev/null +++ b/source/webui/src/pages/users/invite/InviteUsersPage.tsx @@ -0,0 +1,177 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useState, useMemo, useContext, useEffect } from 'react'; +import { + Container, + ContentLayout, + Header, + Form, + FormField, + Input, + Select, + SelectProps, + Textarea, + Button, + SpaceBetween, +} from '@cloudscape-design/components'; +import { useDispatch } from 'react-redux'; +import { validateAccountIds, parseAccountIds, validateEmail } from '../../../utils/validation.ts'; +import { useInviteUserMutation } from '../../../store/usersApiSlice.ts'; +import { UserContext } from '../../../contexts/UserContext.tsx'; +import { getErrorMessage } from '../../../utils/error.ts'; +import { getHighestUserGroup } from '../../../utils/userPermissions.ts'; +import { addNotification } from '../../../store/notificationsSlice.ts'; +import { USER_TYPE_DELEGATED_ADMIN, USER_TYPE_ACCOUNT_OPERATOR, InviteUserRequest } from '@data-models'; + +const OPTION_DELEGATED_ADMIN = { label: 'Delegated Admin', value: USER_TYPE_DELEGATED_ADMIN }; +const OPTION_ACCOUNT_OPERATOR = { label: 'Account Operator', value: USER_TYPE_ACCOUNT_OPERATOR }; + +export const InviteUsersPage = () => { + const dispatch = useDispatch(); + const { email: currentUserEmail, groups } = useContext(UserContext); + const highestUserGroup = getHighestUserGroup(groups); + const isDelegatedAdmin = highestUserGroup === 'DelegatedAdminGroup'; + + const initialPermissionType = isDelegatedAdmin ? OPTION_ACCOUNT_OPERATOR : null; + + const [email, setEmail] = useState(''); + const [permissionType, setPermissionType] = useState(initialPermissionType); + const [ownedAccounts, setOwnedAccounts] = useState(''); + const [inviteUser, { isLoading, error, reset }] = useInviteUserMutation(); + + const permissionOptions = isDelegatedAdmin + ? [OPTION_ACCOUNT_OPERATOR] + : [OPTION_DELEGATED_ADMIN, OPTION_ACCOUNT_OPERATOR]; + + const handleSubmit = async () => { + if (!email || !permissionType) { + return; + } + + const inviteRequest: InviteUserRequest = { + email, + role: permissionType.value === 'delegated-admin' ? ('DelegatedAdmin' as const) : ('AccountOperator' as const), + ...(isAccountOperator && ownedAccounts ? { accountIds: parseAccountIds(ownedAccounts) } : {}), + }; + + const result = await inviteUser(inviteRequest); + + if ('data' in result) { + dispatch( + addNotification({ + type: 'success', + content: `User invitation sent successfully to ${email}`, + id: `invite-success-${Date.now()}`, + }), + ); + + setEmail(''); + setPermissionType(initialPermissionType); + setOwnedAccounts(''); + reset(); + } + }; + + const isAccountOperator = permissionType?.value === OPTION_ACCOUNT_OPERATOR.value; + const validationError = useMemo(() => { + if (!isAccountOperator || !ownedAccounts.trim()) { + return null; + } + return validateAccountIds(ownedAccounts); + }, [ownedAccounts, isAccountOperator]); + + useEffect(() => { + if (error) { + dispatch( + addNotification({ + type: 'error', + content: `Failed to invite user: ${getErrorMessage(error)}`, + id: `invite-error-${Date.now()}`, + }), + ); + } + }, [error, dispatch]); + + const emailValidationError = useMemo(() => validateEmail(email), [email]); + + const isFormValid = useMemo(() => { + const hasValidEmail = !!email.trim() && !emailValidationError; + const hasPermissionType = !!permissionType; + const hasValidAccountIds = !isAccountOperator || !validationError; + + return hasValidEmail && hasPermissionType && hasValidAccountIds; + }, [email, emailValidationError, permissionType, isAccountOperator, validationError]); + + return ( + + Invite Users + + } + > + Invitation Details}> +
+ Submit + + } + > + + + setEmail(detail.value)} + placeholder="johndoe@example.com" + invalid={!!emailValidationError} + type="email" + /> + + + +