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 b5acecb8..00000000 Binary files a/docs/architecture_diagram.png and /dev/null differ 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 00000000..cf914e62 Binary files /dev/null and b/docs/automated-security-response-on-aws-architecture-diagram.png differ 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 00000000..3758a2f4 Binary files /dev/null and b/source/webui/public/cognito-login-banner.png differ 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": "iVBORw0KGgoAAAANSUhEUgAAAV4AAACyCAYAAAAK2qdXAAAZtklEQVR4nO2d7XXbPA+Gwfc8CzgjuCMkI7gjJCMkIzgjJCO4IzgjOCMkI6QjOCPg/SGopSHwS6Joub6vc3jaWBJFkRQEgiDomJkAAAC043/nLgAAAFwbELwAANAYCF4AAGgMBC8AADQGghcAABoDwQsAAI2B4AUAgMZA8AIAQGMgeAEAoDEQvAAA0BgIXgAAaAwELwAANAaCFwAAGgPBCwAAjYHgBQCAxkDwAgBAYyB4AQCgMRC8AADQGAheAABoDAQvAAA0BoIXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELwAANAYCF4AAGgMBC8AADQGghcAABoDwQsAAI2B4AUAgMZA8AIwAefc1jnHXno5d5nA8oHgBaAuq3MXACyfyYLXObdyzn2pr/6Xcw4dEAAADGpovPdEtFa/reV3AAAAilqCt+T3s+OcO3ja+fbc5blW0A7gWpkkeJ1zayLaBA5v5PgSufX+v9QyXgNoB3CVTNV4tVb7mjh+dsT2DPvzmUE7gGtmquB99P7/SUPB+0jLAy/7MkA7gKtltOB1zm3odHj4xszfRPTm/baW85YEhrTLAO0ArpYpGq82I7ypf3v+acErDvR75U7Xu9RtnXO36Vyy7rOW/A7qPkf53dQgnXOPcvyorjs457JGJM65W8lDLxY4WTiQm59QrR3EpdGqm/45g/UTyXMjz6Tz22MiEEyGmYsTdcPEIxGxpI/IsWNBvifXEdFqRFmO6viaiLZEdPDOyUkfiftuC/I6ENE641k+vGv28tstEe0z7nEkoltVvmNO2SLluc/M46TerGedsR1eCvJ6zOxPufX9aPSD3Zh3Cum60riLug7nd7atOr5Tx+8z8lwbnTtHWA2uS5Q1N31F7lkqPAaCMZDvlzpf12OyzNSNML4Kr3sJlOejMB+/HKs524E6ATmmfNtI/a9G1J3+MEHwIiXTWFNDyMzQ8544vyXfNa9zzu1paD75RUR3zOz6RETPKo8VER0KXOxW9Hdy8k3yu/Hyv6Fhva9JtGv5+5WInlS5fhLRb3Vdaij+S+7/089L8ruT47oc2uxQtR2o+yj5ZpxPInpWZfthlO0lYv7x667nlbx6l3yfvHwxSQjKKZXUNNQwzaEqDTWBqNnAyLeKxmucP3poSLbWtomcb2llsaG91rb2qTogW/s2NdhEXSdHJYk89ZC/1FQzpR2CdZp7r9K2lWtujfaFxouUTGM0Xq29au22R2saS3QtK0VPqjwzc+j5iTsvjwf186bA0+MXM2vtVKPv/5uZn2MXSJ6f6uepk126vatMKgbw28Gq4xOY+ZVOtXxrBFbUtpLvJw1HHQAkGSN4tQDVL1yP7pCLW0xRguE+903hZ/+DCDl9Xs26SAnm2teZyHOemAXmCJTknNOxQV7lA5fC748r39wwtm0BGEuR4DU66Huo04s24L/ct7Vcq86E1lKDz24wp827tu10CjrPOeyfug9pzT1ErGxT2haAYko13tSkmmZOTa81Y19469zVguNYLB3dDmM1d7/+p7QtAMVkC14ZNpYKXq3pXbKdVwvKbI0oYKeF4B2HrjcdC9pM1E3++fjtN7ptARhDicZ7T6fDs7fUcEzMDb72sBIb3SWiX85STUuffzFuSN6qOUvI9Sv0ttTmmWp9sLSrn09V+zcAmv8KztUC8140iVLuCTPBRBcieJ1zO4qPVNY01CaXzjcpheBcBQHXSZbgTcTdLeXeObfC5MXyh7POuQMtL9aG5gZ9CVwauaaG2uaBS7T16uFn6ZD3ouyIYjrQQveNwqvX9Eq9uZjDZDO1bQEoYqzgvdMvXyxRt8Qylt8lMPqFD3gwLN2OqD+Or8z8YC0qYOZPWaRwDsFbw0WxhRscAH9ICl7xvfU797dMmpWgbbqX6NOrn7mk/Prc74wVaWdDPhRWzIIlMKUdQuiPyaX1TXBh5Gi8WjstXtHDwwDpVr6LHnrT8OXcFKzMsobsS2ZgFlmQHVUL3hqjJ51nSdsCUEyO4NVDzuj69QhRn97Ai91C88i6hwyxfS3Vjx4WRLRHfd7SBe85ht657fBGyge3QmDydxq6l13iPAS4EKKCV3xu/ZfuOxU4JIIWNpZPrx5+Rzu/mCt2heUYDCvFZcrPdxV4mfVw+yUW8Ea0pr36+W1CHbZiYAaJ7S4hfr57KpuUqt0OozVf+egXta2Ub0MQ0GAMsdBlNIzEHw03mEpGfnt13NpN4CQ0InUfguhOBhnlyA2gbe2kYO1OsCMV5JzsHSCOVp7eNTosZDQsoVyzUdcEA7ir63SQdR3M3nrOR3XOvZFPdljPie1gXbvXZVTt0YeHHISspHAQ9FDbhvofwkIiJVP4QNcRa8dstWKerrzja0NYpdJOv4QZ5bjNvI8ZQzjy0sVS6Q4U5xa8pW3xYQjDlOAd3Q40fgeKYB0VlEcLewhepKIUMzUMgpFwZ1+bwhtF3IG4m+mPxlb1eKfOre2JTs0YyUkg7rwyflDne2p5F/yibueGUOS1n3JtLn1ZU94g+n45E1pjrkle57VFTn6vzKx3okh6bkxpB2b+Nu6Zi3mNlOeO8oLk/KbOp/mBTsu+lElIsGAcM5+7DANkQuqehktR3yW9pV7qVogN8paGs+t9HN73DIG7WMRO/SjpJCQodc92djczr4xWOxD9bQvKLa/YjC0b7isRfVZQQsAVs0jBCwAA/zJjN7sEAAAwEgheAABoDAQvAAA0BoIXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELwAANAYCF4AAGjMKMEr8Ve3zrmDc46NtJfjl7i32kXhnLt3zh2l3j8ucEslAK6OolgNErzmhcq3W3ll5pJoXiADCQ5zVD9/StQuAMBCyRa8okkdaOQ2MNztNgwqIjsgHPTvqGsAlk2WqUE0XUvoPhPRDx5u5/4gx/rQeYhROg9WuMloCEplHpq6VxkAYAT/ZZ63o1Oh2weBNmPiIlZpG5j52zn3QH/b55OInhKX+Tbgkj3SAACVSApeMTHoTf+elhKI/NqRj1zWh05swti2HIAzk2Nq0EJ3yk7D4LxA6AKwAMa4k13sNjYApgUAlsAYwTur1uScW0V8hA9ybFQZ5Nqdke9Rjun9tf41IHgBWAKpbYiJaEuZ255PTdT5COduq/1YkK/1DKH0Ylzvb/l9zHl+6j5QJ9dFzvW3Kd+pY4803E7+1juut2m/946t5dlLt6P/iNTdvrBN1+r6rO3nkZD+5ZTj1WDZc7dUtr15FNFgDzTcUj7Gzjm34sSusc65Aw3t1DFOyiCudL6G3U9QpVzk9ERWTEv3j228++7ILrt/vtZi/b83NNypOQc/f21aunfOrTl/clWPIjA/AK6epKmBu63J9cuydc7tKpZjR6cC75OInvnUN/gHyRbdHi+xJbLOuT0NBdcbdV4ZId9jfY/WrOSZPqjsg2Ex1n/6z3XcTaQOhG9BXlrwwtUQgBy1mDqhaA1Jj0S0naJyU/di+nkeEufroe8uM98i84SXjx4qMxGtx1wXOffLqFem7oMULTMNzQjB9situ4y6zDIXUCegYWZAQlIpa3KNO633p3FoRZ3Wyc65Fxkel+KvnvqmTvOMleWVugUcPSHtS6/KemXmc2uzufwmojtmflpImd/oVHtey3LlFLptlvAsAJydbK8G7oacPyhso9sS0Zd4HmR5B0j0Ml9YvzJzzvDYH66utLlBhIL+CERtwQvjWT52i0DaRAvNqLlB7PaWmQeAq6fInYyZfzPzT+q039BLtKFu4uuYEQtA22dzhY0WznriavDCZwp0EGYgeBNuffd02i5vjNWOABDRyHi8zPzOzA9EdEPdhJT1QvVmiFiMWP372BdTa7e18gWCCE1/tLOiuNarj0HbBUCYtAMFM38z8ysz/6BOC7ZseLdEdAgIXy0wvwKB1U8SDV2ktDar84W2W4csc4PY+v1RxzcjcBIAf6i29Y9owU9EdEdDk8GKuhl6Ta2VVBC8DRDh6Y8eNoEJVUyqARCh+p5rzNzvgKAn4W5n2gromxA/oiU5Wi/MDABEmHOzywcaapqplWk3rBY2ZKQbTJw1RQvREw8WMSn57fy+JA8NAJbAbIJXhKHWevWwVE961QrAg8m0mZBJNl/4ap9eaLsAJJh7e/eU25cWkLV2yNX5IipXXWLmBv//3wTBC8CAuQVvStDqIWgtwavzheCtiCymOVk9KOE89cIV+E8DYDC34NULGVKCt9bkmxVRa6wZA4LDxtd6+1VqMDMAkMFsgldWrWlhd/IiinuSjgEweedbI1+iYeyG3LwswVtLMz83U57jF53W8T2dCt7fjC2iADDJ3d69aGcGcRvTixx+BZaM6hgKL5XcznS+U3aY0OWO5iMz+zXDZtbCcvE7KWe/A0gqI/kg+R9SvUQYvrsAhEiFL6PT0H576jRHM/QgdQJpTzQIo/hFkV0b6HQHBv9eZkjEvgykdkuYkq+U3Xw2snfG2JMXHpI6oRPd7SFSTh0WcpMbXs64XzRMZ6BOrJQT+jIULpRj7Y2EdO0pfcIwpmpp+ki9xCK0cgXCQKhXzneQH3UTRsfCfHb63pFythS8t5nPkiU4jfszFW4PhIR0bSnH1PBO44eNr8x8x4moVNzFfLgbeZ/gNSPzHZRVyh+NE+zxThJLl06H4rFJOn2sZEJPlzdV15/UhfcMBTf6Rd0OHbllsCbQMKkGQATHzPkndzbSFXUaYMjO+UxExIm90CL3WEnet2R7OfwmEaQl95CYAvfUzb5rb4tv6mzCvzkSzMXLQ9uv3yVdXehDscfvvZ++mfnmXOUB4BIoErwAaJxzL3TqMfI89qMLwLUAwQsm4Zw70qk3w49r0/oBKGXuBRTgH0bc5nyh+w6hC0AaCF4wBWzdDsAIYGoAo5CJxi/vp2/qzAxYYg1AAmi8oBjxZDion3N3iAbg6oHGC5KIG2FsCXS/6wgAIANovGAqJYtLAAAEwQvyeKfhxNlv6nx24T4GQCEwNQAAQGOg8QIAQGP+O3cBAABgSSTiuhB1ZrdPSsR2iZEdCN05x4Xpyzm3VTvQAgAuCAmM/2W828mttORaLRfGbkYwCufcrVEGcw9G59zaObenzj/9hWyhS/Q3UNZe8tNBs5LkmhrG7Fe2pq5wB+fcIfSwAIBFc0/DzWJ7jTCK+HVr3+4iRUxkR7HQ99Dl/LYmg2X5+4dxfg7F24q1svFuiOgD2i8AF0dIEOUKKB0LO1sGiDDU569L8jDOHZgGRCk80FDBfKZuNabzE3Xuk89eXsULh8YK3nddGFWwGymYz4o61RyaLwAXgLyrISG3yXyX9Y7fq4I9FUP3ztqkVTRjfa61AeuOToXub+oE7qulHTPzmxx7EJlXHH96Fo1Xdn54JaI7Ov0a9PuSAQCWjxaQOs5yjgC1BF3u7tYhwZ6r8VrnnZQnoFU/ze2bPqupQbaZ0Y31WGijAQCcB38izHyXUxnwcDdqonzBGRLQt5kyZKDtGvFEBrvRMLP1sahKCxuvtd8ZbL0ALBiZj/E1zjdDiK4z5220uSEpOA0zgRaGOffV5+QIVF3WWZhd8AZmNnNdUbbGrCbLb9sSzdk5t5FrjkZ+Ozk2yv4s7na7QL6j3WekTHsj395VL3fIlnuvF+NeH865bPNQ7Xqu3Q+m0Ko9arXFRLQZ4U3925MjAC2Bl7pOH9fadrQPSR/Lse9q2ozGc7Yips4u62/ffSjZypiG25c/Js5/ofT247l5rajbjDF7W/ZAPh/6HOo6Z85W6ckt7hP1HUuHnLwD5V8V1HX0GWrV81z9YEqq3R5zt0WF512pfv0ROXbMzPNDPcNLQdsfDTmyL2yzj8zzmIhWc9Xtn/uO7HilglcLJ/MlkUbVDZSTthXzM59NNfqRyoQMy/U5AvIw4vmPRHSbyFeXP/ejkVM31ep5jn4w6QWZoT3mbItKz/wYq1fqvAD84/cZeeqPiikIvfP99t/Lb/47FxX4NHw/TUFPnVas6zb6UahSx5kNMVrwBh7M7JRGZX0Yjb42Gt7M0yj3FxlCn7phzVZeMvNLSkOtvU87nac8s1XGVGezhPlOP5uUVb+oR4prpKHyHyW/jTr/PnDNxsi7Wj3P0Q9GvxwztcecbVHpufXHZm2Uxz8e1T69ttflNzVL6j68/nnbQD+Lfdx0ewQ/DsbzMmWMyCbVcWZDTBG8WdfS8CsbvYeR76CiVOVHBVPGc+iOf0i95MYzMYW1fevc4ItFtlYY0yJ1+Y+hsnjXrI0yDbTKyvVcvR9UKke19pizLSo8t75P6Bm0YEsOz41rTGFIQ8F+K79rJS400tXnpbRjSzns26V6HTPPLHjJ/sqZnZeGw6+chjy5ZkwHKuiQ+mXJ0jZoqJV9ZeafbPDAy5hTvyXl11rfLlGGmvU8uR9UbO9q7TFXW1R6bv2uh4SbNh3k1E/u8H9g3/WO+cI7NDrVzzBWIz8pK1W0q8/i1SCBKV5ouC/XMxs+crKSxZ+lzN2/y59hXdWc5a/IYDZWl9Nw3fkm2w3vBO6cvPV5Y9aax2jiXkO0nH6w4PZo0RbaCyf03Nq7YcxiipDHj+/RoK95D5zno9s/WW8il34Y9+vZEtGXeNJMDvQzVvBuDFeXP4nEJqeueeBuNZtFcUUJMTe1gaP0OQSzvIz6eXSHGfgbZgocomFHqS14U9Ss5zn6wRguuT1GY3xwgs/N3eIof3XXbUa7a2E9+Ega/ru6D/h/hz6yuv2SH02i7l1l5p9E9NMoq5/3TtwlR7v2tVhA8UpENxyPW6krb+xyvT+dhu0VM1W+ViMYOJAn/i7RbKy18M3iYVSu5+r9YCQX2x4TCfnuhijS7qWvlCoh+vzoQgoRxP6H97Pgo9mX852ZH+hvzBmrH66I6EV8q4sVjbkF7zszP2c8uO6YOv5nSLPWcTD1fbSGvaIKX6sR6IbTGpl+/uyOwvaa8tYveq16nqsflHLp7VGMaJqlgjfXdBDLs0gJEU3bbw99fjIaWS4sMWeY+Qd1WrClOd9Sp2gUCd+q0cmI6Emdt3F5kYhqdcyTF0Qa6adxXv+16ldTzW2C0C+uFd/Up1TTSwn2WalYz7P0gxFcdHuM5J5Oy/mWUpik3fXQP/W+p5b++n+HtNWYnVffv0rcBdGCn6gL/DUY1VA3iZ5NVY2XmX/R8EFfGi3ptIYxvtE8ZOd5pC5WcPFXa8E0f9EXVM9mPzgzlyJ4T/7OHG3otkyZG7TG+sdOa9h3Q0LTtPMa13/L/arBzJ/MfGeU7TZTySSieUwNWutdU94QxOfG0qgT6SYyEfCbmZ9EKw/ZbPpg7S1swFM1snPnbzJDPVftB2dkaeU5wcXj7pZyn6FohYKjp+y7Pbpfha4fbWbI4IGG7ZqtUFQXvGLj0ja/l8QEQ7OhmWezuSNbO9u5+jtl6OfRDaafv3TIPdomORcj63kpQ/R/rj0S1Pa8SH1UQ5PNuUFtBtpm4fWTkY+7zj+7n8w1ufZKw84bs4Hoc2cf8suQ4Ym64bHuCLW1Xt0g+n6jBU7ggzZrEOcSCuu5eT8I8M+2RwAteO9KRhk0HOWOtfPm2Hct74iQxjt3XN3Rboyz7UBBQ603NtGWcreaDdHQH2a+v85PN9iU5x/kHZhZPyuZ9Xy2fqD459ujR+yjU+2iA0+FmB3fcEHs7bQnPsSJe/rHVzJ68u+ZnBysgBa02e08mztZ4USbbuimTufyYviVVs39JxAXVA+9BxpAwYRkS7vWJDLq+az9wOMq2kPQdZy12MAn4Mudajvd1i+k/G8Lr9duiy0mWFOmsiBz+/FmTbTJ4gr/67Ru7GdLNNzsrhb6OQargcQjwL/nijLMHSLU9XlLf9GD9byQfnBt7aHLO3Z4XurTmwqoXqLxjrl+EtIv9cc4u63n3nOtZKLNOq+JxiMz7CVfW6KufNEhqOSrO2Bo2bT1/MFJPtHA9urnNysWxlLIrOez9YOMcvxr7aF9d6fsN2YtBw62W2ApfU9ytVlgFZx/fbbGW+rJJM+lF+38KjIp8biIRSVhIVc0jMYUiipkBb/eUziU4tYr24c61of16+NGWPFhQ1H/cyJK+eXTsWJD8XhTUfdbxuPNjYgVDbtYu55r94Mpaa72mKstKjzfpADgRn6pXSJCO25klWPq9ZKHH4Jy3/elwLmPgT7xRYW7VuQWbuoOFFZs00EsThq/8wCTCrcYuGdOClW6fllCgjiUckNpttiBotrLXruea/eDqWmO9pirLQqfSwcbN9/JCu95UCBROBRjVjloGLe3+DkieeSmUdswja3Q4i8jFcQSJVtbLHqRqbMnlwrHYDBq62UxXoRQKnpBCvJlGrfnGlPmTg2ptq9dz7X7QY1Uuz3maovCZ9JCb/IHi+zRbfSjYtRFVgxm7356pFH0HPR3uW9pP5tU/05uvjjEZvZI3bDdshX9iX3K4XCTvbtMH+7OsuW8UWcTCuYh+XzR6Sz8T2Z+98q5oaGB/5m6ybRRM6xiwLeev3/20XnXplY9G/lW6Qc1uKT2AOV4cxCx1bbPRNP72mIF79IICd5zlQcAcLm0iMcLAADAA4IXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELz56LXjSw9uDQBYKPDjBQCAxkDjBQCAxkDwAgBAYyB4AQCgMRC8AADQGAheAABoDAQvAAA0BoIXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELwAANAYCF4AAGgMBC8AADQGghcAABoDwQsAAI2B4AUAgMZA8AIAQGMgeAEAoDEQvAAA0BgIXgAAaMz/AX5pcqkbHzkuAAAAAElFTkSuQmCC" + }, + { + "Category": "FORM_LOGO", + "ColorMode": "LIGHT", + "Extension": "PNG", + "Bytes": "iVBORw0KGgoAAAANSUhEUgAAAV4AAACyCAYAAAAK2qdXAAAZtklEQVR4nO2d7XXbPA+Gwfc8CzgjuCMkI7gjJCMkIzgjJCO4IzgjOCMkI6QjOCPg/SGopSHwS6Joub6vc3jaWBJFkRQEgiDomJkAAAC043/nLgAAAFwbELwAANAYCF4AAGgMBC8AADQGghcAABoDwQsAAI2B4AUAgMZA8AIAQGMgeAEAoDEQvAAA0BgIXgAAaAwELwAANAaCFwAAGgPBCwAAjYHgBQCAxkDwAgBAYyB4AQCgMRC8AADQGAheAABoDAQvAAA0BoIXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELwAANAYCF4AAGgMBC8AADQGghcAABoDwQsAAI2B4AUAgMZA8AIwAefc1jnHXno5d5nA8oHgBaAuq3MXACyfyYLXObdyzn2pr/6Xcw4dEAAADGpovPdEtFa/reV3AAAAilqCt+T3s+OcO3ja+fbc5blW0A7gWpkkeJ1zayLaBA5v5PgSufX+v9QyXgNoB3CVTNV4tVb7mjh+dsT2DPvzmUE7gGtmquB99P7/SUPB+0jLAy/7MkA7gKtltOB1zm3odHj4xszfRPTm/baW85YEhrTLAO0ArpYpGq82I7ypf3v+acErDvR75U7Xu9RtnXO36Vyy7rOW/A7qPkf53dQgnXOPcvyorjs457JGJM65W8lDLxY4WTiQm59QrR3EpdGqm/45g/UTyXMjz6Tz22MiEEyGmYsTdcPEIxGxpI/IsWNBvifXEdFqRFmO6viaiLZEdPDOyUkfiftuC/I6ENE641k+vGv28tstEe0z7nEkoltVvmNO2SLluc/M46TerGedsR1eCvJ6zOxPufX9aPSD3Zh3Cum60riLug7nd7atOr5Tx+8z8lwbnTtHWA2uS5Q1N31F7lkqPAaCMZDvlzpf12OyzNSNML4Kr3sJlOejMB+/HKs524E6ATmmfNtI/a9G1J3+MEHwIiXTWFNDyMzQ8544vyXfNa9zzu1paD75RUR3zOz6RETPKo8VER0KXOxW9Hdy8k3yu/Hyv6Fhva9JtGv5+5WInlS5fhLRb3Vdaij+S+7/089L8ruT47oc2uxQtR2o+yj5ZpxPInpWZfthlO0lYv7x667nlbx6l3yfvHwxSQjKKZXUNNQwzaEqDTWBqNnAyLeKxmucP3poSLbWtomcb2llsaG91rb2qTogW/s2NdhEXSdHJYk89ZC/1FQzpR2CdZp7r9K2lWtujfaFxouUTGM0Xq29au22R2saS3QtK0VPqjwzc+j5iTsvjwf186bA0+MXM2vtVKPv/5uZn2MXSJ6f6uepk126vatMKgbw28Gq4xOY+ZVOtXxrBFbUtpLvJw1HHQAkGSN4tQDVL1yP7pCLW0xRguE+903hZ/+DCDl9Xs26SAnm2teZyHOemAXmCJTknNOxQV7lA5fC748r39wwtm0BGEuR4DU66Huo04s24L/ct7Vcq86E1lKDz24wp827tu10CjrPOeyfug9pzT1ErGxT2haAYko13tSkmmZOTa81Y19469zVguNYLB3dDmM1d7/+p7QtAMVkC14ZNpYKXq3pXbKdVwvKbI0oYKeF4B2HrjcdC9pM1E3++fjtN7ptARhDicZ7T6fDs7fUcEzMDb72sBIb3SWiX85STUuffzFuSN6qOUvI9Sv0ttTmmWp9sLSrn09V+zcAmv8KztUC8140iVLuCTPBRBcieJ1zO4qPVNY01CaXzjcpheBcBQHXSZbgTcTdLeXeObfC5MXyh7POuQMtL9aG5gZ9CVwauaaG2uaBS7T16uFn6ZD3ouyIYjrQQveNwqvX9Eq9uZjDZDO1bQEoYqzgvdMvXyxRt8Qylt8lMPqFD3gwLN2OqD+Or8z8YC0qYOZPWaRwDsFbw0WxhRscAH9ICl7xvfU797dMmpWgbbqX6NOrn7mk/Prc74wVaWdDPhRWzIIlMKUdQuiPyaX1TXBh5Gi8WjstXtHDwwDpVr6LHnrT8OXcFKzMsobsS2ZgFlmQHVUL3hqjJ51nSdsCUEyO4NVDzuj69QhRn97Ai91C88i6hwyxfS3Vjx4WRLRHfd7SBe85ht657fBGyge3QmDydxq6l13iPAS4EKKCV3xu/ZfuOxU4JIIWNpZPrx5+Rzu/mCt2heUYDCvFZcrPdxV4mfVw+yUW8Ea0pr36+W1CHbZiYAaJ7S4hfr57KpuUqt0OozVf+egXta2Ub0MQ0GAMsdBlNIzEHw03mEpGfnt13NpN4CQ0InUfguhOBhnlyA2gbe2kYO1OsCMV5JzsHSCOVp7eNTosZDQsoVyzUdcEA7ir63SQdR3M3nrOR3XOvZFPdljPie1gXbvXZVTt0YeHHISspHAQ9FDbhvofwkIiJVP4QNcRa8dstWKerrzja0NYpdJOv4QZ5bjNvI8ZQzjy0sVS6Q4U5xa8pW3xYQjDlOAd3Q40fgeKYB0VlEcLewhepKIUMzUMgpFwZ1+bwhtF3IG4m+mPxlb1eKfOre2JTs0YyUkg7rwyflDne2p5F/yibueGUOS1n3JtLn1ZU94g+n45E1pjrkle57VFTn6vzKx3okh6bkxpB2b+Nu6Zi3mNlOeO8oLk/KbOp/mBTsu+lElIsGAcM5+7DANkQuqehktR3yW9pV7qVogN8paGs+t9HN73DIG7WMRO/SjpJCQodc92djczr4xWOxD9bQvKLa/YjC0b7isRfVZQQsAVs0jBCwAA/zJjN7sEAAAwEgheAABoDAQvAAA0BoIXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELwAANAYCF4AAGjMKMEr8Ve3zrmDc46NtJfjl7i32kXhnLt3zh2l3j8ucEslAK6OolgNErzmhcq3W3ll5pJoXiADCQ5zVD9/StQuAMBCyRa8okkdaOQ2MNztNgwqIjsgHPTvqGsAlk2WqUE0XUvoPhPRDx5u5/4gx/rQeYhROg9WuMloCEplHpq6VxkAYAT/ZZ63o1Oh2weBNmPiIlZpG5j52zn3QH/b55OInhKX+Tbgkj3SAACVSApeMTHoTf+elhKI/NqRj1zWh05swti2HIAzk2Nq0EJ3yk7D4LxA6AKwAMa4k13sNjYApgUAlsAYwTur1uScW0V8hA9ybFQZ5Nqdke9Rjun9tf41IHgBWAKpbYiJaEuZ255PTdT5COduq/1YkK/1DKH0Ylzvb/l9zHl+6j5QJ9dFzvW3Kd+pY4803E7+1juut2m/946t5dlLt6P/iNTdvrBN1+r6rO3nkZD+5ZTj1WDZc7dUtr15FNFgDzTcUj7Gzjm34sSusc65Aw3t1DFOyiCudL6G3U9QpVzk9ERWTEv3j228++7ILrt/vtZi/b83NNypOQc/f21aunfOrTl/clWPIjA/AK6epKmBu63J9cuydc7tKpZjR6cC75OInvnUN/gHyRbdHi+xJbLOuT0NBdcbdV4ZId9jfY/WrOSZPqjsg2Ex1n/6z3XcTaQOhG9BXlrwwtUQgBy1mDqhaA1Jj0S0naJyU/di+nkeEufroe8uM98i84SXjx4qMxGtx1wXOffLqFem7oMULTMNzQjB9situ4y6zDIXUCegYWZAQlIpa3KNO633p3FoRZ3Wyc65Fxkel+KvnvqmTvOMleWVugUcPSHtS6/KemXmc2uzufwmojtmflpImd/oVHtey3LlFLptlvAsAJydbK8G7oacPyhso9sS0Zd4HmR5B0j0Ml9YvzJzzvDYH66utLlBhIL+CERtwQvjWT52i0DaRAvNqLlB7PaWmQeAq6fInYyZfzPzT+q039BLtKFu4uuYEQtA22dzhY0WznriavDCZwp0EGYgeBNuffd02i5vjNWOABDRyHi8zPzOzA9EdEPdhJT1QvVmiFiMWP372BdTa7e18gWCCE1/tLOiuNarj0HbBUCYtAMFM38z8ysz/6BOC7ZseLdEdAgIXy0wvwKB1U8SDV2ktDar84W2W4csc4PY+v1RxzcjcBIAf6i29Y9owU9EdEdDk8GKuhl6Ta2VVBC8DRDh6Y8eNoEJVUyqARCh+p5rzNzvgKAn4W5n2gromxA/oiU5Wi/MDABEmHOzywcaapqplWk3rBY2ZKQbTJw1RQvREw8WMSn57fy+JA8NAJbAbIJXhKHWevWwVE961QrAg8m0mZBJNl/4ap9eaLsAJJh7e/eU25cWkLV2yNX5IipXXWLmBv//3wTBC8CAuQVvStDqIWgtwavzheCtiCymOVk9KOE89cIV+E8DYDC34NULGVKCt9bkmxVRa6wZA4LDxtd6+1VqMDMAkMFsgldWrWlhd/IiinuSjgEweedbI1+iYeyG3LwswVtLMz83U57jF53W8T2dCt7fjC2iADDJ3d69aGcGcRvTixx+BZaM6hgKL5XcznS+U3aY0OWO5iMz+zXDZtbCcvE7KWe/A0gqI/kg+R9SvUQYvrsAhEiFL6PT0H576jRHM/QgdQJpTzQIo/hFkV0b6HQHBv9eZkjEvgykdkuYkq+U3Xw2snfG2JMXHpI6oRPd7SFSTh0WcpMbXs64XzRMZ6BOrJQT+jIULpRj7Y2EdO0pfcIwpmpp+ki9xCK0cgXCQKhXzneQH3UTRsfCfHb63pFythS8t5nPkiU4jfszFW4PhIR0bSnH1PBO44eNr8x8x4moVNzFfLgbeZ/gNSPzHZRVyh+NE+zxThJLl06H4rFJOn2sZEJPlzdV15/UhfcMBTf6Rd0OHbllsCbQMKkGQATHzPkndzbSFXUaYMjO+UxExIm90CL3WEnet2R7OfwmEaQl95CYAvfUzb5rb4tv6mzCvzkSzMXLQ9uv3yVdXehDscfvvZ++mfnmXOUB4BIoErwAaJxzL3TqMfI89qMLwLUAwQsm4Zw70qk3w49r0/oBKGXuBRTgH0bc5nyh+w6hC0AaCF4wBWzdDsAIYGoAo5CJxi/vp2/qzAxYYg1AAmi8oBjxZDion3N3iAbg6oHGC5KIG2FsCXS/6wgAIANovGAqJYtLAAAEwQvyeKfhxNlv6nx24T4GQCEwNQAAQGOg8QIAQGP+O3cBAABgSSTiuhB1ZrdPSsR2iZEdCN05x4Xpyzm3VTvQAgAuCAmM/2W828mttORaLRfGbkYwCufcrVEGcw9G59zaObenzj/9hWyhS/Q3UNZe8tNBs5LkmhrG7Fe2pq5wB+fcIfSwAIBFc0/DzWJ7jTCK+HVr3+4iRUxkR7HQ99Dl/LYmg2X5+4dxfg7F24q1svFuiOgD2i8AF0dIEOUKKB0LO1sGiDDU569L8jDOHZgGRCk80FDBfKZuNabzE3Xuk89eXsULh8YK3nddGFWwGymYz4o61RyaLwAXgLyrISG3yXyX9Y7fq4I9FUP3ztqkVTRjfa61AeuOToXub+oE7qulHTPzmxx7EJlXHH96Fo1Xdn54JaI7Ov0a9PuSAQCWjxaQOs5yjgC1BF3u7tYhwZ6r8VrnnZQnoFU/ze2bPqupQbaZ0Y31WGijAQCcB38izHyXUxnwcDdqonzBGRLQt5kyZKDtGvFEBrvRMLP1sahKCxuvtd8ZbL0ALBiZj/E1zjdDiK4z5220uSEpOA0zgRaGOffV5+QIVF3WWZhd8AZmNnNdUbbGrCbLb9sSzdk5t5FrjkZ+Ozk2yv4s7na7QL6j3WekTHsj395VL3fIlnuvF+NeH865bPNQ7Xqu3Q+m0Ko9arXFRLQZ4U3925MjAC2Bl7pOH9fadrQPSR/Lse9q2ozGc7Yips4u62/ffSjZypiG25c/Js5/ofT247l5rajbjDF7W/ZAPh/6HOo6Z85W6ckt7hP1HUuHnLwD5V8V1HX0GWrV81z9YEqq3R5zt0WF512pfv0ROXbMzPNDPcNLQdsfDTmyL2yzj8zzmIhWc9Xtn/uO7HilglcLJ/MlkUbVDZSTthXzM59NNfqRyoQMy/U5AvIw4vmPRHSbyFeXP/ejkVM31ep5jn4w6QWZoT3mbItKz/wYq1fqvAD84/cZeeqPiikIvfP99t/Lb/47FxX4NHw/TUFPnVas6zb6UahSx5kNMVrwBh7M7JRGZX0Yjb42Gt7M0yj3FxlCn7phzVZeMvNLSkOtvU87nac8s1XGVGezhPlOP5uUVb+oR4prpKHyHyW/jTr/PnDNxsi7Wj3P0Q9GvxwztcecbVHpufXHZm2Uxz8e1T69ttflNzVL6j68/nnbQD+Lfdx0ewQ/DsbzMmWMyCbVcWZDTBG8WdfS8CsbvYeR76CiVOVHBVPGc+iOf0i95MYzMYW1fevc4ItFtlYY0yJ1+Y+hsnjXrI0yDbTKyvVcvR9UKke19pizLSo8t75P6Bm0YEsOz41rTGFIQ8F+K79rJS400tXnpbRjSzns26V6HTPPLHjJ/sqZnZeGw6+chjy5ZkwHKuiQ+mXJ0jZoqJV9ZeafbPDAy5hTvyXl11rfLlGGmvU8uR9UbO9q7TFXW1R6bv2uh4SbNh3k1E/u8H9g3/WO+cI7NDrVzzBWIz8pK1W0q8/i1SCBKV5ouC/XMxs+crKSxZ+lzN2/y59hXdWc5a/IYDZWl9Nw3fkm2w3vBO6cvPV5Y9aax2jiXkO0nH6w4PZo0RbaCyf03Nq7YcxiipDHj+/RoK95D5zno9s/WW8il34Y9+vZEtGXeNJMDvQzVvBuDFeXP4nEJqeueeBuNZtFcUUJMTe1gaP0OQSzvIz6eXSHGfgbZgocomFHqS14U9Ss5zn6wRguuT1GY3xwgs/N3eIof3XXbUa7a2E9+Ega/ru6D/h/hz6yuv2SH02i7l1l5p9E9NMoq5/3TtwlR7v2tVhA8UpENxyPW6krb+xyvT+dhu0VM1W+ViMYOJAn/i7RbKy18M3iYVSu5+r9YCQX2x4TCfnuhijS7qWvlCoh+vzoQgoRxP6H97Pgo9mX852ZH+hvzBmrH66I6EV8q4sVjbkF7zszP2c8uO6YOv5nSLPWcTD1fbSGvaIKX6sR6IbTGpl+/uyOwvaa8tYveq16nqsflHLp7VGMaJqlgjfXdBDLs0gJEU3bbw99fjIaWS4sMWeY+Qd1WrClOd9Sp2gUCd+q0cmI6Emdt3F5kYhqdcyTF0Qa6adxXv+16ldTzW2C0C+uFd/Up1TTSwn2WalYz7P0gxFcdHuM5J5Oy/mWUpik3fXQP/W+p5b++n+HtNWYnVffv0rcBdGCn6gL/DUY1VA3iZ5NVY2XmX/R8EFfGi3ptIYxvtE8ZOd5pC5WcPFXa8E0f9EXVM9mPzgzlyJ4T/7OHG3otkyZG7TG+sdOa9h3Q0LTtPMa13/L/arBzJ/MfGeU7TZTySSieUwNWutdU94QxOfG0qgT6SYyEfCbmZ9EKw/ZbPpg7S1swFM1snPnbzJDPVftB2dkaeU5wcXj7pZyn6FohYKjp+y7Pbpfha4fbWbI4IGG7ZqtUFQXvGLj0ja/l8QEQ7OhmWezuSNbO9u5+jtl6OfRDaafv3TIPdomORcj63kpQ/R/rj0S1Pa8SH1UQ5PNuUFtBtpm4fWTkY+7zj+7n8w1ufZKw84bs4Hoc2cf8suQ4Ym64bHuCLW1Xt0g+n6jBU7ggzZrEOcSCuu5eT8I8M+2RwAteO9KRhk0HOWOtfPm2Hct74iQxjt3XN3Rboyz7UBBQ603NtGWcreaDdHQH2a+v85PN9iU5x/kHZhZPyuZ9Xy2fqD459ujR+yjU+2iA0+FmB3fcEHs7bQnPsSJe/rHVzJ68u+ZnBysgBa02e08mztZ4USbbuimTufyYviVVs39JxAXVA+9BxpAwYRkS7vWJDLq+az9wOMq2kPQdZy12MAn4Mudajvd1i+k/G8Lr9duiy0mWFOmsiBz+/FmTbTJ4gr/67Ru7GdLNNzsrhb6OQargcQjwL/nijLMHSLU9XlLf9GD9byQfnBt7aHLO3Z4XurTmwqoXqLxjrl+EtIv9cc4u63n3nOtZKLNOq+JxiMz7CVfW6KufNEhqOSrO2Bo2bT1/MFJPtHA9urnNysWxlLIrOez9YOMcvxr7aF9d6fsN2YtBw62W2ApfU9ytVlgFZx/fbbGW+rJJM+lF+38KjIp8biIRSVhIVc0jMYUiipkBb/eUziU4tYr24c61of16+NGWPFhQ1H/cyJK+eXTsWJD8XhTUfdbxuPNjYgVDbtYu55r94Mpaa72mKstKjzfpADgRn6pXSJCO25klWPq9ZKHH4Jy3/elwLmPgT7xRYW7VuQWbuoOFFZs00EsThq/8wCTCrcYuGdOClW6fllCgjiUckNpttiBotrLXruea/eDqWmO9pirLQqfSwcbN9/JCu95UCBROBRjVjloGLe3+DkieeSmUdswja3Q4i8jFcQSJVtbLHqRqbMnlwrHYDBq62UxXoRQKnpBCvJlGrfnGlPmTg2ptq9dz7X7QY1Uuz3maovCZ9JCb/IHi+zRbfSjYtRFVgxm7356pFH0HPR3uW9pP5tU/05uvjjEZvZI3bDdshX9iX3K4XCTvbtMH+7OsuW8UWcTCuYh+XzR6Sz8T2Z+98q5oaGB/5m6ybRRM6xiwLeev3/20XnXplY9G/lW6Qc1uKT2AOV4cxCx1bbPRNP72mIF79IICd5zlQcAcLm0iMcLAADAA4IXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELz56LXjSw9uDQBYKPDjBQCAxkDjBQCAxkDwAgBAYyB4AQCgMRC8AADQGAheAABoDAQvAAA0BoIXAAAaA8ELAACNgeAFAIDGQPACAEBjIHgBAKAxELwAANAYCF4AAGgMBC8AADQGghcAABoDwQsAAI2B4AUAgMZA8AIAQGMgeAEAoDEQvAAA0BgIXgAAaMz/AX5pcqkbHzkuAAAAAElFTkSuQmCC" + } + ], + "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 00000000..1141f0a6 Binary files /dev/null and b/source/webui/public/favicon.ico differ diff --git a/source/webui/public/logo.png b/source/webui/public/logo.png new file mode 100644 index 00000000..1141f0a6 Binary files /dev/null and b/source/webui/public/logo.png differ 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 ( + <> + +
Redirecting to login...
+ + ); + } + + 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" + /> + + + +