├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── deployment ├── build-s3-dist.sh └── run-unit-tests.sh ├── solution-manifest.yaml └── source ├── assets └── diagrams │ ├── architecture-diagram-v_1-0_0.jpg │ ├── architecture-diagram-v_1-0_0_apis.jpg │ ├── architecture-diagram-v_1-0_0_auto_session.jpg │ ├── architecture-diagram-v_1-0_0_base.jpg │ └── architecture-diagram-v_1-0_0_key_rotation.jpg ├── bin ├── secure_media_stream.ts └── wizard │ ├── index.ts │ └── lib │ ├── api-module.ts │ ├── auto-session-revocation-module.ts │ ├── handlers.ts │ ├── main-module.ts │ └── prompt-component.ts ├── cdk.context.json ├── cdk.json ├── helpers ├── opts.ts └── validators │ ├── api.ts │ ├── auto_session_revocation.ts │ ├── configuration.ts │ ├── hosting.ts │ └── main.ts ├── install_dependencies.ps1 ├── install_dependencies.sh ├── jest.config.js ├── lambda ├── custom_resource_us_east_1 │ ├── index.js │ └── le.js ├── export_params │ └── index.js ├── generate_secret_update_cff │ ├── cff.js │ └── index.js ├── generate_token │ └── nodejs │ │ └── index.js ├── layers │ ├── admzip │ │ └── nodejs │ │ │ ├── package-lock.json │ │ │ └── package.json │ └── aws_secure_media_delivery_nodejs │ │ └── nodejs │ │ ├── package-lock.json │ │ └── package.json ├── prepare_query │ └── index.js ├── save_auto_session │ └── index.js ├── save_manual_session │ └── nodejs │ │ └── index.js ├── swap_secrets │ └── index.js ├── update_rulegroup │ └── index.js └── update_token │ └── index.js ├── lib ├── api │ ├── api.ts │ └── endpoints.ts ├── application_registry │ └── application_registry.ts ├── auto_revocation_stack.ts ├── autorevocation │ └── auto_revocation_workflow.ts ├── cfn │ ├── cfn_parameters.ts │ └── check_input_parameters.ts ├── cfn_nag │ └── cfn_nag_utils.ts ├── custom_resources │ ├── cr_create_le_rule.ts │ ├── cr_init_secrets.ts │ ├── cr_load_assets_table.ts │ └── cr_load_athena_config_table.ts ├── main │ ├── dashboard.ts │ ├── rotate_secrets_workflow.ts │ ├── secrets.ts │ └── session_revocation.ts └── secure_media_stream_stack.ts ├── package-lock.json ├── package.json ├── resources ├── demo_website │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── css │ │ │ └── app.css │ │ ├── img │ │ │ ├── favicon.ico │ │ │ └── smile.png │ │ ├── index.html │ │ ├── index.js │ │ └── js │ │ │ └── main.js │ └── webpack.config.js ├── empty_demo_website │ └── .empty ├── mock │ └── assets.json └── sdk │ └── node │ └── v1 │ ├── aws-secure-media-delivery.js │ ├── package-lock.json │ └── package.json ├── solution.context.json.template ├── test ├── __mocks__ │ ├── aws-sdk-mock.ts │ └── mock-responses.ts ├── api.test.ts ├── auto_revocation_workflow.test.ts ├── auto_session_revocation_stack.test.ts ├── check_input_parameters.test.ts ├── check_token_cff.test.ts ├── cr_create_le_rule.test.ts ├── cr_init_secrets.test.ts ├── cr_load_assets_table.test.ts ├── cr_load_athena_config_table.test.ts ├── custom_resource_us_east_1.test.ts ├── dashboard.test.ts ├── end_to_end_token.test.ts ├── endpoints.test.ts ├── export_params.test.ts ├── generate_token.test.ts ├── generate_update_secrets.test.ts ├── prepare_query.test.ts ├── rotate_secrets_workflow.test.ts ├── save_auto_session.test.ts ├── save_manual_session.test.ts ├── secrets.test.ts ├── secure_media_delivery.test.ts ├── secure_media_stream_stack.test.ts ├── session_revocation.test.ts ├── sig4_lambda_edge.test.ts ├── swap_secrets.test.ts ├── update_rulegroup.test.ts └── update_token.test.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Please complete the following information about the solution:** 20 | - [ ] Version: [e.g. v1.0.0] 21 | 22 | To get the version of the solution, you can look at the description of the created CloudFormation stack. For example, "_(SO0021) - Video On Demand workflow with AWS Step Functions, MediaConvert, MediaPackage, S3, CloudFront and DynamoDB. Version **v5.0.0**_". If the description does not contain the version information, you can look at the mappings section of the template: 23 | 24 | ```yaml 25 | Mappings: 26 | SourceCode: 27 | General: 28 | S3Bucket: "solutions" 29 | KeyPrefix: "video-on-demand-on-aws/v5.0.0" 30 | ``` 31 | 32 | - [ ] Region: [e.g. us-east-1] 33 | - [ ] Was the solution modified from the version published on this repository? 34 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub? 35 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses? 36 | - [ ] Were there any errors in the CloudWatch Logs? 37 | 38 | **Screenshots** 39 | If applicable, add screenshots to help explain your problem (please **DO NOT include sensitive information**). 40 | 41 | **Additional context** 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this solution 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the feature you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !deployment/.typescript/cdk-solution-helper/index.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | !source/.typescript/lambda/**/*.js 6 | !source/lambda/**/*.js 7 | source/solution.context.json 8 | source/lambda/layers/jsonpath/python/* 9 | source/lambda/layers/aws_secure_media_delivery_python/python/* 10 | !source/lambda/layers/aws_secure_media_delivery_python/python/requirements.txt 11 | 12 | # CDK asset staging directory 13 | .cdk.staging 14 | cdk.out 15 | dist 16 | !**/cdk-solution-helper/index.js 17 | .venv 18 | deployment/global-s3-assets 19 | deployment/regional-s3-assets 20 | deployment/staging 21 | open-source/ 22 | deployment/staging 23 | 24 | # Temporary folders 25 | tmp/ 26 | temp/ 27 | 28 | ### VisualStudioCode ### 29 | .vscode/* 30 | 31 | ### macOS ### 32 | .DS_Store 33 | 34 | **/coverage 35 | **/coverage-reports 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 7 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [1.2.6] - 2024-11-21 10 | 11 | ### Security 12 | 13 | - Security updates for npm packages 14 | 15 | ## [1.2.5] - 2024-09-17 16 | 17 | ### Security 18 | 19 | - Security updates for npm packages 20 | 21 | ## [1.2.4] - 2024-08-09 22 | 23 | ### Security 24 | 25 | - Upgraded vulnerable packages 26 | 27 | ## [1.2.3] - 2024-06-11 28 | 29 | ### Security 30 | 31 | - CDK updates to 2.143.0 which removed alpha code and fixed deployment issues 32 | 33 | ### Changed 34 | 35 | - Added solution manifest 36 | - Updated nested dependencies for Security Updates 37 | 38 | ## [1.2.2] - 2023-11-02 39 | 40 | ### Changed 41 | 42 | - Security updates 43 | - Added solution manifest 44 | 45 | ## [1.2.1] - 2023-09-28 46 | 47 | ### Fixed 48 | 49 | - Fixed an issue where the solution was not deployed properly with `cdk deploy` command 50 | 51 | ## [1.2.0] - 2023-06-29 52 | 53 | ### Changed 54 | 55 | - Integrated Service Catalog App Registry into the solution. 56 | - All AWS Lambda functions have been updated to the NodeJS 18 runtime. 57 | - Along with NodeJS 18 runtime upgrade, [aws-sdk v2](https://github.com/aws/aws-sdk-js) has been updated to [aws-sdk v3](https://github.com/aws/aws-sdk-js-v3) 58 | 59 | ### Removed 60 | 61 | - [aws-sdk v2](https://github.com/aws/aws-sdk-js) has been removed from Lambda Layers 62 | 63 | ## [1.1.4] - 2023-06-01 64 | 65 | ### Changed 66 | 67 | - Updated aws-cdk and aws-cdk-lib to newest version 2.81.0. 68 | 69 | ### Fixed 70 | 71 | - Custom resource created with aws-cdk-lib now on NodeJS 16. 72 | 73 | ## [1.1.3] - 2023-05-18 74 | 75 | ### Changed 76 | 77 | - Updated video player on the demo website to a stable version. 78 | - Updated WAF RuleGroup Name Arn to have the correct value. 79 | - All AWS Lambda functions have been updated to the NodeJS 16 runtime. 80 | 81 | ## [1.1.2] - 2023-04-14 82 | 83 | ### Changed 84 | 85 | - Updated object ownership configuration on on the ApiEndpointsLogsBucket bucket 86 | 87 | ## [1.1.1] - 2023-02-02 88 | 89 | ### Added 90 | 91 | - Added architecture diagram. 92 | - Added the package babel/traverse 93 | 94 | ## [1.1.0] - 2022-10-15 95 | 96 | ### Added 97 | 98 | - Upgrade wizard enforce http or https protocol 99 | - Upgrade demo website with QRcode, token parameters selector and links to solutions page 100 | - Add constraints in the wizard for stack name and video asset url format 101 | - Add suffix on solution name 102 | 103 | ## [1.0.1] - 2022-09-05 104 | 105 | ### Added 106 | 107 | - Add metrics for Lambda Custom Resource 108 | 109 | ## [1.0.0] - 2022-08-25 110 | 111 | ### Added 112 | 113 | - All files, initial version 114 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing 15 | open](https://github.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/issues), or [recently 16 | closed](https://github.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), 17 | issues to make sure somebody else hasn't already reported the issue. Please try to include as much 18 | information as you can. Details like these are incredibly useful: 19 | 20 | * A reproducible test case or series of steps 21 | * The version of our code being used 22 | * Any modifications you've made relevant to the bug 23 | * Anything unusual about your environment or deployment 24 | 25 | 26 | ## Contributing via Pull Requests 27 | 28 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 29 | 30 | 1. You are working against the latest source on the *main* branch. 31 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 32 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 33 | 34 | To send us a pull request, please: 35 | 36 | 1. Fork the repository. 37 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 38 | 3. Ensure all build processes execute successfully (see README.md for additional guidance). 39 | 4. Ensure all unit, integration, and/or snapshot tests pass, as applicable. 40 | 5. Commit to your fork using clear commit messages. 41 | 6. Send us a pull request, answering any default questions in the pull request interface. 42 | 7. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 43 | 44 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 45 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 46 | 47 | 48 | ## Finding contributions to work on 49 | 50 | Looking at the existing issues is a great way to find something to contribute on. As our projects, 51 | by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help 52 | wanted/invalid/question/wontfix), looking at any ['help 53 | wanted'](https://github.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/labels/help%20wanted) 54 | issues is a great place to start. 55 | 56 | 57 | ## Code of Conduct 58 | 59 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 60 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 61 | opensource-codeofconduct@amazon.com with any additional questions or comments. 62 | 63 | 64 | ## Security issue notifications 65 | 66 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. 67 | 68 | 69 | ## Licensing 70 | 71 | See the 72 | [LICENSE](https://github.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/blob/main/LICENSE.txt) 73 | file for our project's licensing. We will ask you to confirm the licensing of your contribution. 74 | 75 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 76 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take all security reports seriously. 4 | When we receive such reports, 5 | we will investigate and subsequently address 6 | any potential vulnerabilities as quickly as possible. 7 | If you discover a potential security issue in this project, 8 | please notify AWS/Amazon Security via our 9 | [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) 10 | or directly via email to [AWS Security](mailto:aws-security@amazon.com). 11 | Please do *not* create a public GitHub issue in this project. 12 | -------------------------------------------------------------------------------- /deployment/build-s3-dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 6 | # with the License. A copy of the License is located at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES 11 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions 12 | # and limitations under the License. 13 | # 14 | set -x 15 | # Important: CDK global version number 16 | cdk_version=2.81.0 17 | 18 | # Check to see if the required parameters have been provided: 19 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 20 | echo "Please provide the base source bucket name, trademark approved solution name and version where the lambda code will eventually reside." 21 | echo "For example: ./build-s3-dist.sh solutions trademarked-solution-name v1.0.0" 22 | exit 1 23 | fi 24 | 25 | export DIST_VERSION=$3 26 | export DIST_OUTPUT_BUCKET=$1 27 | export SOLUTION_ID=SO0195 28 | export SOLUTION_NAME=$2 29 | export SOLUTION_TRADEMARKEDNAME=$2 30 | 31 | # Get reference for all important folders 32 | template_dir="$PWD" 33 | staging_dist_dir="$template_dir/staging" 34 | template_dist_dir="$template_dir/global-s3-assets" 35 | build_dist_dir="$template_dir/regional-s3-assets" 36 | source_dir="$template_dir/../source" 37 | demo_website_dir="$template_dir/../source/resources/demo_website" 38 | 39 | 40 | [ "$DEBUG" == 'true' ] && set -x 41 | set -e 42 | 43 | echo "------------------------------------------------------------------------------" 44 | echo "[Init] Remove any old dist files from previous runs" 45 | echo "------------------------------------------------------------------------------" 46 | 47 | echo "rm -rf $template_dist_dir" 48 | rm -rf $template_dist_dir 49 | echo "mkdir -p $template_dist_dir" 50 | mkdir -p $template_dist_dir 51 | echo "rm -rf $build_dist_dir" 52 | rm -rf $build_dist_dir 53 | echo "mkdir -p $build_dist_dir" 54 | mkdir -p $build_dist_dir 55 | echo "rm -rf $staging_dist_dir" 56 | rm -rf $staging_dist_dir 57 | echo "mkdir -p $staging_dist_dir" 58 | mkdir -p $staging_dist_dir 59 | echo "rm -rf $demo_website_dir/dist" 60 | rm -rf $demo_website_dir/dist 61 | 62 | 63 | echo "------------------------------------------------------------------------------" 64 | echo "NPM Install in the source folder" 65 | echo "------------------------------------------------------------------------------" 66 | 67 | # Install the npm install in the source folder 68 | echo "cd $source_dir" 69 | cd $source_dir 70 | echo "npm install" 71 | npm install 72 | 73 | # Install the demo website package 74 | echo "Building and bundling demo website" 75 | cd $demo_website_dir 76 | npm install 77 | npm run build 78 | 79 | echo "cd "$source_dir"" 80 | cd "$source_dir" 81 | 82 | chmod +x ./install_dependencies.sh && ./install_dependencies.sh 83 | 84 | #replace assets_bucket_name 85 | sed -e "s#MY_ASSETS_BUCKET_NAME#$DIST_OUTPUT_BUCKET#g" solution.context.json.template > solution.context.json 86 | 87 | # Run 'cdk synth' to generate raw solution outputs 88 | node_modules/aws-cdk/bin/cdk synth --context solution_version=$DIST_VERSION --asset-metadata false --path-metadata false >$staging_dist_dir/secure-media-delivery-at-the-edge-on-aws.yaml 89 | 90 | 91 | mv cdk.out/* $staging_dist_dir 92 | 93 | #zipping the assets 94 | i=1 95 | cd $staging_dist_dir 96 | echo "Searching for assets..." 97 | for cdk_key in `ls | grep '^asset'`; do 98 | wordtoremove="asset." 99 | item=${cdk_key//$wordtoremove/} 100 | asset_new_name="myasset_$i.zip" 101 | 102 | if [[ $item == *zip ]]; 103 | then 104 | mv $cdk_key $asset_new_name 105 | current_asset_name=$item 106 | else 107 | cd $cdk_key 108 | echo "zipping $cdk_key to $asset_new_name" 109 | zip -qr $asset_new_name . 110 | cd .. 111 | mv $cdk_key/$asset_new_name $asset_new_name 112 | rm -rf $cdk_key 113 | current_asset_name=$item.zip 114 | fi 115 | 116 | sed -i'' -e "s#$current_asset_name#$SOLUTION_NAME/$DIST_VERSION/$asset_new_name#g" $staging_dist_dir/secure-media-delivery-at-the-edge-on-aws.yaml 117 | 118 | let "i+=1" 119 | 120 | done 121 | 122 | 123 | echo "Assets zipped" 124 | 125 | ############ End tweak template ############# 126 | 127 | 128 | # Remove unnecessary output files 129 | echo "cd $staging_dist_dir" 130 | 131 | cd $staging_dist_dir 132 | ls 133 | 134 | echo "------------------------------------------------------------------------------" 135 | echo "[Packing] Template artifacts" 136 | echo "------------------------------------------------------------------------------" 137 | 138 | # Move outputs from staging to template_dist_dir 139 | echo ls $staging_dist_dir/ 140 | ls $staging_dist_dir/ 141 | echo "Move outputs from staging to template_dist_dir" 142 | 143 | echo "cp $template_dir/*.template $template_dist_dir/" 144 | cp $staging_dist_dir/secure-media-delivery-at-the-edge-on-aws.yaml $template_dist_dir/secure-media-delivery-at-the-edge-on-aws.template 145 | 146 | rm secure-media-delivery-at-the-edge-on-aws.yaml 147 | 148 | echo cp $cdk synth --asset-metadata /*.zip $build_dist_dir/ 149 | cp $staging_dist_dir/*.zip $build_dist_dir/ 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /deployment/run-unit-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned 4 | # 5 | # This script should be run from the repo's deployment directory 6 | # cd deployment 7 | # ./run-unit-tests.sh 8 | # 9 | 10 | [ "$DEBUG" == 'true' ] && set -x 11 | set -e 12 | 13 | cd ../source 14 | 15 | echo "Install node dependencies" 16 | npm install 17 | 18 | cp solution.context.json.template solution.context.json 19 | 20 | echo "Install NodeJs ws_secure_media_delivery layer dependencies for AWS Lambda" 21 | npm install --prefix resources/sdk/node/v1/ 22 | 23 | 24 | cd ../deployment 25 | 26 | 27 | prepare_jest_coverage_report() { 28 | local component_name=$1 29 | 30 | if [ ! -d "coverage" ]; then 31 | echo "ValidationError: Missing required directory coverage after running unit tests" 32 | exit 129 33 | fi 34 | 35 | # prepare coverage reports 36 | rm -fr coverage/lcov-report 37 | mkdir -p $coverage_reports_top_path/jest 38 | coverage_report_path=$coverage_reports_top_path/jest/$component_name 39 | rm -fr $coverage_report_path 40 | mv coverage $coverage_report_path 41 | } 42 | 43 | run_javascript_test() { 44 | local component_path=$1 45 | local component_name=$2 46 | 47 | echo "------------------------------------------------------------------------------" 48 | echo "[Test] Run javascript unit test with coverage for $component_name" 49 | echo "------------------------------------------------------------------------------" 50 | #echo "cd $component_path" 51 | #cd $component_path 52 | pwd 53 | echo npm test 54 | # run unit tests 55 | cd ../source 56 | 57 | npm test 58 | 59 | # prepare coverage reports 60 | prepare_jest_coverage_report $component_name 61 | } 62 | 63 | # Get reference for all important folders 64 | template_dir="$PWD" 65 | source_dir="$template_dir/../source" 66 | coverage_reports_top_path=$source_dir/test/coverage-reports 67 | 68 | # Test the attached Lambda function 69 | declare -a my_packages=( 70 | "test" 71 | ) 72 | 73 | for one_package in "${my_packages[@]}" 74 | do 75 | echo "here" 76 | run_javascript_test $source_dir/$one_package $one_package 77 | 78 | # Check the result of the test and exit if a failure is identified 79 | if [ $? -eq 0 ] 80 | then 81 | echo "Test for $one_package passed" 82 | else 83 | echo "******************************************************************************" 84 | echo "Lambda test FAILED for $one_package" 85 | echo "******************************************************************************" 86 | exit 1 87 | fi 88 | done 89 | -------------------------------------------------------------------------------- /solution-manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: SO0195 3 | name: secure-media-delivery-at-the-edge-on-aws 4 | version: v1.2.6 5 | cloudformation_templates: 6 | - template: secure-media-delivery-at-the-edge-on-aws.template 7 | main_template: true 8 | build_environment: 9 | build_image: "aws/codebuild/standard:7.0" 10 | -------------------------------------------------------------------------------- /source/assets/diagrams/architecture-diagram-v_1-0_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/assets/diagrams/architecture-diagram-v_1-0_0.jpg -------------------------------------------------------------------------------- /source/assets/diagrams/architecture-diagram-v_1-0_0_apis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/assets/diagrams/architecture-diagram-v_1-0_0_apis.jpg -------------------------------------------------------------------------------- /source/assets/diagrams/architecture-diagram-v_1-0_0_auto_session.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/assets/diagrams/architecture-diagram-v_1-0_0_auto_session.jpg -------------------------------------------------------------------------------- /source/assets/diagrams/architecture-diagram-v_1-0_0_base.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/assets/diagrams/architecture-diagram-v_1-0_0_base.jpg -------------------------------------------------------------------------------- /source/assets/diagrams/architecture-diagram-v_1-0_0_key_rotation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/assets/diagrams/architecture-diagram-v_1-0_0_key_rotation.jpg -------------------------------------------------------------------------------- /source/bin/secure_media_stream.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | import { App, DefaultStackSynthesizer } from "aws-cdk-lib"; 7 | import { getOpts } from "../helpers/opts"; 8 | 9 | import { SecureMediaStreamingStack, SecureMediaStreamStackProps } from "../lib/secure_media_stream_stack"; 10 | import { AutoSessionRevocationStack, AutoSessionRevocationStackProps } from "../lib/auto_revocation_stack"; 11 | import { IConfiguration } from "../helpers/validators/configuration"; 12 | 13 | const app = new App(); 14 | 15 | const solutionName = 'secure-media-delivery-at-the-edge-on-aws'; 16 | const solutionId = app.node.tryGetContext('solution_id'); 17 | const solutionDisplayName = app.node.tryGetContext('solution_name'); 18 | const solutionVersion = app.node.tryGetContext('solution_version'); 19 | const description = `(${solutionId}) - ${solutionDisplayName}. Version ${solutionVersion}`; 20 | 21 | export const getMainStackProps = (config: IConfiguration): SecureMediaStreamStackProps => { 22 | 23 | const stackSynthesizer = config.main?.assets_bucket_name ? 24 | new DefaultStackSynthesizer( 25 | { fileAssetsBucketName: config.main?.assets_bucket_name + "-${AWS::Region}", 26 | generateBootstrapVersionRule: false 27 | } 28 | ) : new DefaultStackSynthesizer() 29 | 30 | 31 | return { 32 | description, 33 | synthesizer: stackSynthesizer, 34 | }; 35 | }; 36 | 37 | export const getAutoSessionStackProps = (): AutoSessionRevocationStackProps => { 38 | 39 | return { 40 | description 41 | }; 42 | }; 43 | 44 | (async () => { 45 | // The stack configuration. 46 | const config = await getOpts(); 47 | config.solutionId = solutionId; 48 | config.solutionVersion = solutionVersion; 49 | config.solutionName = solutionName; 50 | config.solutionDisplayName = solutionDisplayName; 51 | 52 | const coreStack = new SecureMediaStreamingStack( 53 | app, 54 | config.main.stack_name, 55 | config, 56 | getMainStackProps(config) 57 | 58 | ); 59 | 60 | if (config.sessionRevocation) { 61 | new AutoSessionRevocationStack( // NOSONAR - typescript:S1848 - false positive for cdk code 62 | app, 63 | config.main.stack_name + "AutoSessionRevocation", 64 | config, 65 | coreStack.sessionToRevoke, 66 | getAutoSessionStackProps() 67 | ); 68 | } 69 | })(); 70 | -------------------------------------------------------------------------------- /source/bin/wizard/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | import * as prompts from 'prompts'; 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | 10 | import { IConfiguration } from '../../helpers/validators/configuration'; 11 | import { AutoSessionRevocationModule } from './lib/auto-session-revocation-module'; 12 | import { PromptComponent } from './lib/prompt-component'; 13 | import { onCancel } from './lib/handlers'; 14 | import { MainModule } from './lib/main-module'; 15 | import { ApiModule } from './lib/api-module'; 16 | 17 | /** 18 | * A question prompting for the components to deploy to the sandbox account. 19 | */ 20 | const componentQuestion = { 21 | type: 'multiselect', 22 | name: 'value', 23 | message: 'Which optional module would you like to deploy ?', 24 | min: 0, 25 | instructions: false, 26 | hint: '- Space to select. Return to submit. \'a\' to toggle all.', 27 | choices: [ 28 | { title: '[API]', 'value': 'b' }, 29 | { title: '[AUTO SESSION REVOCATION]', 'value': 'c' } 30 | ] 31 | }; 32 | 33 | /** 34 | * Prompts the user whether the configuration is valid 35 | * and should be written. 36 | */ 37 | const confirmConfigurationQuestion = { 38 | type: 'confirm', 39 | name: 'value', 40 | message: 'Please check your choices before saving the current configuration. Would you like to use it ?' 41 | }; 42 | 43 | /** 44 | * A map between component identifiers and their instance. 45 | */ 46 | const moduleMap: { [key: string]: PromptComponent } = { 47 | 'a': new MainModule(), 48 | 'b': new ApiModule(), 49 | 'c': new AutoSessionRevocationModule(), 50 | }; 51 | 52 | 53 | /** 54 | * Prompts the user for different information and 55 | * returns the gathered configuration. 56 | */ 57 | const getConfiguration = async (): Promise => { 58 | const configuration: IConfiguration = { 59 | "main": { 60 | "stack_name": "MYSTREAM", 61 | "rotate_secrets_frequency": "1m", 62 | "rotate_secrets_pattern": "P", 63 | "wcu": "100", 64 | "retention": "5", 65 | "metrics": true 66 | } 67 | }; 68 | 69 | const mainComponent = new Array('a'); 70 | 71 | const components: Array = (await prompts.prompt(componentQuestion, { onCancel })).value; 72 | const allComponents = mainComponent.concat(components); 73 | 74 | // Iterating over the component prompts. 75 | for (const item of allComponents) { 76 | const moduleImpl = moduleMap[item]; 77 | 78 | if (moduleImpl) { 79 | try { 80 | await moduleImpl.prompt(configuration); 81 | } catch (e) { 82 | console.log((e as Error).message); 83 | process.exit(0); 84 | } 85 | } 86 | } 87 | configuration.main.metrics = true; 88 | return (configuration); 89 | }; 90 | 91 | (async () => { 92 | const configuration = await getConfiguration(); 93 | 94 | // The pretty-printed version of the configuration. 95 | const data = JSON.stringify(configuration, null, 2); 96 | 97 | 98 | console.log("\n--------------------- Summary -------------------\n") 99 | // Prompting the user to confirm. 100 | const confirmation = await prompts.prompt(confirmConfigurationQuestion); 101 | 102 | if (!confirmation.value) { 103 | console.log(`The configuration has been rejected, exiting.`); 104 | process.exit(0); 105 | } 106 | 107 | // The path to the configuration file. 108 | const filePath = path.resolve(__dirname, '..', '..', '..', 'solution.context.json'); 109 | 110 | // Writing the configuration. 111 | fs.writeFileSync(filePath, data); 112 | console.log(`\nThe configuration has been successfully written to ${filePath}.\nYou can now deploy the solution by running :\n\nnpx cdk deploy`); 113 | })(); -------------------------------------------------------------------------------- /source/bin/wizard/lib/api-module.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as prompts from "prompts"; 5 | import { PromptComponent } from "./prompt-component"; 6 | import { onCancel } from "./handlers"; 7 | import { IConfiguration } from "../../../helpers/validators/configuration"; 8 | import { IApi } from "../../../helpers/validators/api"; 9 | import { IHosting } from "../../../helpers/validators/hosting"; 10 | 11 | /** 12 | * A question prompting the user for the session invalidation 13 | * to allocate to a prototype. 14 | */ 15 | const apiQuestions = [ 16 | { 17 | type: "toggle", 18 | name: "demo", 19 | message: "[API] --> Do you want to deploy a demo website?", 20 | choices: [ 21 | { title: "Yes", value: true }, 22 | { title: "Python", value: false }, 23 | ], 24 | initial: 1, 25 | }, 26 | ]; 27 | 28 | const selectAssetHosting = [ 29 | { 30 | type: "toggle", 31 | name: "hosting", 32 | message: 33 | "[API] --> Do you want to configure your existing hosting used for asset delivery?", 34 | initial: true, 35 | active: "yes", 36 | inactive: "no", 37 | }, 38 | ]; 39 | 40 | const selectVideoStreamType = [ 41 | { 42 | type: "multiselect", 43 | name: "value", 44 | message: "[API] --> Which video stream type would you like to configure?", 45 | min: 1, 46 | instructions: false, 47 | hint: "- Space to select. Return to submit. 'a' to toggle all.", 48 | choices: [ 49 | { title: "HLS", value: "hls" }, 50 | { title: "DASH", value: "dash" }, 51 | ], 52 | }, 53 | ]; 54 | 55 | function hostQuestions(type: string) { 56 | 57 | return [ 58 | { 59 | type: "text", 60 | name: "hostname", 61 | message: "[API][" + type + "] --> Domain name used for asset delivery (http:// or https://)", 62 | validate: (value: string) => 63 | !value.startsWith('https://') && !value.startsWith('http://') 64 | ? "Hostname is mandatory and must start with http:// or https://" 65 | : true, 66 | }, 67 | { 68 | type: "text", 69 | name: "url_path", 70 | message: "[API][" + type + "] --> URL path for existing playable asset", 71 | validate: (value: string) => 72 | !value.startsWith('/') ? "URL path is mandatory and must start with /" : true, 73 | 74 | }, 75 | { 76 | type: "select", 77 | name: "ttl", 78 | message: "[API][" + type + "] --> TTL for the token", 79 | choices: [ 80 | { title: "30 minutes", value: "+30m" }, 81 | { title: "One hour", value: "+1h" }, 82 | { title: "3 hours", value: "+3h" }, 83 | { title: "6 hours", value: "+6h" }, 84 | { title: "24 hours", value: "+24h" }, 85 | ], 86 | initial: 1, 87 | }, 88 | ]; 89 | } 90 | 91 | export class ApiModule implements PromptComponent { 92 | /** 93 | * Implements the logic to prompt questions to the user 94 | * and to fill the given configuration with the provided responses. 95 | * @param configuration an object in which the configuration must be stored. 96 | */ 97 | async prompt(configuration: IConfiguration): Promise { 98 | console.log("\n--------------------- API Module -------------------\n"); 99 | configuration.api = await prompts.prompt(apiQuestions, { onCancel }); 100 | 101 | const configureHosting = await prompts.prompt(selectAssetHosting, { 102 | onCancel, 103 | }); 104 | 105 | if (configureHosting.hosting) { 106 | const streamType = await prompts.prompt(selectVideoStreamType, { 107 | onCancel, 108 | }); 109 | 110 | if (streamType.value.includes("hls")) { 111 | configuration.hls = ( 112 | await prompts.prompt(hostQuestions("HLS"), { onCancel }) 113 | ); 114 | } 115 | 116 | if (streamType.value.includes("dash")) { 117 | configuration.dash = ( 118 | await prompts.prompt(hostQuestions("DASH"), { onCancel }) 119 | ); 120 | } 121 | } 122 | 123 | return configuration; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /source/bin/wizard/lib/handlers.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /** 5 | * The default handler that is called when the user cancels 6 | * a prompting question (e.g Ctrl+C). The default behavior is to exit the 7 | * wizard. 8 | */ 9 | export const onCancel = (): void => process.exit(1); -------------------------------------------------------------------------------- /source/bin/wizard/lib/main-module.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as Joi from "joi"; 5 | import * as prompts from "prompts"; 6 | 7 | import { PromptComponent } from "./prompt-component"; 8 | import { onCancel } from "./handlers"; 9 | import { IConfiguration } from "../../../helpers/validators/configuration"; 10 | import { IMain } from "../../../helpers/validators/main"; 11 | 12 | const coreQuestions = [ 13 | { 14 | type: "text", 15 | name: "stack_name", 16 | message: "[Base module] --> Stack name", 17 | validate: (value: string) => 18 | !/^[A-Za-z][A-Za-z0-9-]*$/.test(value) 19 | ? "The name of the stack is mandatory and can include letters (A-Z and a-z), numbers (0-9), and dashes (-)." 20 | : true, 21 | }, 22 | { 23 | type: "text", 24 | name: "wcu", 25 | message: 26 | "[Base module] --> Set the capacity limit expressed in WCUs for WAF Rule Group to keep the session list that should be blocked (between 2 and 1500)", 27 | validate: (value: string) => 28 | Joi.number().min(2).required().validate(value).error || Joi.number().max(1500).required().validate(value).error 29 | ? "Capacity is mandatory and must be a number between 2 and 1500" 30 | : true, 31 | }, 32 | { 33 | type: "text", 34 | name: "retention", 35 | message: 36 | "[Base module] --> Set the retention time for compromised sessions (in minutes)", 37 | validate: (value: number) => 38 | Joi.number().min(1).required().validate(value).error 39 | ? "Retention is mandatory and must be a number higher than 1" 40 | : true, 41 | }, 42 | 43 | { 44 | type: "select", 45 | name: "rotate_secrets_frequency", 46 | message: 47 | "[Base module] --> At what frequency do you want to rotate the secrets?", 48 | choices: [ 49 | { title: "Manual", value: "m" }, 50 | { title: "Every day", value: "24h" }, 51 | { title: "Every week", value: "1w" }, 52 | { title: "Monthly", value: "1m" }, 53 | ], 54 | initial: 1, 55 | }, 56 | ]; 57 | const rotation_day_of_the_week_question = [ 58 | { 59 | type: "select", 60 | name: "value", 61 | message: 62 | "[Base module] --> On which day of the week you would like to trigger it", 63 | choices: [ 64 | { title: "Monday", value: "2" }, 65 | { title: "Tuesday", value: "3" }, 66 | { title: "Wednesday", value: "4" }, 67 | { title: "Thursday", value: "5" }, 68 | { title: "Friday", value: "6" }, 69 | { title: "Saturday", value: "7" }, 70 | { title: "Sunday", value: "1" }, 71 | ], 72 | initial: 1, 73 | }, 74 | ]; 75 | 76 | const rotation_week_of_month_question = [ 77 | { 78 | type: "select", 79 | name: "value", 80 | message: 81 | "[Base module] --> On which week of the month you would like to trigger it", 82 | choices: [ 83 | { title: "Week 1", value: "1" }, 84 | { title: "Week 2", value: "2" }, 85 | { title: "Week 3", value: "3" }, 86 | { title: "Week 4", value: "4" }, 87 | ], 88 | initial: 1, 89 | }, 90 | ]; 91 | 92 | const rotation_datetime_question = [ 93 | { 94 | type: "text", 95 | name: "value", 96 | message: 97 | "[Base module] --> At what time of the day should take place (use the format HH:mm, events use UTC+0 time zone) ", 98 | validate: (value: string) => 99 | Joi.string() 100 | .regex(/^(0\d|1\d|2[0-3]):[0-5]\d$/) 101 | .validate(value).error 102 | ? "The expected format is HH:mm" 103 | : true, 104 | }, 105 | ]; 106 | 107 | export class MainModule implements PromptComponent { 108 | /** 109 | * Implements the logic to prompt questions to the user 110 | * and to fill the given configuration with the provided responses. 111 | * @param configuration an object in which the configuration must be stored. 112 | */ 113 | async prompt(configuration: IConfiguration): Promise { 114 | console.log( 115 | "\n--------------------- Base configuration -------------------\n" 116 | ); 117 | configuration.main = ( 118 | await prompts.prompt(coreQuestions, { onCancel }) 119 | ); 120 | 121 | if (configuration.main.rotate_secrets_frequency !== "m") { 122 | //Minutes Hours Day_of_month Month Day_of_week Year 123 | //MIN HOUR * * DAY * 124 | let day_of_the_week = "*"; 125 | const day_of_the_month = "?"; 126 | 127 | if (configuration.main.rotate_secrets_frequency === "1w") { 128 | const day = await prompts.prompt(rotation_day_of_the_week_question, { 129 | onCancel, 130 | }); 131 | day_of_the_week = day.value; 132 | } else if (configuration.main.rotate_secrets_frequency === "1m") { 133 | //1m 134 | const answer_day_week = await prompts.prompt( 135 | rotation_day_of_the_week_question, 136 | { onCancel } 137 | ); 138 | const answer_week_month = await prompts.prompt( 139 | rotation_week_of_month_question, 140 | { onCancel } 141 | ); 142 | 143 | day_of_the_week = answer_day_week.value + "#" + answer_week_month.value; 144 | } 145 | 146 | const answer_datetime = await prompts.prompt(rotation_datetime_question, { 147 | onCancel, 148 | }); 149 | const datetime = answer_datetime.value.split(":"); 150 | configuration.main.rotate_secrets_pattern = 151 | datetime[1] + 152 | " " + 153 | datetime[0] + 154 | " " + 155 | day_of_the_month + 156 | " * " + 157 | day_of_the_week + 158 | " *"; 159 | } 160 | 161 | return configuration; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /source/bin/wizard/lib/prompt-component.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { IConfiguration } from "../../../helpers/validators/configuration"; 5 | 6 | export interface PromptComponent { 7 | 8 | /** 9 | * Implements the logic to prompt questions to the user 10 | * and to fill the given configuration with the provided responses. 11 | * @param configuration an object in which the configuration must be stored. 12 | */ 13 | prompt(configuration: IConfiguration) : Promise; 14 | } -------------------------------------------------------------------------------- /source/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "acknowledged-issue-numbers": [ 3 | 19179 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /source/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/secure_media_stream.ts", 3 | "context": { 4 | "solution_id": "SO0195", 5 | "solution_name": "Secure Media Delivery at the Edge on AWS", 6 | "solution_version": "v1.2.6" 7 | } 8 | } -------------------------------------------------------------------------------- /source/helpers/opts.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { schema, IConfiguration } from './validators/configuration'; 5 | 6 | /** 7 | * A reference to the configuration file. 8 | */ 9 | let config = null; 10 | 11 | /** 12 | * This function will validate the configuration read from the CDK context 13 | * file, and will pass the resulted value to the caller. 14 | * @throws an exception if the configuration is not valid. 15 | */ 16 | export const getOpts = async (): Promise => { 17 | 18 | try { 19 | config = require('../solution.context.json'); 20 | } catch (e) { 21 | 22 | console.error(e); 23 | console.error(` 24 | The 'solution.context.json' configuration file could not be found. 25 | Please run 'npm run wizard' to generate a configuration before deploying. 26 | `); 27 | process.exit(1); 28 | } 29 | 30 | // Validating the project configuration. 31 | const result = schema.validate(config); 32 | 33 | // Verifying whether the configuration is valid. 34 | if (result.error) { 35 | throw new Error(result.error.message); 36 | } 37 | // Returning validated options. 38 | return (result.value); 39 | }; 40 | -------------------------------------------------------------------------------- /source/helpers/validators/api.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as Joi from 'joi'; 5 | 6 | /** 7 | * A description of the Api configuration 8 | * in Typescript. 9 | */ 10 | export interface IApi { 11 | 12 | /** 13 | * The limit (in dollars) at which a notification is 14 | * to be sent when the actual budget is superior 15 | * to the limit value. 16 | */ 17 | demo: boolean; 18 | } 19 | 20 | /** 21 | * The `Joi` schema for validating the api configuration. 22 | */ 23 | export const apiSchema = Joi.object().keys({ 24 | demo: Joi.boolean().required(), 25 | 26 | }); -------------------------------------------------------------------------------- /source/helpers/validators/auto_session_revocation.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as Joi from 'joi'; 5 | 6 | /** 7 | * A description of the Session Invalidation configuration 8 | * in Typescript. 9 | */ 10 | export interface ISessionRevocation { 11 | 12 | /** 13 | * The limit (in dollars) at which a notification is 14 | * to be sent when the actual budget is superior 15 | * to the limit value. 16 | */ 17 | trigger_workflow_frequency: number ; 18 | db_name: string; 19 | table_name: string; 20 | request_ip_column: string; 21 | ua_column_name: string; 22 | referer_column_name: string; 23 | uri_column_name: string; 24 | status_column_name: string; 25 | response_bytes_column_name: string; 26 | date_column_name: string; 27 | time_column_name: string; 28 | lookback_period: number; 29 | ip_penalty: number; 30 | ip_rate: number; 31 | referer_penalty: number; 32 | ua_penalty: number; 33 | min_sessions_number: number; 34 | min_session_duration: number; 35 | score_threshold: number; 36 | partitioned: number; 37 | } 38 | 39 | /** 40 | * The `Joi` schema for validating the session invalidation configuration. 41 | */ 42 | export const sessionRevocationSchema = Joi.object().keys({ 43 | trigger_workflow_frequency: Joi.number().required(), 44 | db_name: Joi.string().required(), 45 | table_name: Joi.string().required(), 46 | request_ip_column: Joi.string().required(), 47 | ua_column_name: Joi.string().required(), 48 | referer_column_name: Joi.string().required(), 49 | uri_column_name: Joi.string().required(), 50 | status_column_name: Joi.string().required(), 51 | response_bytes_column_name: Joi.string().required(), 52 | date_column_name: Joi.string().required(), 53 | time_column_name: Joi.string().required(), 54 | lookback_period: Joi.number().required(), 55 | ip_penalty: Joi.number().required(), 56 | ip_rate: Joi.number().required(), 57 | referer_penalty: Joi.number().required(), 58 | ua_penalty: Joi.number().required(), 59 | min_sessions_number: Joi.number().required(), 60 | min_session_duration: Joi.number().required(), 61 | score_threshold: Joi.number().required(), 62 | partitioned: Joi.number().required(), 63 | 64 | }); -------------------------------------------------------------------------------- /source/helpers/validators/configuration.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as Joi from 'joi'; 5 | import { apiSchema, IApi } from './api'; 6 | import { coreSchema, IMain } from './main'; 7 | import { hostingSchema, IHosting } from './hosting'; 8 | 9 | 10 | import { sessionRevocationSchema, ISessionRevocation } from './auto_session_revocation'; 11 | 12 | /** 13 | * Describes a configuration associated with the 14 | * current stack in Typescript. 15 | */ 16 | export interface IConfiguration { 17 | 18 | main: IMain; 19 | sessionRevocation?: ISessionRevocation; 20 | api?: IApi; 21 | hls?: IHosting; 22 | dash?: IHosting; 23 | solutionId?: string; 24 | solutionVersion?: string; 25 | solutionName?: string; 26 | solutionDisplayName?: string; 27 | } 28 | 29 | /** 30 | * The `Joi` schema for validating the configuration. 31 | */ 32 | export const schema = Joi.object().keys({ 33 | main: coreSchema.required(), 34 | sessionRevocation: sessionRevocationSchema.optional(), 35 | api: apiSchema.optional(), 36 | hosting: hostingSchema.optional(), 37 | 38 | }).unknown().required(); 39 | 40 | -------------------------------------------------------------------------------- /source/helpers/validators/hosting.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as Joi from 'joi'; 5 | 6 | 7 | export interface IHosting { 8 | 9 | hostname: string; 10 | url_path: string; 11 | ttl: string; 12 | 13 | } 14 | 15 | /** 16 | * The `Joi` schema for validating the api configuration. 17 | */ 18 | export const hostingSchema = Joi.object().keys({ 19 | url_path: Joi.string().required(), 20 | ttl: Joi.string().required(), 21 | hostname: Joi.string().required(), 22 | }); -------------------------------------------------------------------------------- /source/helpers/validators/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as Joi from 'joi'; 5 | 6 | /** 7 | * A description of the Core configuration 8 | * in Typescript. 9 | */ 10 | export interface IMain { 11 | 12 | /** 13 | * The limit (in dollars) at which a notification is 14 | * to be sent when the actual budget is superior 15 | * to the limit value. 16 | */ 17 | stack_name: string; 18 | assets_bucket_name?: string; 19 | rotate_secrets_frequency: string; 20 | rotate_secrets_pattern: string; 21 | wcu: string; 22 | retention: string; 23 | metrics: boolean; 24 | 25 | } 26 | 27 | /** 28 | * The `Joi` schema for validating the core configuration. 29 | */ 30 | export const coreSchema = Joi.object().keys({ 31 | stack_name:Joi.string().required(), 32 | assets_bucket_name:Joi.string().optional(), 33 | rotate_secrets_frequency: Joi.string().required(), 34 | rotate_secrets_pattern: Joi.string().optional(), 35 | wcu:Joi.string().required(), 36 | retention:Joi.string().required(), 37 | metrics: Joi.boolean().optional(), 38 | }); -------------------------------------------------------------------------------- /source/install_dependencies.ps1: -------------------------------------------------------------------------------- 1 | 2 | Write-Output "Install node dependencies" 3 | npm install 4 | 5 | Write-Output "Install NodeJs ws_secure_media_delivery layer dependencie for AWS Lambda" 6 | Set-Location .\lambda\layers\aws_secure_media_delivery_nodejs\nodejs 7 | npm install 8 | Set-Location ..\..\..\..\ 9 | 10 | Write-Output "Install NodeJs AdmZip layer dependencies for AWS Lambda" 11 | Set-Location .\lambda\layers\admzip\nodejs 12 | npm install 13 | Set-Location ..\..\..\..\ -------------------------------------------------------------------------------- /source/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | echo "Install node dependencies" 4 | npm install 5 | 6 | echo "Install NodeJs ws_secure_media_delivery layer dependencies for AWS Lambda" 7 | npm install --prefix lambda/layers/aws_secure_media_delivery_nodejs/nodejs 8 | 9 | echo "Install NodeJs AdmZip layer dependencies for AWS Lambda" 10 | npm install --prefix lambda/layers/admzip/nodejs 11 | 12 | -------------------------------------------------------------------------------- /source/jest.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module.exports = { 5 | "modulePaths": [ 6 | "/resources/sdk/node/v1/" 7 | ], 8 | "roots": [ 9 | "/test" 10 | ], 11 | testMatch: [ '**/*.test.ts'], 12 | "transform": { 13 | "^.+\\.tsx?$": "ts-jest" 14 | }, 15 | coverageReporters: [ 16 | "text", 17 | ["lcov", {"projectRoot": "../"}] 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /source/lambda/custom_resource_us_east_1/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | let fs = require("fs"); 5 | let path = require("path"); 6 | let AdmZip = require("adm-zip"); 7 | const os = require('os'); 8 | 9 | const { Lambda } = require("@aws-sdk/client-lambda"); 10 | const { SSM } = require("@aws-sdk/client-ssm"); 11 | const { WAFV2 } = require("@aws-sdk/client-wafv2"); 12 | const lambda = process.env.METRICS == "true" ? new Lambda({ customUserAgent: process.env.SOLUTION_IDENTIFIER, region: 'us-east-1' }) : new Lambda({ region: 'us-east-1' }); 13 | const ssm = process.env.METRICS == "true" ? new SSM({ customUserAgent: process.env.SOLUTION_IDENTIFIER }) : new SSM(); 14 | const wafv2 = process.env.METRICS == "true" ? new WAFV2({ customUserAgent: process.env.SOLUTION_IDENTIFIER, region: 'us-east-1' }) : new WAFV2({ region: 'us-east-1' }); 15 | 16 | 17 | exports.handler = async (event, context) => { 18 | 19 | console.log("Event=" + JSON.stringify(event)); 20 | 21 | await createWafRuleGroup(); 22 | 23 | if (parseInt(process.env.DEPLOY_LE) == 1) { 24 | //deploy Lambda Edge only if the user selected API module in the wizard 25 | await createLambdaEdge(); 26 | } 27 | 28 | } 29 | 30 | async function createLambdaEdge() { 31 | let functionArn = ''; 32 | const le_zip_path = path.join(os.tmpdir(), 'lambda_edge.zip'); 33 | const le_path = "./le.js"; 34 | const tmp_le_path = path.join(os.tmpdir(), 'le.js'); 35 | const code_path = path.resolve(le_zip_path) 36 | let zip = new AdmZip(); 37 | try { 38 | //zipping le.js 39 | console.log("copy " + le_path + " to " + tmp_le_path); 40 | fs.copyFileSync(le_path, tmp_le_path); 41 | 42 | console.log("zipping " + tmp_le_path + " into " + le_zip_path) 43 | zip.addLocalFile(tmp_le_path); 44 | zip.writeZip(le_zip_path); 45 | console.log("zip created"); 46 | // Creates Edge Lambda 47 | const params = { 48 | Code: { 49 | ZipFile: fs.readFileSync(code_path) 50 | }, 51 | FunctionName: process.env.STACK_NAME + '_Sig4LE', /* required */ 52 | Handler: 'le.handler', /* required */ 53 | Role: process.env.ROLE_ARN, /* required */ 54 | Runtime: 'nodejs18.x', /* required */ 55 | Description: 'Sign sign4 requests' 56 | }; 57 | 58 | let result = await lambda.createFunction(params); 59 | functionArn = result.FunctionArn; 60 | await publishLEVersion(functionArn); 61 | } catch (error) { 62 | if (error.name === "ResourceConflictException") { 63 | console.log("LambdaEdge exist already. Nothing to do.") 64 | } else { 65 | console.error(error); 66 | throw new Error('Creating Edge Lambda failed.'); 67 | } 68 | 69 | } 70 | 71 | 72 | } 73 | 74 | async function publishLEVersion(functionArn) { 75 | // Publishes Edge Lambda version 76 | try { 77 | let isFunctionStateActive = false 78 | let retry = 0 79 | let delayinMilliseconds = 5000; 80 | while (!isFunctionStateActive) { 81 | let response = await lambda.getFunctionConfiguration({ 82 | FunctionName: functionArn 83 | }); 84 | console.log(`Response from get function configuration ${JSON.stringify(response)}`) 85 | if (response.State === 'Active' || retry > 10) { 86 | isFunctionStateActive = true 87 | } else { 88 | await waitForTime(delayinMilliseconds) 89 | retry++ 90 | delayinMilliseconds += 5000; 91 | } 92 | } 93 | 94 | let params = { 95 | FunctionName: functionArn 96 | }; 97 | 98 | let result = await lambda.publishVersion(params); 99 | await saveToSSM(process.env.LAMBDA_VERSION, `${functionArn}:${result.Version}`) 100 | 101 | } catch (error) { 102 | console.error(error); 103 | throw Error('Publishing Edge Lambda version failed.'); 104 | } 105 | } 106 | 107 | async function createWafRuleGroup() { 108 | try { 109 | // Creates WAF Rule Group 110 | const params = { 111 | Capacity: parseInt(process.env.WCU), 112 | Name: process.env.RULE_NAME, 113 | Scope: 'CLOUDFRONT', 114 | VisibilityConfig: { 115 | CloudWatchMetricsEnabled: false, 116 | MetricName: "metricName", 117 | SampledRequestsEnabled: false, 118 | }, 119 | Description: "Revoked sessions", 120 | Rules: [], 121 | }; 122 | 123 | let result = await wafv2.createRuleGroup(params); 124 | await saveToSSM(process.env.RULE_ID, result.Summary.Id) 125 | 126 | } catch (error) { 127 | if (error.name === "WAFDuplicateItemException") { 128 | console.log("The rule group exist already. Nothing to do.") 129 | } else { 130 | console.error(error); 131 | throw Error('Creating WAF Rule group failed.'); 132 | } 133 | } 134 | } 135 | 136 | async function saveToSSM(paramName, paramValue) { 137 | console.log('Saving to SSM...'); 138 | 139 | const params = { 140 | Name: paramName, 141 | Value: paramValue, 142 | Type: 'String', 143 | Overwrite: true 144 | }; 145 | const request = await ssm.putParameter(params); 146 | return request.Parameter; 147 | } 148 | 149 | // Function to add delay for waiting on process 150 | const waitForTime = async (ms) => { 151 | return new Promise(resolve => setTimeout(resolve, ms)); 152 | }; -------------------------------------------------------------------------------- /source/lambda/custom_resource_us_east_1/le.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Declare constants required for the signature process 5 | const crypto = require('crypto'); 6 | const qs = require('querystring'); 7 | const emptyHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; 8 | // CloudFront includes the x-amz-cf-id header in the signature for custom origins 9 | const signedHeadersCustomOrigin = 'host;x-amz-cf-id;x-amz-content-sha256;x-amz-date;x-amz-security-token'; 10 | // Retrieve the temporary IAM credentials of the function that were granted by 11 | // the Lambda@Edge service based on the function permissions. 12 | //const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN } = process.env; 13 | 14 | 15 | // Since the function is configured to be executed on origin request events, the handler 16 | // is executed every time CloudFront needs to go back to the origin, which is S3 here. 17 | exports.handler = async event => { 18 | console.log("event=" + JSON.stringify(event)) 19 | // Retrieve the original request that CloudFront was going to send to S3 20 | const request = event.Records[0].cf.request; 21 | 22 | // The request object has different properties depending on the type of 23 | // origin that is being used. Account for that here. 24 | let originType = ''; 25 | if (request.origin.hasOwnProperty('custom')) 26 | originType = 'custom'; 27 | else 28 | throw new Error("Unexpected origin type. Expected 'custom'. Got: " + JSON.stringify(request.origin)); 29 | 30 | // Create a JSON object with the fields that should be included in the Sigv4 request, 31 | // including the X-Amz-Cf-Id header that CloudFront adds to every request forwarded 32 | // upstream. This header is exposed to Lambda@Edge in the event object 33 | const sigv4Options = { 34 | method: request.method, 35 | path: request.origin[originType].path + request.uri, 36 | query: request.querystring, 37 | credentials: { 38 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 39 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 40 | sessionToken: process.env.AWS_SESSION_TOKEN 41 | }, 42 | host: request.headers['host'][0].value, 43 | xAmzCfId: event.Records[0].cf.config.requestId, 44 | originType: originType 45 | }; 46 | 47 | console.log(sigv4Options) 48 | 49 | // Compute the signature object that includes the following headers: X-Amz-Security-Token, Authorization, 50 | // X-Amz-Date, X-Amz-Content-Sha256, and X-Amz-Security-Token 51 | const signature = signV4(sigv4Options); 52 | 53 | // Finally, add the signature headers to the request 54 | console.log("signature="+JSON.stringify(signature)); 55 | 56 | for(const header in signature){ 57 | request.headers[header.toLowerCase()] = [{ 58 | key: header, 59 | value: signature[header].toString() 60 | }]; 61 | } 62 | return request; 63 | }; 64 | 65 | 66 | // Helper functions to sign the request using AWS Signature Version 4 67 | function signV4(options) { 68 | // Infer the region from the host header 69 | // Create the canonical request 70 | const region = options.host.split('.')[2]; 71 | const date = (new Date()).toISOString().replace(/[:-]|\.\d{3}/g, ''); 72 | let canonicalHeaders = ''; 73 | let signedHeaders = ''; 74 | canonicalHeaders = ['host:'+options.host, 'x-amz-cf-id:'+options.xAmzCfId, 'x-amz-content-sha256:'+emptyHash, 'x-amz-date:'+date, 'x-amz-security-token:'+options.credentials.sessionToken].join('\n'); 75 | signedHeaders = signedHeadersCustomOrigin; 76 | const requestParameters = createCanonicalQS(options.query); 77 | 78 | const canonicalURI = encodeRfc3986(encodeURIComponent(decodeURIComponent(options.path).replace(/\+/g, ' ')).replace(/%2F/g, '/')); 79 | const canonicalRequest = [options.method, canonicalURI, requestParameters, canonicalHeaders + '\n', signedHeaders,emptyHash].join('\n'); 80 | // Create string to sign 81 | const credentialScope = [date.slice(0, 8), region, 'execute-api/aws4_request'].join('/'); 82 | const stringToSign = ['AWS4-HMAC-SHA256', date, credentialScope, hash(canonicalRequest, 'hex')].join('\n'); 83 | // Calculate the signature 84 | const signature = hmac(hmac(hmac(hmac(hmac('AWS4' + options.credentials.secretAccessKey, date.slice(0, 8)), region), "execute-api"), 'aws4_request'), stringToSign, 'hex'); 85 | // Form the authorization header 86 | const authorizationHeader = ['AWS4-HMAC-SHA256 Credential=' + options.credentials.accessKeyId + '/' + credentialScope,'SignedHeaders=' + signedHeaders,'Signature=' + signature].join(', '); 87 | 88 | // return required headers for Sigv4 to be added to the request 89 | return { 90 | 'Authorization': authorizationHeader, 91 | 'X-Amz-Content-Sha256' : emptyHash, 92 | 'X-Amz-Date': date, 93 | 'X-Amz-Security-Token': options.credentials.sessionToken 94 | }; 95 | } 96 | 97 | function createCanonicalQS(input_qs){ 98 | let canonicalQS=''; 99 | let qsparsed = qs.parse(input_qs); 100 | Object.keys(qsparsed).sort().forEach((param)=>{ 101 | canonicalQS += encodeQS(param)+'='+encodeQS(qsparsed[param])+'&' 102 | }); 103 | canonicalQS = canonicalQS.slice(0, -1); 104 | 105 | return canonicalQS; 106 | } 107 | 108 | function encodeQS(input_str){ 109 | return input_str.replace(/[!'()*=]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) 110 | } 111 | 112 | function encodeRfc3986(urlEncodedStr) { 113 | return urlEncodedStr.replace(/[!'()*]/g, c => '%25' + c.charCodeAt(0).toString(16).toUpperCase()) 114 | } 115 | 116 | function hash(string, encoding) { 117 | return crypto.createHash('sha256').update(string, 'utf8').digest(encoding) 118 | } 119 | 120 | function hmac(key, string, encoding) { 121 | return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding) 122 | } -------------------------------------------------------------------------------- /source/lambda/export_params/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | const { Lambda } = require("@aws-sdk/client-lambda"); 4 | const lambda = new Lambda(); 5 | 6 | 7 | exports.handler = async (event, context) => { 8 | console.log("event="+JSON.stringify(event)); 9 | 10 | for (const record of event.Records) { 11 | 12 | console.log('Stream record: ', JSON.stringify(record)); 13 | 14 | const db_item = record.dynamodb.NewImage; 15 | console.log("db_item="+JSON.stringify(db_item)); 16 | console.log("db_item['score_threshold']="+db_item['score_threshold']); 17 | console.log("db_item['score_threshold']['N']="+db_item['score_threshold']['N']); 18 | if(! parseFloat(db_item['score_threshold']['N'])>1) 19 | throw new Error('Score_threshold is lower than 1'); 20 | 21 | 22 | const params = { 23 | FunctionName: process.env.SUBMIT_QUERY_FUNCTION, 24 | Environment: { 25 | 'Variables': { 26 | 'ip_penalty': db_item['ip_penalty']['N'], 27 | 'referer_penalty': db_item['referer_penalty']['N'], 28 | 'ua_penalty': db_item['ua_penalty']['N'], 29 | 'ip_rate': db_item['ip_rate']['N'], 30 | 'uri_column_name': db_item['uri_column_name']['S'], 31 | 'referer_column_name': db_item['referer_column_name']['S'], 32 | 'ua_column_name': db_item['ua_column_name']['S'], 33 | 'request_ip_column': db_item['request_ip_column']['S'], 34 | 'status_column_name': db_item['status_column_name']['S'], 35 | 'response_bytes_column_name': db_item['response_bytes_column_name']['S'], 36 | 'date_column_name': db_item['date_column_name']['S'], 37 | 'time_column_name': db_item['time_column_name']['S'], 38 | 'db_name': db_item['db_name']['S'], 39 | 'table_name': db_item['table_name']['S'], 40 | 'min_sessions_number': db_item['min_sessions_number']['N'], 41 | 'min_session_duration': db_item['min_session_duration']['N'], 42 | 'score_threshold': db_item['score_threshold']['N'], 43 | 'partitioned': db_item['partitioned']['N'], 44 | 'lookback_period': db_item['lookback_period']['N'] 45 | } 46 | } 47 | }; 48 | console.log("params="+JSON.stringify(params)); 49 | const result = await lambda.updateFunctionConfiguration(params); 50 | console.log(result); 51 | console.log(`Lambda function ${process.env.SUBMIT_QUERY_FUNCTION} configuration updated`); 52 | 53 | 54 | } 55 | 56 | return "OK"; 57 | 58 | 59 | }; -------------------------------------------------------------------------------- /source/lambda/generate_secret_update_cff/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | const { CloudFront } = require("@aws-sdk/client-cloudfront"); 4 | const { SecretsManager } = require("@aws-sdk/client-secrets-manager"); 5 | 6 | 7 | const secretsmanager = process.env.METRICS == "true" ? new SecretsManager({customUserAgent: process.env.SOLUTION_IDENTIFIER}) : new SecretsManager(); 8 | const cloudfront = process.env.METRICS == "true" ? new CloudFront({customUserAgent: process.env.SOLUTION_IDENTIFIER}) : new CloudFront(); 9 | 10 | 11 | const crypto = require("crypto"); 12 | const fs = require('fs'); 13 | 14 | 15 | 16 | function generateSecretKey() { 17 | 18 | const randomKeySuffix = crypto.randomBytes(10).toString('hex'); 19 | const dateObj = new Date(); 20 | const month = String(dateObj.getMonth() + 1).padStart(2, '0'); 21 | const day = String(dateObj.getUTCDate()); 22 | const year = String(dateObj.getUTCFullYear()); 23 | 24 | const nowDate = year + month + day; 25 | return nowDate + '_' + randomKeySuffix; 26 | } 27 | 28 | function generateSecretValue() { 29 | 30 | return crypto.randomBytes(64).toString('hex'); 31 | } 32 | 33 | async function getCffUpdatedCode(secret1Key, secret1Value, secret2Key, secret2Value) { 34 | 35 | let newContent = ""; 36 | const allFileContents = fs.readFileSync('cff.js', 'utf-8'); 37 | allFileContents.split(/\r?\n/).forEach(line => { 38 | let newLine; 39 | 40 | line = line.trim() 41 | 42 | if (line.startsWith('var secrets = ')) 43 | newLine = "var secrets = { \"" + secret1Key + "\" : \"" + secret1Value + "\", \"" + secret2Key + "\": \"" + secret2Value + "\" }"; 44 | else if (line.startsWith('exports.handler') || (line.startsWith('exports.decodeString'))) 45 | newLine = ""; 46 | else if (line.includes("return exports.decodeString(str)")) 47 | newLine = line.replace("exports.", ""); 48 | else 49 | newLine = line; 50 | 51 | newContent = newContent + newLine + "\n"; 52 | 53 | 54 | 55 | }); 56 | 57 | return updateCff(newContent); 58 | 59 | } 60 | 61 | async function updateCff(functionCodeAsStr) { 62 | 63 | console.log("Get ETAG for CloudFront Function " + process.env.CFF_NAME); 64 | 65 | let params = { 66 | Name: process.env.CFF_NAME 67 | }; 68 | 69 | let response = await cloudfront.describeFunction(params); 70 | console.log("Update CloudFront Function Code"); 71 | params = { 72 | FunctionCode: Buffer.from(functionCodeAsStr), 73 | FunctionConfig: { 74 | 'Comment': 'CloudFront Function Token validator', 75 | 'Runtime': 'cloudfront-js-1.0' 76 | }, 77 | IfMatch: response['ETag'], 78 | Name: process.env.CFF_NAME 79 | }; 80 | 81 | response = await cloudfront.updateFunction(params); 82 | console.log("response = "+JSON.stringify(response)); 83 | 84 | console.log("Publish CloudFront Function"); 85 | params = { 86 | IfMatch: response['ETag'], 87 | Name: process.env.CFF_NAME 88 | }; 89 | await cloudfront.publishFunction(params); 90 | 91 | 92 | console.log("Cloudfront Function updated"); 93 | 94 | } 95 | 96 | exports.handler = async (event, context) => { 97 | console.log("event=" + JSON.stringify(event)); 98 | const temporaryKeyName = process.env.TEMPORARY_KEY_NAME; 99 | const primaryKeyName = process.env.PRIMARY_KEY_NAME; 100 | const secondaryKeyName = process.env.SECONDARY_KEY_NAME; 101 | 102 | if (event.initialize) { 103 | //Lambda triggered by the custom resource on deploy 104 | console.log("Initialize temporary secret") 105 | 106 | //update temporary secret with a new value 107 | const newSecretKey = generateSecretKey(); 108 | const newSecretValue = generateSecretValue(); 109 | const objectTemporary = {}; 110 | objectTemporary[newSecretKey] = newSecretValue; 111 | let params = { 112 | SecretId: temporaryKeyName, 113 | SecretString: JSON.stringify(objectTemporary) 114 | }; 115 | 116 | await secretsmanager.putSecretValue(params); 117 | 118 | console.log("Initialize primary secret") 119 | 120 | //update primary secret with a new value 121 | const newPrimarySecretKey = generateSecretKey(); 122 | const newPrimarySecretValue = generateSecretValue(); 123 | const objectPrimary = {}; 124 | objectPrimary[newPrimarySecretKey] = newPrimarySecretValue; 125 | params = { 126 | SecretId: primaryKeyName, 127 | SecretString: JSON.stringify(objectPrimary) 128 | }; 129 | 130 | await secretsmanager.putSecretValue(params); 131 | 132 | console.log("Initialize temporary secret") 133 | 134 | //update secondary secret with a new value 135 | const newSecondarySecretKey = generateSecretKey(); 136 | const newSecondarySecretValue = generateSecretValue(); 137 | const objectSecondary = {}; 138 | objectSecondary[newSecondarySecretKey] = newSecondarySecretValue; 139 | params = { 140 | SecretId: secondaryKeyName, 141 | SecretString: JSON.stringify(objectSecondary) 142 | }; 143 | 144 | await secretsmanager.putSecretValue(params); 145 | 146 | 147 | await getCffUpdatedCode(newPrimarySecretKey, newPrimarySecretValue, newSecondarySecretKey, newSecondarySecretValue); 148 | 149 | 150 | } else { 151 | //Lambda triggered by the SF to rotate the secrets 152 | 153 | // Update temporary secret with a new value 154 | const newSecretKey = generateSecretKey(); 155 | const newSecretValue = generateSecretValue(); 156 | const objectTemporary = {}; 157 | objectTemporary[newSecretKey] = newSecretValue; 158 | 159 | let params = { 160 | SecretId: temporaryKeyName, 161 | SecretString: JSON.stringify(objectTemporary) 162 | }; 163 | 164 | await secretsmanager.putSecretValue(params); 165 | 166 | //get primary secret 167 | params = { 168 | SecretId: primaryKeyName 169 | }; 170 | 171 | const responseSecret = await secretsmanager.getSecretValue(params); 172 | 173 | const primarySecretAsJson = JSON.parse(responseSecret.SecretString); 174 | 175 | const primarySecretKeyName = Object.keys(primarySecretAsJson)[0]; 176 | const primarySecretKeyValue = Object.values(primarySecretAsJson)[0]; 177 | 178 | await getCffUpdatedCode(newSecretKey, newSecretValue, primarySecretKeyName, primarySecretKeyValue); 179 | 180 | } 181 | 182 | 183 | 184 | return "OK"; 185 | 186 | }; -------------------------------------------------------------------------------- /source/lambda/generate_token/nodejs/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { DynamoDBDocument } = require("@aws-sdk/lib-dynamodb"); 5 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 6 | const awsSMD = require("aws-secure-media-delivery"); 7 | 8 | const docClient = process.env.METRICS == "true" ? DynamoDBDocument.from(new DynamoDB({ customUserAgent: process.env.SOLUTION_IDENTIFIER })) : DynamoDBDocument.from(new DynamoDB()); 9 | 10 | const stackName = process.env.STACK_NAME; 11 | const tableName = process.env.TABLE_NAME; 12 | 13 | const response400 = { 14 | statusCode: 400, 15 | body: "Bad request" 16 | } 17 | 18 | 19 | awsSMD.Secret.setDEBUG(true); 20 | let secret = new awsSMD.Secret(stackName,4); 21 | secret.initSMClient(); 22 | awsSMD.Token.setDEBUG(true) 23 | let token = new awsSMD.Token(secret); 24 | 25 | function _populate_country_region_city(token_policy, headers) { 26 | const viewer_attributes = {}; 27 | 28 | if(token_policy['co']){ 29 | if(headers['cloudfront-viewer-country']){ 30 | viewer_attributes['co'] = headers['cloudfront-viewer-country']; 31 | } else if(!token_policy['co_fallback']) { 32 | return response400; 33 | } 34 | } 35 | 36 | if(token_policy['reg']){ 37 | if(headers['cloudfront-viewer-country-region']){ 38 | viewer_attributes['reg'] = headers['cloudfront-viewer-country-region']; 39 | } else if(!token_policy['reg_fallback']) { 40 | return response400; 41 | } 42 | } 43 | 44 | if(token_policy['cty']){ 45 | if(headers['cloudfront-viewer-city']){ 46 | viewer_attributes['cty'] = headers['cloudfront-viewer-city']; 47 | } else if(!token_policy['cty_fallback']) { 48 | return response400; 49 | } 50 | } 51 | 52 | return viewer_attributes; 53 | } 54 | 55 | function _populate_viewer_attributes(token_policy, viewer_ip, headers, request_querystrings) { 56 | let viewer_attributes = _populate_country_region_city(token_policy, headers); 57 | if (viewer_attributes.statusCode) return viewer_attributes; 58 | 59 | if(token_policy['ip']) viewer_attributes['ip'] = viewer_ip; 60 | 61 | if(token_policy['headers'] && token_policy['headers'].length > 0){ 62 | viewer_attributes['headers'] = headers; 63 | } 64 | 65 | if(token_policy['querystrings'] && token_policy['querystrings'].length > 0){ 66 | viewer_attributes['qs'] = request_querystrings; 67 | } 68 | 69 | return viewer_attributes; 70 | } 71 | 72 | exports.handler = async (event, context) => { 73 | 74 | console.log(JSON.stringify(event)) 75 | let id; 76 | const headers = event.headers; 77 | let request_querystrings = event.queryStringParameters; 78 | let viewer_ip; 79 | 80 | if(event['queryStringParameters'] && event.queryStringParameters['id']){ 81 | id = event.queryStringParameters['id']; 82 | if(!/^\w+$/.test(id) || (id.length > 200)) return response400; 83 | delete request_querystrings['id']; 84 | } else { 85 | return response400; 86 | } 87 | 88 | if(headers['cloudfront-viewer-address']){ 89 | viewer_ip = headers['cloudfront-viewer-address'].substring(0, headers['cloudfront-viewer-address'].lastIndexOf(':')) 90 | } else { 91 | viewer_ip = event.requestContext.http.sourceIp; 92 | } 93 | 94 | const params = { 95 | TableName: tableName, 96 | Key:{"id": id} 97 | }; 98 | 99 | const video_metadata = await docClient.get(params); 100 | console.log("From DynamoDB:"+JSON.stringify(video_metadata)); 101 | if(!video_metadata.Item){ 102 | return { 103 | "statusCode": 404, 104 | "body": 'No video asset for the given ID' 105 | }; 106 | } 107 | 108 | const endpoint_hostname = video_metadata.Item['endpoint_hostname']; 109 | const video_url = video_metadata.Item['url_path']; 110 | const token_policy = video_metadata.Item.token_policy; 111 | const viewer_attributes = _populate_viewer_attributes(token_policy, viewer_ip, headers, request_querystrings); 112 | 113 | 114 | let original_url; 115 | if(endpoint_hostname && video_url){ 116 | original_url = endpoint_hostname + video_url; 117 | } else { 118 | original_url = null; 119 | } 120 | const playback_url = await token.generate(viewer_attributes, original_url, token_policy); 121 | const body = { 122 | "playback_url": playback_url, 123 | "token_policy" : { 124 | "ip": token_policy.ip ? 1 : 0, 125 | "ip_value": viewer_ip, 126 | "ua": token_policy.headers.includes('user-agent') ? 1 : 0, 127 | "ua_value": headers['user-agent'], 128 | "referer": token_policy.headers.includes('referer') ? 1 : 0, 129 | "referer_value": headers['referer'] 130 | } 131 | }; 132 | return { 133 | "statusCode": 200, 134 | "body": 135 | JSON.stringify(body) 136 | }; 137 | 138 | }; -------------------------------------------------------------------------------- /source/lambda/layers/admzip/nodejs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adm-zip", 3 | "version": "1.2.6", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "adm-zip", 9 | "version": "1.2.6", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "adm-zip": "^0.5.10" 13 | } 14 | }, 15 | "node_modules/adm-zip": { 16 | "version": "0.5.10", 17 | "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", 18 | "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", 19 | "engines": { 20 | "node": ">=6.0" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/lambda/layers/admzip/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adm-zip", 3 | "version": "1.2.6", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "adm-zip": "^0.5.10" 11 | }, 12 | "author": { 13 | "name": "Amazon Web Services", 14 | "url": "https://aws.amazon.com/solutions" 15 | }, 16 | "license": "Apache-2.0" 17 | } -------------------------------------------------------------------------------- /source/lambda/layers/aws_secure_media_delivery_nodejs/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "API_endpoint_layer", 3 | "version": "1.2.6", 4 | "description": "", 5 | "main": "", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "aws-secure-media-delivery": "file:../../../../resources/sdk/node/v1/", 11 | "base64url": "^3.0.1", 12 | "debug": "^4.3.3", 13 | "jsonwebtoken": "^9.0.2" 14 | }, 15 | "overrides": { 16 | "jsonwebtoken": { 17 | "semver": "^7.5.4" 18 | } 19 | }, 20 | "author": { 21 | "name": "Amazon Web Services", 22 | "url": "https://aws.amazon.com/solutions" 23 | }, 24 | "license": "Apache-2.0" 25 | } -------------------------------------------------------------------------------- /source/lambda/save_auto_session/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 5 | const dynamodb = process.env.METRICS == "true" ? new DynamoDB({customUserAgent: process.env.SOLUTION_IDENTIFIER}) : new DynamoDB(); 6 | 7 | exports.handler = async (event, context) => { 8 | console.log("event="+JSON.stringify(event)); 9 | if(Array.isArray(event) && event.length >1){ 10 | console.log("number of elements:"+(event.length-1)); 11 | const SECONDS_IN_AN_HOUR = 60 * 60; 12 | const currentTimestamp = Math.round(Date.now() / 1000); 13 | const expirationTime = currentTimestamp + 24 * SECONDS_IN_AN_HOUR * parseInt(process.env.TTL); 14 | for( const item of event.slice(1) ){ 15 | 16 | const myItem = { 17 | 'session_id': { 'S': item['Data'][0]['VarCharValue']}, 18 | 'type': { 'S': 'AUTO' }, 19 | 'reason': { 'S': 'COMPROMISED' }, 20 | 'score' : { 'N': item['Data'][1]['VarCharValue']}, 21 | 'ip_rate' : { 'N': item['Data'][2]['VarCharValue']}, 22 | 'ip_penalty' : { 'N': item['Data'][3]['VarCharValue']}, 23 | 'referer_penalty' : { 'N': item['Data'][4]['VarCharValue']}, 24 | 'ua_penalty' : { 'N': item['Data'][5]['VarCharValue']}, 25 | 'last_updated' : { 'N': String(currentTimestamp) }, 26 | 'ttl': { 'N': String(expirationTime)} 27 | } 28 | await dynamodb.putItem({ 29 | "TableName": process.env.TABLE_NAME, 30 | "Item": myItem 31 | }); 32 | console.log(`Item inserted, sessionid=${item['Data'][0]['VarCharValue']}`); 33 | } 34 | return "OK"; 35 | }else{ 36 | 37 | throw new Error('Event received must be an array with at least 2 elements'); 38 | } 39 | 40 | 41 | }; -------------------------------------------------------------------------------- /source/lambda/save_manual_session/nodejs/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const awsSMD = require("aws-secure-media-delivery"); 5 | 6 | const tableName = process.env.TABLE_NAME; 7 | const TTL = process.env.TTL; 8 | 9 | awsSMD.Session.setDEBUG(true); 10 | awsSMD.Session.initialize(tableName); 11 | 12 | const response400 = { 13 | statusCode: 400, 14 | body: "Bad request" 15 | } 16 | 17 | const response200 = { 18 | statusCode: 200, 19 | body: "Session submitted to the revocation list" 20 | } 21 | 22 | exports.handler = async (event, context) => { 23 | console.log("event="+JSON.stringify(event)); 24 | 25 | let id; 26 | 27 | if(event['queryStringParameters'] && event.queryStringParameters['sessionid']){ 28 | id = event.queryStringParameters['sessionid']; 29 | if(!/^\w+$/.test(id) || (id.length > 200)) return response400; 30 | } else { 31 | return response400; 32 | } 33 | 34 | let revokeSession = new awsSMD.Session(id); 35 | let result = await revokeSession.revoke(TTL*86400); 36 | return result ? response200 : response400; 37 | 38 | }; -------------------------------------------------------------------------------- /source/lambda/swap_secrets/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { SecretsManager } = require("@aws-sdk/client-secrets-manager"); 5 | const secretsmanager = process.env.METRICS == "true" ? new SecretsManager({customUserAgent: process.env.SOLUTION_IDENTIFIER}) : new SecretsManager(); 6 | 7 | 8 | 9 | 10 | exports.handler = async (event, context) => { 11 | console.log("event="+JSON.stringify(event)); 12 | 13 | const temporaryKeyName = process.env.TEMPORARY_KEY_NAME; 14 | const primaryKeyName = process.env.PRIMARY_KEY_NAME; 15 | const secondaryKeyName = process.env.SECONDARY_KEY_NAME; 16 | 17 | //get temporary secret 18 | let params = { 19 | SecretId: temporaryKeyName 20 | }; 21 | 22 | let responseSecret = await secretsmanager.getSecretValue(params); 23 | console.log(responseSecret); 24 | 25 | const temporarySecretAsJson = JSON.parse(responseSecret.SecretString); 26 | const temporarySecretKeyName = Object.keys(temporarySecretAsJson)[0]; 27 | const temporarySecretKeyValue = Object.values(temporarySecretAsJson)[0]; 28 | 29 | //get primary secret 30 | params = { 31 | SecretId: primaryKeyName 32 | }; 33 | 34 | responseSecret = await secretsmanager.getSecretValue(params); 35 | 36 | const primarySecretAsJson = JSON.parse(responseSecret.SecretString); 37 | const primarySecretKeyName = Object.keys(primarySecretAsJson)[0]; 38 | const primarySecretKeyValue = Object.values(primarySecretAsJson)[0]; 39 | 40 | const objectSecondary = {}; 41 | objectSecondary[primarySecretKeyName] = primarySecretKeyValue; 42 | 43 | //set primary value to secondary secret 44 | params = { 45 | SecretId: secondaryKeyName, 46 | SecretString: JSON.stringify(objectSecondary) 47 | }; 48 | await secretsmanager.putSecretValue(params); 49 | 50 | const objectPrimary = {}; 51 | objectPrimary[temporarySecretKeyName] = temporarySecretKeyValue; 52 | 53 | //set temporary value to primary secret 54 | params = { 55 | SecretId: primaryKeyName, 56 | SecretString: JSON.stringify(objectPrimary) 57 | }; 58 | 59 | await secretsmanager.putSecretValue(params); 60 | 61 | const objectTemporary = {}; 62 | objectTemporary["INITIALIZED_KEY"] = "INITIALIZED_VALUE"; 63 | 64 | //delete the temporary keys 65 | params = { 66 | SecretId: temporaryKeyName, 67 | SecretString: JSON.stringify(objectTemporary) 68 | }; 69 | 70 | await secretsmanager.putSecretValue(params); 71 | 72 | 73 | return "OK"; 74 | 75 | 76 | }; -------------------------------------------------------------------------------- /source/lambda/update_rulegroup/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 5 | const { WAFV2 } = require("@aws-sdk/client-wafv2"); 6 | 7 | const wafv2 = process.env.METRICS == "true" ? new WAFV2({ region: 'us-east-1', customUserAgent: process.env.SOLUTION_IDENTIFIER }) : new WAFV2({ region: 'us-east-1' }); 8 | const dynamodb = process.env.METRICS == "true" ? new DynamoDB({customUserAgent: process.env.SOLUTION_IDENTIFIER}) : new DynamoDB(); 9 | 10 | 11 | const crypto = require("crypto"); 12 | 13 | function getFormattedRuleConfig(sessionId, ruleName, priority) { 14 | return { 15 | "Name": ruleName, 16 | "Priority": priority, 17 | "Statement": { 18 | "ByteMatchStatement": { 19 | "SearchString": Buffer.from(sessionId), 20 | "FieldToMatch": { 21 | "UriPath": { 22 | 23 | } 24 | }, 25 | "TextTransformations": [ 26 | { 27 | "Priority": 0, 28 | "Type": "NONE" 29 | } 30 | ], 31 | "PositionalConstraint": "STARTS_WITH" 32 | } 33 | }, 34 | "Action": { 35 | "Block": { 36 | 37 | } 38 | }, 39 | "VisibilityConfig": { 40 | "SampledRequestsEnabled": true, 41 | "CloudWatchMetricsEnabled": true, 42 | "MetricName": "Example" 43 | } 44 | } 45 | 46 | } 47 | 48 | async function getCurrentRules() { 49 | 50 | const params = { 51 | Id: process.env.RULE_ID, 52 | Name: process.env.RULE_NAME, 53 | Scope: 'CLOUDFRONT' 54 | }; 55 | return wafv2.getRuleGroup(params); 56 | } 57 | 58 | async function updateRules(visibility, lockToken, rules) { 59 | console.log("Update rule group"); 60 | const params = { 61 | Name: process.env.RULE_NAME, 62 | Id: process.env.RULE_ID, 63 | Description: "TokenRevoke", 64 | Scope: "CLOUDFRONT", 65 | VisibilityConfig: visibility, 66 | LockToken: lockToken, 67 | Rules: rules 68 | }; 69 | return wafv2.updateRuleGroup(params); 70 | 71 | } 72 | 73 | async function querySessions() { 74 | 75 | const nowDate = new Date(); 76 | const retentionDateTime = new Date(nowDate.getTime() - parseInt(process.env.RETENTION) * 60000); 77 | 78 | const retentionEpochTimestamp = Math.round(retentionDateTime.getTime() / 1000) 79 | 80 | 81 | const params = { 82 | IndexName: process.env.GSI_INDEX_NAME, 83 | ExpressionAttributeValues: { 84 | ':r': { S: 'COMPROMISED' }, 85 | ':l': { N: String(retentionEpochTimestamp) } 86 | }, 87 | KeyConditionExpression: 'reason = :r and last_updated >= :l', 88 | TableName: process.env.TABLE_NAME 89 | }; 90 | 91 | return dynamodb.query(params); 92 | } 93 | 94 | function getRandomAlphanumericString() { 95 | return crypto.randomBytes(8).toString('hex'); 96 | 97 | } 98 | 99 | exports.handler = async (event, context) => { 100 | console.log("event=" + JSON.stringify(event)); 101 | 102 | const result = await querySessions(); 103 | if (!result['Items']) { 104 | console.log("No Session ID from DynamoDB Table. Nothing to do.") 105 | } else { 106 | let globalIndex = 1 107 | let localIndex 108 | let rules = [] 109 | const maxSessions = parseInt(process.env.MAX_SESSIONS) / 2; 110 | 111 | const items = result['Items']; 112 | console.log(`${items.length} Sessions IDs from DynamoDB to process`) 113 | 114 | //look for manual sessions 115 | const manualSessions = items.filter(function (e) { 116 | return e['type']['S'] == 'MANUAL'; 117 | }); 118 | 119 | //look for auto sessions 120 | const autoSessions = items.filter(function (e) { 121 | return e['type']['S'] == 'AUTO'; 122 | }); 123 | 124 | autoSessions.sort((a, b) => { 125 | return b['score']['N'] - a['score']['N']; 126 | }); 127 | 128 | localIndex = 1 129 | for (const item of manualSessions) { 130 | if (globalIndex > maxSessions) { 131 | console.log("Max items added to rule group reached, stopping iteration through results from dynamodb") 132 | break 133 | } 134 | 135 | const myRuleName = String(getRandomAlphanumericString()) 136 | const currentRule1 = getFormattedRuleConfig('/' + item['session_id']['S'], myRuleName, globalIndex) 137 | rules.push(currentRule1) 138 | globalIndex += 1 139 | localIndex += 1 140 | 141 | console.log(`${(localIndex - 1)} MANUAL Sessions IDs to add to Rule Group`) 142 | } 143 | 144 | localIndex = 1 145 | for (const item of autoSessions) { 146 | if (globalIndex <= maxSessions) { 147 | const myRuleName = String(getRandomAlphanumericString()) 148 | const currentRule2 = getFormattedRuleConfig('/' + item['session_id']['S'], myRuleName, globalIndex) 149 | rules.push(currentRule2) 150 | globalIndex += 1 151 | localIndex += 1 152 | } 153 | else { 154 | console.log("Max items added to rule group reached, stopping iteration through results from dynamodb") 155 | break 156 | } 157 | console.log(`${(localIndex - 1)} AUTO Sessions IDs to add to Rule Group`) 158 | } 159 | 160 | console.log(`${(globalIndex - 1)} Sessions IDs from DynamoDB to attach to rule group`) 161 | if (rules.length > 0) { 162 | const attachedRules = await getCurrentRules(); 163 | await updateRules(attachedRules['RuleGroup']['VisibilityConfig'], attachedRules['LockToken'], rules) 164 | } 165 | 166 | } 167 | 168 | 169 | return { 170 | 'statusCode': 200, 171 | 'body': JSON.stringify("Revoked sessions: ") 172 | } 173 | 174 | }; -------------------------------------------------------------------------------- /source/lambda/update_token/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const { DynamoDBDocument } = require("@aws-sdk/lib-dynamodb"); 5 | const { DynamoDB } = require("@aws-sdk/client-dynamodb"); 6 | const dynamodb = process.env.METRICS == "true" ? DynamoDBDocument.from(new DynamoDB({customUserAgent: process.env.SOLUTION_IDENTIFIER})) : DynamoDBDocument.from(new DynamoDB()); 7 | 8 | 9 | exports.handler = async (event, context) => { 10 | console.log('Received event:', JSON.stringify(event)); 11 | try { 12 | const ip = parseInt(event.queryStringParameters.ip); 13 | const ua = parseInt(event.queryStringParameters.ua); 14 | const referer = parseInt(event.queryStringParameters.referer); 15 | console.log(event.queryStringParameters); 16 | 17 | const tokenHeaders=[]; 18 | if(ua==1){ 19 | tokenHeaders.push("user-agent") 20 | } 21 | 22 | if(referer==1){ 23 | tokenHeaders.push("referer") 24 | } 25 | 26 | const tokenIp = ip==1 ? true : false; 27 | 28 | console.log(tokenHeaders); 29 | console.log(tokenIp); 30 | 31 | const params = { 32 | TableName: process.env.TABLE_NAME, 33 | Key: { 34 | id: String(event.queryStringParameters.id), 35 | }, 36 | UpdateExpression: "set token_policy.headers = :myHeaders, token_policy.ip = :myIp ", 37 | ExpressionAttributeValues: { 38 | ":myHeaders": tokenHeaders, 39 | ":myIp": tokenIp 40 | }, 41 | ReturnValues: "UPDATED_NEW", 42 | }; 43 | 44 | console.log(JSON.stringify(params)); 45 | await dynamodb.update(params); 46 | 47 | return { 48 | statusCode: 200, 49 | body: "Token updated" 50 | }; 51 | } catch (error) { 52 | console.log(error); 53 | return { 54 | statusCode: 400, 55 | body: "Error when updating the token" 56 | }; 57 | } 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /source/lib/application_registry/application_registry.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as appreg from '@aws-cdk/aws-servicecatalogappregistry-alpha'; 5 | import { 6 | Aws, 7 | CfnMapping, 8 | Fn, 9 | Stack, 10 | Tags, 11 | } from 'aws-cdk-lib'; 12 | import { IConfiguration } from '../../helpers/validators/configuration'; 13 | 14 | export function applyAppRegistry(stack: Stack, solutionConfig: IConfiguration) { 15 | const map = new CfnMapping(stack, "Solution"); 16 | map.setValue("Data", "ID", solutionConfig.solutionId); 17 | map.setValue("Data", "Version", solutionConfig.solutionVersion); 18 | map.setValue("Data", "AppRegistryApplicationName", solutionConfig.solutionName); 19 | map.setValue("Data", "SolutionName", solutionConfig.solutionDisplayName); 20 | map.setValue("Data", "ApplicationType", "AWS-Solutions"); 21 | 22 | const application = new appreg.Application(stack, "AppRegistry", { 23 | applicationName: Fn.join("-", [ 24 | map.findInMap("Data", "AppRegistryApplicationName"), 25 | Aws.REGION, 26 | Aws.ACCOUNT_ID, 27 | Aws.STACK_NAME, 28 | ]), 29 | description: `Service Catalog application to track and manage all your resources for the solution ${map.findInMap("Data", "SolutionName")}`, 30 | }); 31 | application.associateApplicationWithStack(stack); 32 | 33 | Tags.of(application).add("Solutions:SolutionID", map.findInMap("Data", "ID")); 34 | Tags.of(application).add("Solutions:SolutionName", map.findInMap("Data", "SolutionName")); 35 | Tags.of(application).add("Solutions:SolutionVersion", map.findInMap("Data", "Version")); 36 | Tags.of(application).add("Solutions:ApplicationType", map.findInMap("Data", "ApplicationType")); 37 | 38 | const attributeGroup = new appreg.AttributeGroup( 39 | stack, 40 | "DefaultApplicationAttributes", 41 | { 42 | attributeGroupName: Fn.join("-", [ 43 | Aws.REGION, 44 | Aws.STACK_NAME 45 | ]), 46 | description: "Attribute group for solution information", 47 | attributes: { 48 | applicationType: map.findInMap("Data", "ApplicationType"), 49 | version: map.findInMap("Data", "Version"), 50 | solutionID: map.findInMap("Data", "ID"), 51 | solutionName: map.findInMap("Data", "SolutionName"), 52 | }, 53 | } 54 | ); 55 | attributeGroup.associateWith(application); 56 | } 57 | -------------------------------------------------------------------------------- /source/lib/auto_revocation_stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | Stack, 6 | RemovalPolicy, 7 | Aws, 8 | aws_dynamodb as ddb, 9 | aws_s3 as s3, 10 | aws_lambda as lambda, 11 | aws_logs as logs, 12 | aws_sqs as sqs, 13 | aws_iam as iam, 14 | aws_lambda_event_sources as event_source, 15 | StackProps 16 | } from "aws-cdk-lib"; 17 | import { ITable } from "aws-cdk-lib/aws-dynamodb"; 18 | 19 | import { Construct } from "constructs"; 20 | import { IConfiguration } from "../helpers/validators/configuration"; 21 | import { AutoRevokeSessionsWorkflow } from "./autorevocation/auto_revocation_workflow"; 22 | import { CrLoadSqlParams } from "./custom_resources/cr_load_athena_config_table"; 23 | import { addCfnSuppressRules } from "./cfn_nag/cfn_nag_utils"; 24 | 25 | export interface AutoSessionRevocationStackProps extends StackProps { 26 | readonly description: string 27 | } 28 | 29 | export class AutoSessionRevocationStack extends Stack { 30 | 31 | constructor( 32 | scope: Construct, 33 | id: string, 34 | configuration: IConfiguration, 35 | sessionsTable: ITable, 36 | props: AutoSessionRevocationStackProps 37 | ) { 38 | super(scope, id, props); 39 | 40 | const sqlQueryBucket = new s3.Bucket(this, "SqlQuery", { 41 | encryption: s3.BucketEncryption.S3_MANAGED, 42 | blockPublicAccess: new s3.BlockPublicAccess({ 43 | blockPublicPolicy: true, 44 | blockPublicAcls: true, 45 | ignorePublicAcls: true, 46 | restrictPublicBuckets: true 47 | }), 48 | versioned: true, 49 | enforceSSL: true, 50 | }); 51 | 52 | addCfnSuppressRules(sqlQueryBucket, [{ id: 'W51', reason: 'The bucket is used to store results from Athena Query' }]); 53 | addCfnSuppressRules(sqlQueryBucket, [{ id: 'W35', reason: 'It is a log bucket, access logging is not necessary' }]); 54 | 55 | 56 | //DynamoDB table holding the configuration for Athena Query (that is populate on deploying the stack and that can be modified by a user at anytime) 57 | const sqlConfigTable = new ddb.Table(this, "SqlConfigTable", { 58 | billingMode: ddb.BillingMode.PAY_PER_REQUEST, 59 | partitionKey: { name: "table_name", type: ddb.AttributeType.STRING }, 60 | removalPolicy: RemovalPolicy.DESTROY, 61 | stream: ddb.StreamViewType.NEW_IMAGE, 62 | pointInTimeRecovery: true, 63 | }); 64 | 65 | addCfnSuppressRules(sqlConfigTable, [{ id: 'W74', reason: 'DynamoDB table has encryption enabled owned by Amazon.' }]); 66 | 67 | 68 | const autoRevocationWorflow = new AutoRevokeSessionsWorkflow( 69 | this, 70 | "GetSessions", 71 | { 72 | bucket: sqlQueryBucket, 73 | dynamodbTable: sessionsTable, 74 | configuration: configuration, 75 | } 76 | ); 77 | 78 | //When DynamoDB table holding the configuration for Athena query is modified, the Lambda is triggered and updates the env params for SubmitQuery Lambda 79 | //the StepFunction when running the query against CloudFront logs 80 | const exportParams = new lambda.Function(this, "ExportParams", { 81 | runtime: lambda.Runtime.NODEJS_18_X, 82 | functionName: Aws.STACK_NAME + "_ExportParams", 83 | code: lambda.Code.fromAsset("lambda/export_params"), 84 | handler: "index.handler", 85 | environment: { 86 | SUBMIT_QUERY_FUNCTION : autoRevocationWorflow.submitQueryFunction.functionName, 87 | SOLUTION_IDENTIFIER: `AwsSolution/${configuration.solutionId}/${configuration.solutionVersion}`, 88 | METRICS: String(configuration.main.metrics) 89 | }, 90 | }); 91 | 92 | exportParams.addToRolePolicy( 93 | new iam.PolicyStatement({ 94 | effect: iam.Effect.ALLOW, 95 | actions: [ 96 | "lambda:UpdateFunctionConfiguration" 97 | ], 98 | resources: [autoRevocationWorflow.submitQueryFunction.functionArn], 99 | }) 100 | ); 101 | 102 | const crLoadSqlParams = new CrLoadSqlParams(this, "SqlConfig", { 103 | table: sqlConfigTable, 104 | configuration: configuration, 105 | }); 106 | 107 | //wait to create the table and lambda 108 | crLoadSqlParams.node.addDependency(sqlConfigTable); 109 | crLoadSqlParams.node.addDependency(exportParams); 110 | 111 | addCfnSuppressRules(exportParams, [{ id: 'W58', reason: 'Lambda has CloudWatch permissions by using service role AWSLambdaBasicExecutionRole' }]); 112 | addCfnSuppressRules(exportParams, [{ id: 'W89', reason: 'We don t have any VPC in the stack, we only use serverless services' }]); 113 | addCfnSuppressRules(exportParams, [{ id: 'W92', reason: 'No need for ReservedConcurrentExecutions, some are used only for the demo website, and others are not used in a concurrent mode.' }]); 114 | 115 | 116 | sqlQueryBucket.grantReadWrite(exportParams); 117 | 118 | // Set Lambda Logs Retention and Removal Policy 119 | const readStreamLogs = new logs.LogGroup(this, "ReadStreamLogs", { 120 | logGroupName: "/aws/lambda/" + exportParams.functionName, 121 | removalPolicy: RemovalPolicy.DESTROY, 122 | retention: logs.RetentionDays.ONE_MONTH, 123 | }); 124 | 125 | addCfnSuppressRules(readStreamLogs, [{ id: 'W84', reason: 'CloudWatch log group is always encrypted by default.' }]); 126 | 127 | 128 | const deadLetterQueue = new sqs.Queue(this, "deadLetterQueue", { 129 | encryption: sqs.QueueEncryption.KMS_MANAGED, 130 | }); 131 | 132 | addCfnSuppressRules(deadLetterQueue, [{ id: 'W92', reason: 'We are satisfied with default KMS encryption on SQS queue.' }]); 133 | 134 | 135 | exportParams.addEventSource( 136 | new event_source.DynamoEventSource(sqlConfigTable, { 137 | startingPosition: lambda.StartingPosition.TRIM_HORIZON, 138 | batchSize: 5, 139 | bisectBatchOnError: true, 140 | onFailure: new event_source.SqsDlq(deadLetterQueue), 141 | retryAttempts: 10, 142 | }) 143 | ); 144 | 145 | 146 | 147 | 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /source/lib/cfn/cfn_parameters.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CfnParameter, Stack } from "aws-cdk-lib"; 5 | import { Construct } from "constructs"; 6 | 7 | interface ParameterGroup { 8 | Label: { default: string }; 9 | Parameters: string[]; 10 | } 11 | 12 | interface ParameterLabels { 13 | [parameterLogicalId: string]: { default: string }; 14 | } 15 | 16 | interface CloudFormationInterface { 17 | ParameterGroups: ParameterGroup[]; 18 | ParameterLabels: ParameterLabels; 19 | } 20 | 21 | const getStackMetadata = (scope: Construct): { [key: string]: any } => 22 | Stack.of(scope).templateOptions.metadata || {}; 23 | 24 | const CFN_INTERFACE_KEY = "AWS::CloudFormation::Interface"; 25 | 26 | const createEmptyCfnInterface = (): CloudFormationInterface => ({ 27 | ParameterGroups: [], 28 | ParameterLabels: {}, 29 | }); 30 | 31 | const getCfnInterface = (scope: Construct): CloudFormationInterface => { 32 | const metadata = getStackMetadata(scope); 33 | return metadata[CFN_INTERFACE_KEY] 34 | ? metadata[CFN_INTERFACE_KEY] 35 | : createEmptyCfnInterface(); 36 | }; 37 | 38 | const updateCfnInterface = ( 39 | cfnInterface: CloudFormationInterface, 40 | scope: Construct 41 | ): void => { 42 | const metadata = getStackMetadata(scope); 43 | metadata[CFN_INTERFACE_KEY] = cfnInterface; 44 | Stack.of(scope).templateOptions.metadata = metadata; 45 | }; 46 | 47 | const getGroupFromInterface = ( 48 | label: string, 49 | cfnInterface: CloudFormationInterface 50 | ): ParameterGroup | undefined => 51 | cfnInterface.ParameterGroups.find((group) => group.Label.default === label); 52 | 53 | const addGroupToInterface = ( 54 | label: string, 55 | cfnInterface: CloudFormationInterface 56 | ): ParameterGroup => { 57 | const existingGroup = getGroupFromInterface(label, cfnInterface); 58 | if (existingGroup) { 59 | return existingGroup; 60 | } else { 61 | const newGroup = { Label: { default: label }, Parameters: [] }; 62 | cfnInterface.ParameterGroups.push(newGroup); 63 | return newGroup; 64 | } 65 | }; 66 | 67 | const addParameterToGroup = ( 68 | parameter: CfnParameter, 69 | group: ParameterGroup 70 | ): void => { 71 | if (group.Parameters.find((logicalId) => logicalId === parameter.logicalId)) { 72 | return; 73 | } else { 74 | group.Parameters.push(parameter.logicalId); 75 | } 76 | }; 77 | 78 | export interface ParameterInterfaceProps { 79 | scope: Construct; 80 | parameter: CfnParameter; 81 | groupLabel?: string; 82 | parameterLabel?: string; 83 | } 84 | 85 | export interface ParametersInterfaceProps { 86 | params: ParameterInterfaceProps[]; 87 | } 88 | 89 | const addParameterToInterface = ( 90 | props: ParameterInterfaceProps 91 | ): CfnParameter => { 92 | const { scope, groupLabel, parameter, parameterLabel } = props; 93 | const cfnInterface = getCfnInterface(scope); 94 | 95 | if (groupLabel) { 96 | const group = addGroupToInterface(groupLabel, cfnInterface); 97 | addParameterToGroup(parameter, group); 98 | } 99 | 100 | if (parameterLabel) { 101 | cfnInterface.ParameterLabels[parameter.logicalId] = { 102 | default: parameterLabel, 103 | }; 104 | } 105 | 106 | updateCfnInterface(cfnInterface, scope); 107 | return parameter; 108 | }; 109 | 110 | export const addParametersToInterface = (props: ParametersInterfaceProps) => { 111 | props.params.forEach((item) => { 112 | addParameterToInterface(item); 113 | }); 114 | }; 115 | -------------------------------------------------------------------------------- /source/lib/cfn_nag/cfn_nag_utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { CfnResource, Resource } from "aws-cdk-lib"; 4 | 5 | 6 | interface CfnNagSuppressRule { 7 | id: string; 8 | reason: string; 9 | } 10 | 11 | /** 12 | * Adds CFN NAG suppress rules to the CDK resource. 13 | * @param resource The CDK resource. 14 | * @param rules The CFN NAG suppress rules. 15 | */ 16 | export function addCfnSuppressRules(resource: Resource | CfnResource | undefined, rules: CfnNagSuppressRule[]) { 17 | if (typeof resource === 'undefined') return; 18 | 19 | if (resource instanceof Resource) { 20 | resource = resource.node.defaultChild as CfnResource; 21 | } 22 | 23 | if (resource.cfnOptions.metadata?.cfn_nag?.rules_to_suppress) { 24 | resource.cfnOptions.metadata.cfn_nag.rules_to_suppress.push(...rules); 25 | } else { 26 | resource.addMetadata('cfn_nag', { rules_to_suppress: rules }); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /source/lib/custom_resources/cr_create_le_rule.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { 4 | Aws, 5 | aws_lambda as lambda, 6 | aws_iam as iam, 7 | triggers, 8 | Duration, 9 | } from "aws-cdk-lib"; 10 | 11 | import { Construct } from "constructs"; 12 | import { addCfnSuppressRules } from "../cfn_nag/cfn_nag_utils"; 13 | 14 | export interface IConfigProps { 15 | WCU: string; 16 | LAMBDA_EDGE_VERSION_SSM_PARAM: string; 17 | WAF_RULE_NAME_SSM_PARAM: string; 18 | WAF_RULE_ID_SSM_PARAM: string; 19 | DEPLOY_LE: boolean; 20 | METRICS: boolean; 21 | SOLUTION_IDENTIFIER: string; 22 | } 23 | 24 | export class CRCreateLEWafRule extends Construct { 25 | 26 | public readonly roleToPass: iam.IRole; 27 | 28 | constructor(scope: Construct, id: string, props: IConfigProps) { 29 | 30 | 31 | super(scope, id); 32 | 33 | const triggerPolicy = new iam.PolicyDocument({ 34 | statements: [ 35 | new iam.PolicyStatement({ 36 | resources: [`arn:aws:lambda:*:${Aws.ACCOUNT_ID}:function:*`], 37 | actions: [ 38 | "lambda:CreateFunction", 39 | "lambda:PublishVersion", 40 | "lambda:GetFunctionConfiguration", 41 | ], 42 | }), 43 | new iam.PolicyStatement({ 44 | resources: ["*"], 45 | actions: ["iam:PassRole"], 46 | conditions: { 47 | StringEquals: { 48 | "iam:PassedToService": ["lambda.amazonaws.com"], 49 | }, 50 | }, 51 | }), 52 | new iam.PolicyStatement({ 53 | resources: [ 54 | `arn:aws:ssm:${Aws.REGION}:${Aws.ACCOUNT_ID}:parameter/*`, 55 | ], 56 | actions: ["ssm:PutParameter"], 57 | }), 58 | new iam.PolicyStatement({ 59 | resources: [ 60 | `arn:aws:wafv2:us-east-1:${Aws.ACCOUNT_ID}:global/rulegroup/*/*`, 61 | ], 62 | actions: ["wafv2:CreateRuleGroup"], 63 | }), 64 | ], 65 | }); 66 | 67 | 68 | const lambdaEdgePolicy = new iam.PolicyDocument({ 69 | statements: [ 70 | new iam.PolicyStatement({ 71 | resources: [`arn:aws:execute-api:${Aws.REGION}:*:*/*`], 72 | actions: [ 73 | "execute-api:Invoke" 74 | ], 75 | }) 76 | ], 77 | }); 78 | 79 | const { managedPolicyArn } = iam.ManagedPolicy.fromAwsManagedPolicyName( 80 | "service-role/AWSLambdaBasicExecutionRole" 81 | ); 82 | 83 | const triggerRole = new iam.Role(this, "TriggerLERole", { 84 | assumedBy: new iam.CompositePrincipal( 85 | new iam.ServicePrincipal("edgelambda.amazonaws.com"), 86 | new iam.ServicePrincipal("lambda.amazonaws.com"), 87 | 88 | ), 89 | managedPolicies: [ 90 | { 91 | managedPolicyArn, 92 | }, 93 | ], 94 | inlinePolicies: { 95 | myPolicy: triggerPolicy, 96 | }, 97 | }); 98 | 99 | addCfnSuppressRules(triggerRole, [ 100 | { 101 | id: "W11", 102 | reason: 103 | "The resource is a Custom Resource, automatically generated by CDK", 104 | }, 105 | ]); 106 | addCfnSuppressRules(triggerRole, [ 107 | { 108 | id: "F38", 109 | reason: 110 | "The resource is a Custom Resource, automatically generated by CDK, impossible to get the arn", 111 | }, 112 | ]); 113 | 114 | const roleToPass = new iam.Role(this, "EdgeLambdaServiceRole", { 115 | assumedBy: new iam.CompositePrincipal( 116 | new iam.ServicePrincipal("edgelambda.amazonaws.com"), 117 | new iam.ServicePrincipal("lambda.amazonaws.com") 118 | ), 119 | managedPolicies: [ 120 | { 121 | managedPolicyArn, 122 | }, 123 | ], 124 | inlinePolicies: { 125 | myPolicy: lambdaEdgePolicy, 126 | }, 127 | }); 128 | 129 | const archiverLayer = new lambda.LayerVersion(this, "AdmZipLayer", { 130 | compatibleRuntimes: [lambda.Runtime.NODEJS_18_X], 131 | code: lambda.Code.fromAsset("lambda/layers/admzip"), 132 | description: "Layer used to zip lambda edge file", 133 | }); 134 | 135 | new triggers.TriggerFunction(this, "UsEast1Trigger", { // NOSONAR 136 | functionName: Aws.STACK_NAME + "_CustomResourceUsEast1", 137 | runtime: lambda.Runtime.NODEJS_18_X, 138 | handler: "index.handler", 139 | timeout: Duration.seconds(600), 140 | code: lambda.Code.fromAsset("lambda/custom_resource_us_east_1"), 141 | layers: [archiverLayer], 142 | environment: { 143 | ROLE_ARN: roleToPass.roleArn, 144 | STACK_NAME: Aws.STACK_NAME, 145 | LAMBDA_VERSION: props.LAMBDA_EDGE_VERSION_SSM_PARAM, 146 | WCU: props.WCU, 147 | RULE_ID: props.WAF_RULE_ID_SSM_PARAM, 148 | RULE_NAME: props.WAF_RULE_NAME_SSM_PARAM, 149 | DEPLOY_LE: props.DEPLOY_LE ? "1" : "0", 150 | METRICS: String(props.METRICS), 151 | SOLUTION_IDENTIFIER: props.SOLUTION_IDENTIFIER 152 | }, 153 | 154 | role: triggerRole, 155 | }); 156 | 157 | this.roleToPass = roleToPass; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /source/lib/custom_resources/cr_init_secrets.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { custom_resources, aws_iam as iam } from "aws-cdk-lib"; 5 | 6 | import { Construct } from "constructs"; 7 | 8 | export interface IConfigProps { 9 | functionArn: string; 10 | functionName: string; 11 | } 12 | 13 | export class CrInitSecrets extends Construct { 14 | constructor(scope: Construct, id: string, props: IConfigProps) { 15 | super(scope, id); 16 | 17 | new custom_resources.AwsCustomResource(this, "rotateSecrets", { // NOSONAR 18 | onCreate: { 19 | service: "Lambda", 20 | action: "invoke", 21 | parameters: { 22 | FunctionName: props.functionName, 23 | Payload: `{"initialize": true}`, 24 | }, 25 | physicalResourceId: custom_resources.PhysicalResourceId.of( 26 | "initSecretsResourceId" 27 | ), 28 | }, 29 | policy: custom_resources.AwsCustomResourcePolicy.fromStatements([ 30 | new iam.PolicyStatement({ 31 | effect: iam.Effect.ALLOW, 32 | actions: ["lambda:InvokeFunction"], 33 | resources: [props.functionArn], 34 | }), 35 | ]) 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /source/lib/custom_resources/cr_load_assets_table.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { custom_resources } from "aws-cdk-lib"; 5 | import { ITable } from "aws-cdk-lib/aws-dynamodb"; 6 | 7 | import { Construct } from "constructs"; 8 | import * as fs from "fs"; 9 | import { IConfiguration } from "../../helpers/validators/configuration"; 10 | 11 | export interface IConfigProps { 12 | table: ITable; 13 | configuration: IConfiguration; 14 | } 15 | 16 | export class CrLoadAssetsTable extends Construct { 17 | constructor(scope: Construct, id: string, props: IConfigProps) { 18 | super(scope, id); 19 | 20 | new custom_resources.AwsCustomResource(this, "initDBResource", { // NOSONAR 21 | onCreate: { 22 | service: "DynamoDB", 23 | action: "batchWriteItem", 24 | parameters: { 25 | RequestItems: { 26 | [props.table.tableName]: this.loadItems(props.configuration), 27 | }, 28 | }, 29 | physicalResourceId: 30 | custom_resources.PhysicalResourceId.of("initDBData"), 31 | }, 32 | policy: custom_resources.AwsCustomResourcePolicy.fromSdkCalls({ 33 | resources: [props.table.tableArn], 34 | }) 35 | }); 36 | } 37 | 38 | private loadItems = (configuration: IConfiguration) => { 39 | let fileContent = fs.readFileSync("resources/mock/assets.json").toString(); 40 | let itemsToInsert = new Array(); 41 | if (configuration.hls) { 42 | const urlPath = configuration.hls?.url_path; 43 | const path = urlPath.substring(0, urlPath.lastIndexOf("/")) + "/"; 44 | 45 | let hlsFileContent = fileContent.replace( 46 | "CUSTOM_HOST_NAME", 47 | configuration.hls?.hostname 48 | ); 49 | hlsFileContent = hlsFileContent.replace( 50 | "CUSTOM_URL_PATH", 51 | configuration.hls?.url_path 52 | ); 53 | hlsFileContent = hlsFileContent.replace( 54 | "CUSTOM_TTL", 55 | configuration.hls?.ttl 56 | ); 57 | hlsFileContent = hlsFileContent.replace("CUSTOM_ID", "1"); 58 | hlsFileContent = hlsFileContent.replace("CUSTOM_PATH", path); 59 | itemsToInsert.push({ PutRequest: { Item: JSON.parse(hlsFileContent) } }); 60 | } 61 | 62 | if (configuration.dash) { 63 | const urlPath = configuration.dash?.url_path; 64 | const path = urlPath.substring(0, urlPath.lastIndexOf("/")) + "/"; 65 | let dashFileContent = fileContent.replace( 66 | "CUSTOM_HOST_NAME", 67 | configuration.dash?.hostname 68 | ); 69 | dashFileContent = dashFileContent.replace( 70 | "CUSTOM_URL_PATH", 71 | configuration.dash?.url_path 72 | ); 73 | dashFileContent = dashFileContent.replace( 74 | "CUSTOM_TTL", 75 | configuration.dash?.ttl 76 | ); 77 | dashFileContent = dashFileContent.replace("CUSTOM_ID", "2"); 78 | dashFileContent = dashFileContent.replace("CUSTOM_PATH", path); 79 | 80 | itemsToInsert.push({ PutRequest: { Item: JSON.parse(dashFileContent) } }); 81 | } 82 | 83 | if (itemsToInsert.length == 0) { 84 | //DASH or HLS not configured 85 | fileContent = fileContent.replace("ID", "1"); 86 | itemsToInsert.push({ PutRequest: { Item: JSON.parse(fileContent) } }); 87 | } 88 | 89 | return itemsToInsert; 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /source/lib/custom_resources/cr_load_athena_config_table.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { custom_resources } from "aws-cdk-lib"; 5 | import { ITable } from "aws-cdk-lib/aws-dynamodb"; 6 | 7 | import { Construct } from "constructs"; 8 | import { IConfiguration } from "../../helpers/validators/configuration"; 9 | 10 | export interface IConfigProps { 11 | table: ITable; 12 | configuration: IConfiguration; 13 | } 14 | 15 | export class CrLoadSqlParams extends Construct { 16 | constructor(scope: Construct, id: string, props: IConfigProps) { 17 | super(scope, id); 18 | 19 | new custom_resources.AwsCustomResource(this, "initDBResource", { // NOSONAR 20 | onCreate: { 21 | service: "DynamoDB", 22 | action: "putItem", 23 | parameters: { 24 | TableName: props.table.tableName, 25 | Item: this.loadItems(props), 26 | }, 27 | physicalResourceId: 28 | custom_resources.PhysicalResourceId.of("loadSqlParams"), 29 | }, 30 | policy: custom_resources.AwsCustomResourcePolicy.fromSdkCalls({ 31 | resources: [props.table.tableArn], 32 | }) 33 | }); 34 | 35 | 36 | } 37 | 38 | 39 | 40 | private loadItems = (props: IConfigProps) => { 41 | return { 42 | trigger_workflow_frequency: { 43 | N: props.configuration.sessionRevocation?.trigger_workflow_frequency.toString(), 44 | }, 45 | db_name: { S: props.configuration.sessionRevocation?.db_name }, 46 | table_name: { S: props.configuration.sessionRevocation?.table_name }, 47 | request_ip_column: { 48 | S: props.configuration.sessionRevocation?.request_ip_column, 49 | }, 50 | ua_column_name: { 51 | S: props.configuration.sessionRevocation?.ua_column_name, 52 | }, 53 | referer_column_name: { 54 | S: props.configuration.sessionRevocation?.referer_column_name, 55 | }, 56 | uri_column_name: { 57 | S: props.configuration.sessionRevocation?.uri_column_name, 58 | }, 59 | status_column_name: { 60 | S: props.configuration.sessionRevocation?.status_column_name, 61 | }, 62 | response_bytes_column_name: { 63 | S: props.configuration.sessionRevocation?.response_bytes_column_name, 64 | }, 65 | date_column_name: { 66 | S: props.configuration.sessionRevocation?.date_column_name, 67 | }, 68 | time_column_name: { 69 | S: props.configuration.sessionRevocation?.time_column_name, 70 | }, 71 | lookback_period: { 72 | N: props.configuration.sessionRevocation?.lookback_period.toString(), 73 | }, 74 | ip_penalty: { 75 | N: props.configuration.sessionRevocation?.ip_rate.toString(), 76 | }, 77 | ip_rate: { 78 | N: props.configuration.sessionRevocation?.ip_penalty.toString(), 79 | }, 80 | referer_penalty: { 81 | N: props.configuration.sessionRevocation?.referer_penalty.toString(), 82 | }, 83 | ua_penalty: { 84 | N: props.configuration.sessionRevocation?.ua_penalty.toString(), 85 | }, 86 | min_sessions_number: { 87 | N: props.configuration.sessionRevocation?.min_sessions_number.toString(), 88 | }, 89 | min_session_duration: { 90 | N: props.configuration.sessionRevocation?.min_session_duration.toString(), 91 | }, 92 | score_threshold: { 93 | N: props.configuration.sessionRevocation?.score_threshold.toString(), 94 | }, 95 | partitioned: { 96 | N: props.configuration.sessionRevocation?.partitioned.toString(), 97 | }, 98 | }; 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /source/lib/main/dashboard.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Duration, Aws, aws_cloudwatch as cloudwatch } from "aws-cdk-lib"; 5 | import { Dashboard } from "aws-cdk-lib/aws-cloudwatch"; 6 | 7 | import { Construct } from "constructs"; 8 | 9 | /** 10 | * The properties expected by the config construct. 11 | */ 12 | export interface ICoreConfigProps { 13 | cfFunctionName: string; 14 | rotateSecretsWorkflowArn: string; 15 | } 16 | 17 | export interface IApiConfigProps { 18 | lambdaFunctionName: string; 19 | region: string; 20 | } 21 | 22 | export class CWDashboard extends Construct { 23 | public readonly dashboard: Dashboard; 24 | private readonly EXECUTION_SUCCEEDED = "ExecutionsSucceeded"; 25 | private readonly EXECUTION_SUCCEEDED_LABEL = "Success"; 26 | private readonly EXECUTION_FAILED = "ExecutionsFailed"; 27 | private readonly EXECUTION_FAILED_LABEL = "Failure"; 28 | 29 | constructor(scope: Construct, id: string) { 30 | super(scope, id); 31 | 32 | this.dashboard = new cloudwatch.Dashboard(this, "MonitoringDashboard", { 33 | dashboardName: Aws.STACK_NAME + "-Secure-Media-Stream-Delivery", 34 | }); 35 | } 36 | 37 | buildCoreDashboard(props: ICoreConfigProps) { 38 | const checkTokenWidget = new cloudwatch.LogQueryWidget({ 39 | logGroupNames: ["/aws/cloudfront/function/" + props.cfFunctionName], 40 | view: cloudwatch.LogQueryVisualizationType.PIE, 41 | title: "Verify JWT token", 42 | width: 9, 43 | height: 6, 44 | region : "us-east-1", 45 | queryLines: [ 46 | "fields @timestamp, @message", 47 | "filter @message like /X_JWT_CHECK/", 48 | 'parse "* * *" as a,b,result', 49 | "stats count(*) as RESULT by result as total", 50 | ], 51 | }); 52 | 53 | const cffComputeUsageMetric = new cloudwatch.Metric({ 54 | namespace: "AWS/CloudFront", 55 | metricName: "FunctionComputeUtilization", 56 | period: Duration.minutes(5), 57 | dimensionsMap: { FunctionName: props.cfFunctionName, Region: "Global" }, 58 | label: "Compute usage", 59 | statistic: "avg", 60 | region: "us-east-1" 61 | }); 62 | 63 | const cffExecutionErrorsMetric = new cloudwatch.Metric({ 64 | namespace: "AWS/CloudFront", 65 | metricName: "FunctionExecutionErrors", 66 | period: Duration.minutes(5), 67 | dimensionsMap: { FunctionName: props.cfFunctionName, Region: "Global" }, 68 | label: "Function Execution Errors", 69 | statistic: "sum", 70 | region: "us-east-1" 71 | }); 72 | 73 | const cffThrottlesMetric = new cloudwatch.Metric({ 74 | namespace: "AWS/CloudFront", 75 | metricName: "FunctionThrottles", 76 | period: Duration.minutes(5), 77 | dimensionsMap: { FunctionName: props.cfFunctionName, Region: "Global" }, 78 | label: "Function Throttles", 79 | statistic: "sum", 80 | region: "us-east-1" 81 | }); 82 | 83 | const cffInvocationsMetric = new cloudwatch.Metric({ 84 | namespace: "AWS/CloudFront", 85 | metricName: "FunctionInvocations", 86 | period: Duration.minutes(5), 87 | region : "us-east-1", 88 | dimensionsMap: { FunctionName: props.cfFunctionName, Region: "Global" }, 89 | label: "Invocations", 90 | statistic: "sum", 91 | }); 92 | 93 | const computeUsageWidget = new cloudwatch.GraphWidget({ 94 | title: "Check JWT Token - Compute Utilization (Avg)", 95 | height: 6, 96 | width: 24, 97 | setPeriodToTimeRange: true, 98 | left: [cffComputeUsageMetric], 99 | }); 100 | 101 | const functionExecutionErrorsWidget = new cloudwatch.GraphWidget({ 102 | title: "Check JWT Token - Function Execution Errors (Sum)", 103 | height: 6, 104 | width: 24, 105 | setPeriodToTimeRange: true, 106 | left: [cffExecutionErrorsMetric], 107 | }); 108 | 109 | const functionThrottlesWidget = new cloudwatch.GraphWidget({ 110 | title: "Check JWT Token - Function Throttles (Sum)", 111 | height: 6, 112 | width: 24, 113 | setPeriodToTimeRange: true, 114 | left: [cffThrottlesMetric], 115 | }); 116 | 117 | const rotateSecretsWidget = new cloudwatch.GraphWidget({ 118 | title: "Rotate Secrets", 119 | view: cloudwatch.GraphWidgetView.PIE, 120 | width: 9, 121 | height: 6, 122 | setPeriodToTimeRange: true, 123 | left: [ 124 | this.sumSfnMetricFails(props.rotateSecretsWorkflowArn), 125 | this.sumSfnMetricSucceeded(props.rotateSecretsWorkflowArn), 126 | ], 127 | }); 128 | 129 | const invocationsWidget = new cloudwatch.GraphWidget({ 130 | title: "Check JWT Token - Invocations (Sum)", 131 | height: 6, 132 | width: 24, 133 | stacked: true, 134 | setPeriodToTimeRange: true, 135 | left: [cffInvocationsMetric], 136 | }); 137 | 138 | const invocationsNbWidget = new cloudwatch.SingleValueWidget({ 139 | title: "Tokens checked", 140 | height: 6, 141 | width: 6, 142 | setPeriodToTimeRange: true, 143 | metrics: [cffInvocationsMetric], 144 | }); 145 | 146 | this.dashboard.addWidgets( 147 | checkTokenWidget, 148 | rotateSecretsWidget, 149 | invocationsNbWidget, 150 | computeUsageWidget, 151 | functionExecutionErrorsWidget, 152 | functionThrottlesWidget, 153 | invocationsWidget 154 | ); 155 | } 156 | 157 | buildApiDashboard(props: IApiConfigProps) { 158 | const tokensGeneratedMetric = new cloudwatch.Metric({ 159 | namespace: "AWS/Lambda", 160 | metricName: "Invocations", 161 | period: Duration.minutes(5), 162 | dimensionsMap: { FunctionName: props.lambdaFunctionName }, 163 | }); 164 | 165 | const invocationsNbWidget = new cloudwatch.SingleValueWidget({ 166 | title: "Nb of tokens generated", 167 | height: 6, 168 | width: 6, 169 | setPeriodToTimeRange: true, 170 | metrics: [tokensGeneratedMetric], 171 | }); 172 | 173 | const invocationsWidget = new cloudwatch.GraphWidget({ 174 | title: "Tokens generated", 175 | height: 6, 176 | width: 18, 177 | region: props.region, 178 | setPeriodToTimeRange: true, 179 | left: [tokensGeneratedMetric], 180 | }); 181 | 182 | this.dashboard.addWidgets(invocationsNbWidget, invocationsWidget); 183 | } 184 | 185 | sumSfnMetricSucceeded(resourceArn: string) { 186 | return this.sumSfnMetric( 187 | resourceArn, 188 | this.EXECUTION_SUCCEEDED, 189 | this.EXECUTION_SUCCEEDED_LABEL 190 | ); 191 | } 192 | 193 | sumSfnMetricFails(resourceArn: string) { 194 | return this.sumSfnMetric( 195 | resourceArn, 196 | this.EXECUTION_FAILED, 197 | this.EXECUTION_FAILED_LABEL 198 | ); 199 | } 200 | 201 | sumSfnMetric(resourceArn: string, metricName: string, label: string) { 202 | return new cloudwatch.Metric({ 203 | namespace: "AWS/States", 204 | metricName: metricName, 205 | period: Duration.minutes(5), 206 | dimensionsMap: { StateMachineArn: resourceArn }, 207 | label: label, 208 | statistic: "sum", 209 | }); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /source/lib/main/secrets.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { 4 | Aws, 5 | CfnOutput, 6 | aws_secretsmanager as secretsmanager 7 | } from "aws-cdk-lib"; 8 | import { Construct } from "constructs"; 9 | import { addCfnSuppressRules } from "../cfn_nag/cfn_nag_utils"; 10 | 11 | export class Secrets extends Construct { 12 | public readonly primarySecret: secretsmanager.ISecret; 13 | public readonly secondarySecret: secretsmanager.ISecret; 14 | public readonly temporarySecret: secretsmanager.ISecret; 15 | 16 | constructor(scope: Construct, id: string) { 17 | super(scope, id); 18 | 19 | const primarySecret = new secretsmanager.Secret(this, "Primary", { 20 | secretName: Aws.STACK_NAME + "_PrimarySecret", 21 | description: "Primary secret for Secure Media Delivery at the Edge on AWS", 22 | generateSecretString: { 23 | secretStringTemplate: JSON.stringify({ MY_PRIMARY_KEY: "" }), 24 | generateStringKey: "MY_PRIMARY_KEY", 25 | }, 26 | }); 27 | 28 | addCfnSuppressRules(primarySecret, [{ id: 'W77', reason: 'By default CDK provisions this secret using a default encryption key sourced from AWS Key Management Service. We are satisfied with default KMS encryption on secrets.' }]); 29 | 30 | 31 | const secondarySecret = new secretsmanager.Secret(this, "Secondary", { 32 | secretName: Aws.STACK_NAME + "_SecondarySecret", 33 | description: "Secondary secret for Secure Media Delivery at the Edge on AWS", 34 | generateSecretString: { 35 | secretStringTemplate: JSON.stringify({ MY_SECONDARY_KEY: "" }), 36 | generateStringKey: "MY_SECONDARY_KEY", 37 | }, 38 | }); 39 | 40 | addCfnSuppressRules(secondarySecret, [{ id: 'W77', reason: 'By default CDK provisions this secret using a default encryption key sourced from AWS Key Management Service. We are satisfied with default KMS encryption on secrets.' }]); 41 | 42 | 43 | const temporarySecret = new secretsmanager.Secret(this, "Temporary", { 44 | secretName: Aws.STACK_NAME + "_TemporarySecret", 45 | description: "Temporary secret for Secure Media Delivery at the Edge on AWS", 46 | generateSecretString: { 47 | secretStringTemplate: JSON.stringify({ MY_TEMPORARY_KEY: "" }), 48 | generateStringKey: "MY_TEMPORARY_KEY", 49 | }, 50 | }); 51 | 52 | addCfnSuppressRules(temporarySecret, [{ id: 'W77', reason: 'By default CDK provisions this secret using a default encryption key sourced from AWS Key Management Service. We are satisfied with default KMS encryption on secrets.' }]); 53 | 54 | this.primarySecret = primarySecret; 55 | this.secondarySecret = secondarySecret; 56 | this.temporarySecret = temporarySecret; 57 | 58 | new CfnOutput(this, "PrimarySecret", { // NOSONAR 59 | value: primarySecret.secretName, 60 | description: "The name of the PrimarySecret", 61 | }); 62 | 63 | new CfnOutput(this, "SecondarySecret", { // NOSONAR 64 | value: secondarySecret.secretName, 65 | description: "The name of the SecondarySecret", 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /source/lib/main/session_revocation.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { 4 | Aws, 5 | RemovalPolicy, 6 | custom_resources, 7 | aws_dynamodb as ddb, 8 | aws_lambda as lambda, 9 | aws_logs as logs, 10 | aws_iam as iam, 11 | aws_sqs as sqs, 12 | aws_lambda_event_sources as event_source, 13 | CfnOutput, 14 | } from "aws-cdk-lib"; 15 | import { ITable } from "aws-cdk-lib/aws-dynamodb"; 16 | import { Construct } from "constructs"; 17 | import { IConfiguration } from "../../helpers/validators/configuration"; 18 | import { addCfnSuppressRules } from "../cfn_nag/cfn_nag_utils"; 19 | 20 | export interface IConfigProps { 21 | sessionToRevoke: ITable; 22 | gsi_index_name: string; 23 | ruleNameParamName: string; 24 | ruleIdParamName: string; 25 | configuration: IConfiguration; 26 | } 27 | 28 | export class SessionRevocation extends Construct { 29 | public readonly sessionsTable: ddb.ITable; 30 | constructor(scope: Construct, id: string, config: IConfigProps) { 31 | super(scope, id); 32 | 33 | const accountId = Aws.ACCOUNT_ID; 34 | //Getting the RuleGroup ID create in us-east-1 region (in a different stack) 35 | const ssmRuleGroupParameterId = new custom_resources.AwsCustomResource( 36 | this, 37 | "SSMParameter", 38 | { 39 | onCreate: { 40 | service: "SSM", 41 | action: "getParameter", 42 | parameters: { Name: config.ruleIdParamName }, 43 | region: Aws.REGION, 44 | physicalResourceId: custom_resources.PhysicalResourceId.of( 45 | `${config.ruleIdParamName}-${Aws.REGION}` 46 | ), 47 | }, 48 | policy: custom_resources.AwsCustomResourcePolicy.fromStatements([ 49 | new iam.PolicyStatement({ 50 | effect: iam.Effect.ALLOW, 51 | actions: ["ssm:GetParameter*"], 52 | resources: [ 53 | `arn:aws:ssm:${Aws.REGION}:${accountId}:parameter/*`, 54 | ], 55 | }), 56 | ]), 57 | } 58 | ); 59 | 60 | 61 | 62 | const ssmRuleGroupId = 63 | ssmRuleGroupParameterId.getResponseField("Parameter.Value"); 64 | 65 | //Revoke an active session 66 | //Update WAF RuleGroup with sessions from DynamoDB 67 | const updateRuleGroupFunction = new lambda.Function( 68 | this, 69 | "UpdateRuleGroup", 70 | { 71 | runtime: lambda.Runtime.NODEJS_18_X, 72 | functionName: Aws.STACK_NAME + "_UpdateRuleGroup", 73 | code: lambda.Code.fromAsset("lambda/update_rulegroup"), 74 | handler: "index.handler", 75 | environment: { 76 | RULE_ID: ssmRuleGroupId, 77 | RULE_NAME: config.ruleNameParamName, 78 | RETENTION: config.configuration.main.retention, 79 | TABLE_NAME: config.sessionToRevoke.tableName, 80 | MAX_SESSIONS: config.configuration.main.wcu, 81 | GSI_INDEX_NAME: config.gsi_index_name, 82 | SOLUTION_IDENTIFIER: `AwsSolution/${config.configuration.solutionId}/${config.configuration.solutionVersion}`, 83 | METRICS: String(config.configuration.main.metrics) 84 | }, 85 | } 86 | ); 87 | 88 | 89 | 90 | // Set Lambda Logs Retention and Removal Policy 91 | const myLogs = new logs.LogGroup(this, "ReadStreamLogs", { 92 | logGroupName: "/aws/lambda/" + updateRuleGroupFunction.functionName, 93 | removalPolicy: RemovalPolicy.DESTROY, 94 | retention: logs.RetentionDays.ONE_MONTH, 95 | }); 96 | 97 | addCfnSuppressRules(myLogs, [{ id: 'W84', reason: 'We are satisfied with default KMS encryption on CloudWatchLogs LogGroup.' }]); 98 | 99 | 100 | updateRuleGroupFunction.addToRolePolicy( 101 | new iam.PolicyStatement({ 102 | effect: iam.Effect.ALLOW, 103 | actions: [ 104 | "wafv2:GetRuleGroup", 105 | "wafv2:UpdateRuleGroup", 106 | "wafv2:ListRuleGroups", 107 | ], 108 | resources: [ 109 | `arn:aws:wafv2:us-east-1:${accountId}:global/rulegroup/${config.ruleNameParamName}/${ssmRuleGroupId}`, 110 | ], 111 | }) 112 | ); 113 | 114 | addCfnSuppressRules(updateRuleGroupFunction, [{ id: 'W58', reason: 'Lambda has CloudWatch permissions by using service role AWSLambdaBasicExecutionRole' }]); 115 | addCfnSuppressRules(updateRuleGroupFunction, [{ id: 'W89', reason: 'We don t have any VPC in the stack, we only use serverless services' }]); 116 | addCfnSuppressRules(updateRuleGroupFunction, [{ id: 'W92', reason: 'No need for ReservedConcurrentExecutions, some are used only for the demo website, and others are not used in a concurrent mode.' }]); 117 | 118 | const deadLetterQueue = new sqs.Queue(this, "updateRuleGroupDlq", { 119 | encryption: sqs.QueueEncryption.KMS_MANAGED, 120 | }); 121 | 122 | addCfnSuppressRules(deadLetterQueue, [{ id: 'W92', reason: 'We are satisfied with default KMS encryption on SQS queue.' }]); 123 | 124 | 125 | //trigger the Lambda every time when DynamoDB table is updated 126 | updateRuleGroupFunction.addEventSource( 127 | new event_source.DynamoEventSource(config.sessionToRevoke, { 128 | startingPosition: lambda.StartingPosition.TRIM_HORIZON, 129 | batchSize: 5, 130 | bisectBatchOnError: true, 131 | onFailure: new event_source.SqsDlq(deadLetterQueue), 132 | retryAttempts: 10, 133 | }) 134 | ); 135 | 136 | config.sessionToRevoke.grantReadData(updateRuleGroupFunction); 137 | addCfnSuppressRules(deadLetterQueue, [{ id: 'W12', reason: '* policy is generated by grantReadData method above' }]); 138 | 139 | 140 | new CfnOutput(this, "WafRuleGroupName", { // NOSONAR 141 | description: "WAF RuleGroup Name", 142 | value: config.ruleNameParamName 143 | }); 144 | 145 | new CfnOutput(this, "WafRuleGroupId", { // NOSONAR 146 | description: "WAF RuleGroup Id", 147 | value: ssmRuleGroupId 148 | }); 149 | 150 | new CfnOutput(this, "WafRuleGroupArn", { // NOSONAR 151 | description: "WAF RuleGroup Name Arn", 152 | value : `arn:aws:wafv2:us-east-1:${Aws.ACCOUNT_ID}:global/rulegroup/${config.ruleNameParamName}/${ssmRuleGroupId}` 153 | }); 154 | 155 | 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-solution", 3 | "version": "v1.2.6", 4 | "description": "Synthesize templates for Secure Media Delivery at the Edge on AWS using AWS Cloud Development Kit (CDK).", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Amazon Web Services", 8 | "url": "https://aws.amazon.com/solutions" 9 | }, 10 | "bin": { 11 | "cdk-solution": "bin/secure_media_stream.js" 12 | }, 13 | "scripts": { 14 | "postinstall": "npm run build", 15 | "build": "npx tsc", 16 | "watch": "npx tsc -w", 17 | "test": "jest --coverage", 18 | "clean": "rm -rf node_modules/ dist/ coverage/ package-lock.json", 19 | "lint": "npx eslint .", 20 | "audit": "npm audit && npx cdk synth | cfn_nag", 21 | "bootstrap": "npx cdk bootstrap", 22 | "deploy": "npx cdk deploy --all", 23 | "wizard": "node dist/bin/wizard/index.js" 24 | }, 25 | "devDependencies": { 26 | "@aws-sdk/client-cloudfront": "^3.622.0", 27 | "@aws-sdk/client-dynamodb": "^3.622.0", 28 | "@aws-sdk/client-lambda": "^3.622.0", 29 | "@aws-sdk/client-secrets-manager": "^3.622.0", 30 | "@aws-sdk/client-ssm": "^3.622.0", 31 | "@aws-sdk/client-wafv2": "^3.622.0", 32 | "@aws-sdk/credential-providers": "^3.622.0", 33 | "@aws-sdk/lib-dynamodb": "^3.622.0", 34 | "@types/babel__traverse": "7.18.2", 35 | "@types/jest": "^29.5.1", 36 | "@types/node": "^20.1.4", 37 | "@types/prompts": "^2.0.14", 38 | "@typescript-eslint/eslint-plugin": "^6.7.0", 39 | "@typescript-eslint/parser": "^6.7.0", 40 | "aws-cdk": "2.150.0", 41 | "aws-sdk-client-mock": "^2.2.0", 42 | "jest": "^29.5.0", 43 | "ts-jest": "^29.1.0", 44 | "ts-node": "^10.7.0", 45 | "typescript": "^5.0.4", 46 | "adm-zip": "^0.5.10" 47 | }, 48 | "dependencies": { 49 | "@aws-cdk/aws-servicecatalogappregistry-alpha": "^2.150.0-alpha.0", 50 | "aws-cdk-lib": "^2.150.0", 51 | "constructs": "^10.3.0", 52 | "joi": "^17.6.0", 53 | "prompts": "^2.4.2", 54 | "source-map-support": "^0.5.16" 55 | }, 56 | "overrides": { 57 | "path-to-regexp": ">=8.1.0" 58 | } 59 | } -------------------------------------------------------------------------------- /source/resources/demo_website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo_website", 3 | "version": "1.2.6", 4 | "description": "Demo website for Secure Media Delivery At the Edge on AWS", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "start": "webpack serve --mode development" 9 | }, 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "license": "Apache-2.0", 15 | "devDependencies": { 16 | "copy-webpack-plugin": "^12.0.2", 17 | "css-loader": "^7.1.2", 18 | "file-loader": "^6.2.0", 19 | "html-webpack-plugin": "^5.6.3", 20 | "style-loader": "^4.0.0", 21 | "webpack": "^5.96.1", 22 | "webpack-cli": "^5.1.4", 23 | "webpack-dev-server": "^5.1.0" 24 | }, 25 | "dependencies": { 26 | "bootstrap": "^5.3.3", 27 | "jquery": "^3.7.1", 28 | "popper.js": "^1.16.1", 29 | "qrcode": "^1.5.4", 30 | "video.js": "8.0.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/resources/demo_website/src/css/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --jumbotron-padding-y: 3rem; 3 | } 4 | .bg-dark { 5 | background-color: #202c3d!important; 6 | } 7 | .jumbotron { 8 | padding-top: var(--jumbotron-padding-y); 9 | padding-bottom: var(--jumbotron-padding-y); 10 | margin-bottom: 0; 11 | background-color: #fff; 12 | } 13 | @media (min-width: 768px) { 14 | .jumbotron { 15 | padding-top: calc(var(--jumbotron-padding-y) * 2); 16 | padding-bottom: calc(var(--jumbotron-padding-y) * 2); 17 | } 18 | } 19 | 20 | .jumbotron p:last-child { 21 | margin-bottom: 0; 22 | } 23 | 24 | .jumbotron .container { 25 | max-width: 40rem; 26 | } 27 | 28 | footer { 29 | padding-top: 3rem; 30 | padding-bottom: 3rem; 31 | } 32 | 33 | footer p { 34 | margin-bottom: .25rem; 35 | } 36 | 37 | .box-shadow { box-shadow: 0 .25rem .75rem rgba(208, 52, 52, 0.05); } 38 | 39 | 40 | .form-signin { 41 | width: 100%; 42 | max-width: 330px; 43 | padding: 15px; 44 | margin: 0 auto; 45 | } 46 | .form-signin .checkbox { 47 | font-weight: 400; 48 | } 49 | .form-signin .form-control { 50 | position: relative; 51 | box-sizing: border-box; 52 | height: auto; 53 | padding: 10px; 54 | font-size: 16px; 55 | } 56 | .form-signin .form-control:focus { 57 | z-index: 2; 58 | } 59 | .form-signin input[type="username"] { 60 | margin-bottom: -1px; 61 | border-bottom-right-radius: 0; 62 | border-bottom-left-radius: 0; 63 | } 64 | .form-signin input[type="password"] { 65 | margin-bottom: 10px; 66 | border-top-left-radius: 0; 67 | border-top-right-radius: 0; 68 | } 69 | 70 | pre { 71 | background-color: rgb(255, 255, 255); 72 | border: 1px solid rgb(230, 230, 230); 73 | padding: 10px 20px; 74 | margin: 20px; 75 | } 76 | .json-key { 77 | color: brown; 78 | } 79 | .json-value { 80 | color: navy; 81 | } 82 | .json-string { 83 | color: olive; 84 | } 85 | 86 | 87 | 88 | .btn:active, 89 | .active { 90 | color: black !important; 91 | background-color: #13d622 !important; 92 | border-color: #13d622 !important; 93 | } 94 | 95 | .btn .i{ 96 | color: white !important; 97 | border-color: white !important; 98 | } 99 | 100 | #hls { 101 | width: 200px; 102 | } 103 | 104 | #dash { 105 | width: 200px; 106 | } 107 | 108 | .my-btn { 109 | width: 200px; 110 | } 111 | 112 | body { 113 | font-weight: 250; 114 | } -------------------------------------------------------------------------------- /source/resources/demo_website/src/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/resources/demo_website/src/img/favicon.ico -------------------------------------------------------------------------------- /source/resources/demo_website/src/img/smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/resources/demo_website/src/img/smile.png -------------------------------------------------------------------------------- /source/resources/demo_website/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | import 'video.js/dist/video-js.css'; 6 | import './css/app.css'; 7 | 8 | import $ from 'jquery'; 9 | import 'bootstrap'; 10 | import videojs from 'video.js'; 11 | import QRCode from 'qrcode' 12 | 13 | window.$ = $; 14 | window.jQuery = $; 15 | window.videojs = videojs; 16 | window.QRCode = QRCode; 17 | 18 | import './js/main.js'; -------------------------------------------------------------------------------- /source/resources/demo_website/webpack.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const path = require('path'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | 8 | module.exports = { 9 | mode: 'production', 10 | entry: './src/index.js', 11 | output: { 12 | filename: 'bundle.js', 13 | path: path.resolve(__dirname, 'dist'), 14 | }, 15 | performance: { 16 | hints: 'warning', 17 | // bundled size is large but the site works well 18 | maxEntrypointSize: 4536000, 19 | maxAssetSize: 4536000 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.css$/, 25 | use: ['style-loader', 'css-loader'], 26 | }, 27 | { 28 | test: /\.ico$/, 29 | type: 'asset/resource', 30 | generator: { 31 | filename: '[name][ext]' 32 | } 33 | }, 34 | ], 35 | }, 36 | plugins: [ 37 | new HtmlWebpackPlugin({ 38 | template: './src/index.html', 39 | favicon: './src/img/favicon.ico' 40 | }), 41 | new CopyWebpackPlugin({ 42 | patterns: [ 43 | { from: 'src/img/smile.png', to: 'img/smile.png' }, 44 | { from: 'src/img/favicon.ico', to: 'img/favicon.ico' } 45 | ], 46 | }), 47 | ], 48 | }; -------------------------------------------------------------------------------- /source/resources/empty_demo_website/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-solutions/secure-media-delivery-at-the-edge-on-aws/1e17e145467b7f81c4d51b0398857beac4a859c2/source/resources/empty_demo_website/.empty -------------------------------------------------------------------------------- /source/resources/mock/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "id":{ 3 | "S":"CUSTOM_ID" 4 | }, 5 | "url_path":{ 6 | "S":"CUSTOM_URL_PATH" 7 | }, 8 | "endpoint_hostname":{ 9 | "S":"CUSTOM_HOST_NAME" 10 | }, 11 | "token_policy":{ 12 | "M":{ 13 | "headers":{ 14 | "L":[ 15 | { 16 | "S":"user-agent" 17 | }, 18 | { 19 | "S":"referer" 20 | } 21 | ] 22 | }, 23 | "exc":{ 24 | "L":[ 25 | { 26 | "S":"/ads/" 27 | } 28 | ] 29 | }, 30 | "nbf":{ 31 | "S":"1645000000" 32 | }, 33 | "session_auto_generate":{ 34 | "N":"12" 35 | }, 36 | "ssn":{ 37 | "BOOL":true 38 | }, 39 | "cty_fallback":{ 40 | "BOOL":true 41 | }, 42 | "paths":{ 43 | "L":[ 44 | { 45 | "S":"CUSTOM_PATH" 46 | } 47 | ] 48 | }, 49 | "ip":{ 50 | "BOOL":false 51 | }, 52 | "cty":{ 53 | "BOOL":false 54 | }, 55 | "co_fallback":{ 56 | "BOOL":true 57 | }, 58 | "co":{ 59 | "BOOL":false 60 | }, 61 | "exp":{ 62 | "S":"CUSTOM_TTL" 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /source/resources/sdk/node/v1/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-secure-media-delivery", 3 | "version": "1.2.6", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "aws-secure-media-delivery", 9 | "version": "1.2.6", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "base64url": "^3.0.1", 13 | "jsonwebtoken": "^9.0.0" 14 | } 15 | }, 16 | "node_modules/base64url": { 17 | "version": "3.0.1", 18 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", 19 | "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", 20 | "engines": { 21 | "node": ">=6.0.0" 22 | } 23 | }, 24 | "node_modules/buffer-equal-constant-time": { 25 | "version": "1.0.1", 26 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 27 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 28 | }, 29 | "node_modules/ecdsa-sig-formatter": { 30 | "version": "1.0.11", 31 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 32 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 33 | "dependencies": { 34 | "safe-buffer": "^5.0.1" 35 | } 36 | }, 37 | "node_modules/jsonwebtoken": { 38 | "version": "9.0.2", 39 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 40 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 41 | "dependencies": { 42 | "jws": "^3.2.2", 43 | "lodash.includes": "^4.3.0", 44 | "lodash.isboolean": "^3.0.3", 45 | "lodash.isinteger": "^4.0.4", 46 | "lodash.isnumber": "^3.0.3", 47 | "lodash.isplainobject": "^4.0.6", 48 | "lodash.isstring": "^4.0.1", 49 | "lodash.once": "^4.0.0", 50 | "ms": "^2.1.1", 51 | "semver": "^7.5.4" 52 | }, 53 | "engines": { 54 | "node": ">=12", 55 | "npm": ">=6" 56 | } 57 | }, 58 | "node_modules/jwa": { 59 | "version": "1.4.1", 60 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 61 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 62 | "dependencies": { 63 | "buffer-equal-constant-time": "1.0.1", 64 | "ecdsa-sig-formatter": "1.0.11", 65 | "safe-buffer": "^5.0.1" 66 | } 67 | }, 68 | "node_modules/jws": { 69 | "version": "3.2.2", 70 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 71 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 72 | "dependencies": { 73 | "jwa": "^1.4.1", 74 | "safe-buffer": "^5.0.1" 75 | } 76 | }, 77 | "node_modules/lodash.includes": { 78 | "version": "4.3.0", 79 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 80 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" 81 | }, 82 | "node_modules/lodash.isboolean": { 83 | "version": "3.0.3", 84 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 85 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" 86 | }, 87 | "node_modules/lodash.isinteger": { 88 | "version": "4.0.4", 89 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 90 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" 91 | }, 92 | "node_modules/lodash.isnumber": { 93 | "version": "3.0.3", 94 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 95 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" 96 | }, 97 | "node_modules/lodash.isplainobject": { 98 | "version": "4.0.6", 99 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 100 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" 101 | }, 102 | "node_modules/lodash.isstring": { 103 | "version": "4.0.1", 104 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 105 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" 106 | }, 107 | "node_modules/lodash.once": { 108 | "version": "4.1.1", 109 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 110 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" 111 | }, 112 | "node_modules/ms": { 113 | "version": "2.1.3", 114 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 115 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 116 | }, 117 | "node_modules/safe-buffer": { 118 | "version": "5.2.1", 119 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 120 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 121 | "funding": [ 122 | { 123 | "type": "github", 124 | "url": "https://github.com/sponsors/feross" 125 | }, 126 | { 127 | "type": "patreon", 128 | "url": "https://www.patreon.com/feross" 129 | }, 130 | { 131 | "type": "consulting", 132 | "url": "https://feross.org/support" 133 | } 134 | ] 135 | }, 136 | "node_modules/semver": { 137 | "version": "7.6.2", 138 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 139 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 140 | "bin": { 141 | "semver": "bin/semver.js" 142 | }, 143 | "engines": { 144 | "node": ">=10" 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /source/resources/sdk/node/v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-secure-media-delivery", 3 | "version": "1.2.6", 4 | "description": "Node.js collection of modules of Secure Media Delivery at the Edge on AWS Solution.", 5 | "main": "aws-secure-media-delivery.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "license": "Apache-2.0", 10 | "author": { 11 | "name": "Amazon Web Services", 12 | "url": "https://aws.amazon.com/solutions" 13 | }, 14 | "dependencies": { 15 | "base64url": "^3.0.1", 16 | "jsonwebtoken": "^9.0.0" 17 | }, 18 | "overrides": { 19 | "jsonwebtoken": { 20 | "semver": "^7.5.4" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /source/solution.context.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "stack_name": "SECURESTREAM", 4 | "assets_bucket_name" : "MY_ASSETS_BUCKET_NAME", 5 | "rotate_secrets_frequency": "1m", 6 | "rotate_secrets_pattern": "P", 7 | "wcu": "100", 8 | "retention": "5", 9 | "metrics": true 10 | }, 11 | "api": { 12 | "demo": true 13 | 14 | }, 15 | "hls": { 16 | "hostname": "H", 17 | "url_path": "U", 18 | "ttl": "+3h" 19 | }, 20 | "dash": { 21 | "hostname": "H", 22 | "url_path": "U", 23 | "ttl": "+24h" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /source/test/__mocks__/aws-sdk-mock.ts: -------------------------------------------------------------------------------- 1 | import { mockClient } from 'aws-sdk-client-mock'; 2 | const { 3 | Lambda, 4 | UpdateFunctionConfigurationCommand, 5 | CreateFunctionCommand, 6 | GetFunctionConfigurationCommand, 7 | PublishVersionCommand, 8 | } = require("@aws-sdk/client-lambda"); 9 | const { SSM, PutParameterCommand } = require("@aws-sdk/client-ssm"); 10 | const { 11 | WAFV2, 12 | GetRuleGroupCommand, 13 | UpdateRuleGroupCommand, 14 | CreateRuleGroupCommand, 15 | } = require("@aws-sdk/client-wafv2"); 16 | const { 17 | CloudFront, 18 | DescribeFunctionCommand, 19 | UpdateFunctionCommand, 20 | PublishFunctionCommand, 21 | } = require("@aws-sdk/client-cloudfront"); 22 | const { 23 | SecretsManager, 24 | GetSecretValueCommand, 25 | PutSecretValueCommand 26 | } = require("@aws-sdk/client-secrets-manager"); 27 | const { DynamoDBDocument, GetCommand, UpdateCommand } = require("@aws-sdk/lib-dynamodb"); 28 | const { DynamoDB, PutItemCommand, QueryCommand } = require("@aws-sdk/client-dynamodb"); 29 | import mockResponse from './mock-responses'; 30 | 31 | const mockAwsClient = (awsClient: any, clientMethod: any, mockResponse: any, mockObject: any = null) => { 32 | const mock = mockObject || mockClient(awsClient); 33 | mock 34 | .on(clientMethod) 35 | .resolves(mockResponse); 36 | return mock; 37 | } 38 | 39 | const mockSecretsManager = () => { 40 | const mock = mockAwsClient(SecretsManager, GetSecretValueCommand, mockResponse.SecretsManager.GetSecretValue); 41 | mockAwsClient(SecretsManager, PutSecretValueCommand, '', mock); 42 | return mock; 43 | } 44 | 45 | const mockLambda = () => { 46 | const mock = mockAwsClient(Lambda, UpdateFunctionConfigurationCommand, undefined); 47 | mockAwsClient(Lambda, CreateFunctionCommand, { FunctionArn: 'MyFunctionArn' }, mock); 48 | mockAwsClient(Lambda, GetFunctionConfigurationCommand, { FunctionArn: 'MyFunctionArn', State: 'Active' }, mock); 49 | mockAwsClient(Lambda, PublishVersionCommand, { Version: '1' }, mock); 50 | return mock; 51 | } 52 | 53 | const mockSSM = () => { 54 | return mockAwsClient(SSM, PutParameterCommand, mockResponse.SSM.PutParameter); 55 | }; 56 | 57 | const mockWAFV2 = () => { 58 | const mock = mockAwsClient(WAFV2, GetRuleGroupCommand, mockResponse.WAFV2.GetRuleGroup); 59 | mockAwsClient(WAFV2, UpdateRuleGroupCommand, undefined, mock); 60 | mockAwsClient(WAFV2, CreateRuleGroupCommand, mockResponse.WAFV2.CreateRuleGroup, mock); 61 | return mock; 62 | }; 63 | 64 | const mockDynamoDB = () => { 65 | const mock = mockAwsClient(DynamoDB, PutItemCommand, ''); 66 | mockAwsClient(DynamoDB, QueryCommand, mockResponse.DynamoDB.Query, mock); 67 | return mock; 68 | }; 69 | 70 | const mockDynamoDBDocument = () => { 71 | const mock = mockAwsClient(DynamoDBDocument, GetCommand, mockResponse.DynamoDBDocument.Get); 72 | mockAwsClient(DynamoDBDocument, UpdateCommand, '', mock); 73 | return mock; 74 | }; 75 | 76 | const mockCloudfront = () => { 77 | const mock = mockAwsClient(CloudFront, DescribeFunctionCommand, mockResponse.CloudFront.DescribeFunction); 78 | mockAwsClient(CloudFront, UpdateFunctionCommand, '', mock); 79 | mockAwsClient(CloudFront, PublishFunctionCommand, '', mock); 80 | return mock; 81 | }; 82 | 83 | export default { 84 | mockAllAWSClients: () => { 85 | const mocks = []; 86 | mocks.push(mockSecretsManager()); 87 | mocks.push(mockLambda()); 88 | mocks.push(mockSSM()); 89 | mocks.push(mockWAFV2()); 90 | mocks.push(mockDynamoDB()); 91 | mocks.push(mockDynamoDBDocument()); 92 | mocks.push(mockCloudfront()); 93 | return mocks; 94 | }, 95 | reseMocks: (mocks: any[]) => { 96 | for (const mock of mocks) { 97 | mock.reset(); 98 | } 99 | }, 100 | }; 101 | -------------------------------------------------------------------------------- /source/test/__mocks__/mock-responses.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | DynamoDB: { 3 | Query: { 4 | Items: [ 5 | { 6 | last_updated: { 7 | N: '1658409174' 8 | }, 9 | score: { 10 | N: '1234561' 11 | }, 12 | reason: { 13 | S: 'COMPROMISED' 14 | }, 15 | session_id: { 16 | S: 'sessionid1' 17 | }, 18 | type: { 19 | S: 'AUTO' 20 | } 21 | }, 22 | { 23 | last_updated: { 24 | N: '1658409174' 25 | }, 26 | score: { 27 | N: '1234561' 28 | }, 29 | reason: { 30 | S: 'COMPROMISED' 31 | }, 32 | session_id: { 33 | S: 'sessionid2' 34 | }, 35 | type: { 36 | S: 'MANUAL' 37 | } 38 | }, 39 | ], 40 | Count: 1, 41 | ScannedCount: 1 42 | } 43 | }, 44 | DynamoDBDocument: { 45 | Get: { 46 | Item: { 47 | url_path: '/out/v1/abcd/index.m3u8', 48 | id: '1', 49 | endpoint_hostname: 'https://aaaaaa.cloudfront.net', 50 | token_policy: { 51 | headers: [ 52 | 'user-agent' 53 | ], 54 | exc: [ 55 | '/ads/' 56 | ], 57 | nbf: '1645000000', 58 | session_auto_generate: 12, 59 | cty_fallback: true, 60 | paths: [ 61 | '/out/v1/abcd/' 62 | ], 63 | ip: false, 64 | cty: true, 65 | co_fallback: true, 66 | co: true, 67 | reg: true, 68 | exp: '+3h', 69 | ssn: true 70 | } 71 | } 72 | }, 73 | }, 74 | CloudFront: { 75 | DescribeFunction: { 76 | ARN: 'x', 77 | Name: 'my_secret', 78 | SecretString: '{"secret1_key_to_replace":"secret1_value_to_replace"}', 79 | }, 80 | }, 81 | SecretsManager: { 82 | GetSecretValue: { 83 | ARN: 'x', 84 | Name: 'my_secret', 85 | SecretString: '{"secret1_key_to_replace":"secret1_value_to_replace"}', 86 | } 87 | }, 88 | SSM: { 89 | PutParameter: { 90 | Parameter: 'abcd', 91 | }, 92 | }, 93 | WAFV2: { 94 | GetRuleGroup: { 95 | RuleGroup: { 96 | Name: 'MYDEMO1_BlockSessions', 97 | Id: 'ca2a976c-1df0-41b2-9234-055318508a9b', 98 | Capacity: 100, 99 | ARN: 'arn:aws:wafv2:myregion:xxccvvbb:global/rulegroup/MYDEMO1_BlockSessions/ca2a976c-1df0-41b2-9234-055318508a9b', 100 | Description: 'TokenRevoke', 101 | Rules: [{ 102 | Name: '91cb0cb58022fa04', 103 | Priority: 1, 104 | Statement: { 105 | ByteMatchStatement: { 106 | SearchString: { 107 | type: 'Buffer', 108 | data: [ 109 | 115, 110 | 101, 111 | 115, 112 | 115, 113 | 105, 114 | 111, 115 | 110, 116 | 105, 117 | 100, 118 | 49, 119 | ], 120 | }, 121 | FieldToMatch: { 122 | UriPath: {} 123 | }, 124 | TextTransformations: [ 125 | { 126 | Priority: 0, 127 | Type: 'NONE' 128 | } 129 | ], 130 | PositionalConstraint: 'STARTS_WITH' 131 | } 132 | }, 133 | Action: { 134 | Block: {} 135 | }, 136 | VisibilityConfig: { 137 | SampledRequestsEnabled: true, 138 | CloudWatchMetricsEnabled: true, 139 | MetricName: 'Example' 140 | } 141 | }], 142 | VisibilityConfig: { 143 | SampledRequestsEnabled: false, 144 | CloudWatchMetricsEnabled: false, 145 | MetricName: 'metricName' 146 | }, 147 | LabelNamespace: 'awswaf:xxccvvbb:rulegroup:MYDEMO1_BlockSessions:' 148 | }, 149 | LockToken: '1946fbfd-9677-41e7-8f8b-3c75192ac2e5' 150 | }, 151 | CreateRuleGroup: { 152 | Summary: { 153 | Id: 'abc', 154 | }, 155 | }, 156 | }, 157 | }; 158 | -------------------------------------------------------------------------------- /source/test/api.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { IConfiguration } from '../helpers/validators/configuration'; 6 | import { aws_dynamodb as dynamodb, Stack 7 | } from 'aws-cdk-lib'; 8 | import { Api } from '../lib/api/api'; 9 | import { Secrets } from '../lib/main/secrets'; 10 | import { CWDashboard } from '../lib/main/dashboard'; 11 | 12 | test('Create Api - demo=true', () => { 13 | const stack = new Stack(); 14 | // WHEN 15 | 16 | const secrets = new Secrets(stack, "Secrets"); 17 | const dashboard = new CWDashboard(stack, "CoreDashboard"); 18 | const myTable = new dynamodb.Table(stack, 'Table', { 19 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } 20 | }); 21 | const myConfig = { 22 | "main": { 23 | "stack_name": "MYSTREAM", 24 | "assets_bucket_name" : "MY_ASSETS_BUCKET_NAME", 25 | "rotate_secrets_frequency": "1m", 26 | "rotate_secrets_pattern": "P", 27 | "wcu": "100", 28 | "retention": "14" 29 | }, 30 | "api": { 31 | "demo": true 32 | 33 | }, 34 | "hls": { 35 | "hostname": "H", 36 | "url_path": "U", 37 | "ttl": "+3h" 38 | }, 39 | "dash": { 40 | "hostname": "H", 41 | "url_path": "U", 42 | "ttl": "+24h" 43 | } 44 | }; 45 | 46 | 47 | new Api(stack, 'Api', { 48 | configuration: myConfig as IConfiguration, 49 | secrets: secrets, 50 | dashboard: dashboard, 51 | sessionsTable: myTable, 52 | sig4LambdaVersionParamName: "sig4LambdaVersionParamName", 53 | sig4LambdaRoleArn: "sig4LambdaRoleArn" 54 | 55 | }) 56 | // THEN 57 | 58 | const template = Template.fromStack(stack); 59 | template.resourceCountIs("AWS::Lambda::LayerVersion", 2); 60 | template.resourceCountIs("AWS::DynamoDB::Table", 2); 61 | template.resourceCountIs("Custom::AWS", 2); 62 | template.resourceCountIs("AWS::Lambda::Function", 5); 63 | template.resourceCountIs("AWS::Logs::LogGroup", 4); 64 | 65 | 66 | 67 | 68 | }); 69 | 70 | test('Create Api - demo=false', () => { 71 | const stack = new Stack(); 72 | // WHEN 73 | 74 | const secrets = new Secrets(stack, "Secrets"); 75 | const dashboard = new CWDashboard(stack, "CoreDashboard"); 76 | const myTable = new dynamodb.Table(stack, 'Table', { 77 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } 78 | }); 79 | const myConfig = { 80 | "main": { 81 | "stack_name": "MYSTREAM", 82 | "assets_bucket_name" : "MY_ASSETS_BUCKET_NAME", 83 | "rotate_secrets_frequency": "1m", 84 | "rotate_secrets_pattern": "P", 85 | "wcu": "100", 86 | "retention": "14" 87 | }, 88 | "api": { 89 | "demo": false 90 | 91 | }, 92 | "hls": { 93 | "hostname": "H", 94 | "url_path": "U", 95 | "ttl": "+3h" 96 | }, 97 | "dash": { 98 | "hostname": "H", 99 | "url_path": "U", 100 | "ttl": "+24h" 101 | } 102 | }; 103 | 104 | 105 | new Api(stack, 'Api', { 106 | configuration: myConfig as IConfiguration, 107 | secrets: secrets, 108 | dashboard: dashboard, 109 | sessionsTable: myTable, 110 | sig4LambdaVersionParamName: "sig4LambdaVersionParamName", 111 | sig4LambdaRoleArn: "sig4LambdaRoleArn" 112 | 113 | }) 114 | // THEN 115 | 116 | const template = Template.fromStack(stack); 117 | template.resourceCountIs("AWS::Lambda::LayerVersion", 2); 118 | template.resourceCountIs("AWS::DynamoDB::Table", 2); 119 | template.resourceCountIs("Custom::AWS", 2); 120 | template.resourceCountIs("AWS::Lambda::Function", 5); 121 | template.resourceCountIs("AWS::Logs::LogGroup", 4); 122 | 123 | 124 | 125 | 126 | }); 127 | 128 | -------------------------------------------------------------------------------- /source/test/auto_revocation_workflow.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { 6 | aws_dynamodb as dynamodb, 7 | aws_s3 as s3, 8 | Stack 9 | } from "aws-cdk-lib"; 10 | import { AutoRevokeSessionsWorkflow } from '../lib/autorevocation/auto_revocation_workflow'; 11 | import { IConfiguration } from '../helpers/validators/configuration'; 12 | 13 | 14 | test('Auto revocation session', () => { 15 | const stack = new Stack(); 16 | // WHEN 17 | 18 | const myTable = new dynamodb.Table(stack, 'Table', { 19 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } 20 | }); 21 | 22 | const myConfig = { 23 | "main": { 24 | "stack_name": "MYSTACK", 25 | "wcu": "100", 26 | "retention": "14", 27 | "rotate_secrets_frequency": "m" 28 | }, 29 | "api": { 30 | "demo": false 31 | }, 32 | "sessionRevocation": { 33 | "trigger_workflow_frequency": 10, 34 | "db_name": "default", 35 | "table_name": "cloudfront_logs", 36 | "request_ip_column": "requestip", 37 | "ua_column_name": "useragent", 38 | "referer_column_name": "referrer", 39 | "uri_column_name": "uri", 40 | "status_column_name": "status", 41 | "response_bytes_column_name": "bytes", 42 | "date_column_name": "date", 43 | "time_column_name": "time", 44 | "lookback_period": 10, 45 | "ip_penalty": 1, 46 | "ip_rate": 1, 47 | "referer_penalty": 1, 48 | "ua_penalty": 1, 49 | "min_sessions_number": 3, 50 | "min_session_duration": 30, 51 | "score_threshold": 2.2, 52 | "partitioned": 0 53 | } 54 | }; 55 | 56 | new AutoRevokeSessionsWorkflow( 57 | stack, 58 | "GetSessions", 59 | { 60 | bucket: new s3.Bucket(stack, "SqlQuery"), 61 | dynamodbTable: myTable, 62 | configuration: myConfig as IConfiguration 63 | } 64 | ); 65 | 66 | // THEN 67 | 68 | const template = Template.fromStack(stack); 69 | template.resourceCountIs("AWS::Lambda::Function", 2); 70 | template.resourceCountIs("AWS::Logs::LogGroup", 3); 71 | template.resourceCountIs("AWS::StepFunctions::StateMachine", 1); 72 | 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /source/test/auto_session_revocation_stack.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { 6 | aws_dynamodb as dynamodb, 7 | aws_s3 as s3, 8 | Stack, App 9 | } from "aws-cdk-lib"; 10 | import { IConfiguration } from '../helpers/validators/configuration'; 11 | import { SecureMediaStreamingStack } from '../lib/secure_media_stream_stack'; 12 | import {getMainStackProps, getAutoSessionStackProps} from '../bin/secure_media_stream' 13 | import { AutoSessionRevocationStack } from '../lib/auto_revocation_stack'; 14 | 15 | test('Auto session revocation stack', () => { 16 | 17 | // WHEN 18 | const app = new App(); 19 | const config = { 20 | "main": { 21 | "stack_name": "MYSTACK", 22 | "wcu": "100", 23 | "retention": "14", 24 | "rotate_secrets_frequency": "m" 25 | }, 26 | "api": { 27 | "demo": false 28 | }, 29 | "sessionRevocation": { 30 | "trigger_workflow_frequency": 10, 31 | "db_name": "default", 32 | "table_name": "cloudfront_logs", 33 | "request_ip_column": "requestip", 34 | "ua_column_name": "useragent", 35 | "referer_column_name": "referrer", 36 | "uri_column_name": "uri", 37 | "status_column_name": "status", 38 | "response_bytes_column_name": "bytes", 39 | "date_column_name": "date", 40 | "time_column_name": "time", 41 | "lookback_period": 10, 42 | "ip_penalty": 1, 43 | "ip_rate": 1, 44 | "referer_penalty": 1, 45 | "ua_penalty": 1, 46 | "min_sessions_number": 3, 47 | "min_session_duration": 30, 48 | "score_threshold": 2.2, 49 | "partitioned": 0 50 | } 51 | } as IConfiguration; 52 | 53 | const coreStack = new SecureMediaStreamingStack( app, 54 | config.main.stack_name, 55 | config, 56 | getMainStackProps(config) 57 | ); 58 | 59 | 60 | const autoStack = new AutoSessionRevocationStack( 61 | app, 62 | config.main.stack_name + "AutoSessionRevocation", 63 | config, 64 | coreStack.sessionToRevoke, 65 | getAutoSessionStackProps() 66 | ); 67 | 68 | const autoTemplate = Template.fromStack(autoStack); 69 | 70 | autoTemplate.resourceCountIs("AWS::Lambda::Function", 4); 71 | autoTemplate.resourceCountIs("AWS::Logs::LogGroup", 4); 72 | autoTemplate.resourceCountIs("AWS::StepFunctions::StateMachine", 1); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /source/test/check_input_parameters.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { 6 | Stack 7 | } from "aws-cdk-lib"; 8 | import { GetInputParameters } from '../lib/cfn/check_input_parameters'; 9 | import { IConfiguration } from '../helpers/validators/configuration'; 10 | 11 | 12 | test('Check input param - cdk', () => { 13 | const stack = new Stack(); 14 | // WHEN 15 | 16 | const myConfig = { 17 | "main": { 18 | "stack_name": "MYSTACK", 19 | "wcu": "100", 20 | "retention": "14", 21 | "rotate_secrets_frequency": "m", 22 | "metrics": true 23 | }, 24 | "api": { 25 | "demo": false 26 | } 27 | }; 28 | 29 | new GetInputParameters(stack, "InputParameters", myConfig as IConfiguration); 30 | 31 | // THEN 32 | const template = Template.fromStack(stack); 33 | expect(Object.keys(template.toJSON().Parameters).length).toEqual(1); 34 | 35 | }); 36 | 37 | test('Check input param - cfn', () => { 38 | const stack = new Stack(); 39 | // WHEN 40 | 41 | const myConfig = { 42 | "main": { 43 | "stack_name": "MYSTREAM", 44 | "assets_bucket_name" : "MY_ASSETS_BUCKET_NAME", 45 | "rotate_secrets_frequency": "1m", 46 | "rotate_secrets_pattern": "P", 47 | "wcu": "100", 48 | "retention": "14", 49 | "metrics": true 50 | }, 51 | "api": { 52 | "demo": true 53 | 54 | }, 55 | "hls": { 56 | "hostname": "H", 57 | "url_path": "U", 58 | "ttl": "+3h" 59 | }, 60 | "dash": { 61 | "hostname": "H", 62 | "url_path": "U", 63 | "ttl": "+24h" 64 | } 65 | }; 66 | 67 | new GetInputParameters(stack, "InputParameters", myConfig as IConfiguration); 68 | 69 | // THEN 70 | const template = Template.fromStack(stack); 71 | expect(Object.keys(template.toJSON().Parameters).length).toEqual(13); 72 | }); 73 | 74 | test('Check input param - cdk hls & dash', () => { 75 | const stack = new Stack(); 76 | // WHEN 77 | 78 | const myConfig = { 79 | "main": { 80 | "stack_name": "MYSTREAM", 81 | "assets_bucket_name" : "MY_ASSETS_BUCKET_NAME", 82 | "rotate_secrets_frequency": "1m", 83 | "rotate_secrets_pattern": "m", 84 | "wcu": "100", 85 | "retention": "14", 86 | "metrics": true 87 | }, 88 | "api": { 89 | "demo": true 90 | 91 | }, 92 | "hls": { 93 | "hostname": "myhostname", 94 | "url_path": "mypath", 95 | "ttl": "+3h" 96 | }, 97 | "dash": { 98 | "hostname": "myhostname", 99 | "url_path": "mypath", 100 | "ttl": "+24h" 101 | } 102 | }; 103 | 104 | new GetInputParameters(stack, "InputParameters", myConfig as IConfiguration); 105 | 106 | // THEN 107 | const template = Template.fromStack(stack); 108 | console.log(template) 109 | expect(Object.keys(template.toJSON().Parameters).length).toEqual(1); 110 | }); 111 | 112 | -------------------------------------------------------------------------------- /source/test/check_token_cff.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const cff = require("../lambda/generate_secret_update_cff/cff.js"); 5 | 6 | const myMock = jest.spyOn(cff, "decodeString"); 7 | 8 | myMock.mockImplementation( param => { 9 | return Buffer.from(String(param), 'base64').toString(); 10 | }); 11 | 12 | describe("Check token", () => { 13 | 14 | 15 | test('Malformed token event', () => { 16 | // arrange and act 17 | var cffEvent = { 18 | "version":"1.0", 19 | "viewer":{ 20 | "ip":"MY_IP" 21 | }, 22 | "request":{ 23 | "method":"GET", 24 | "uri":"/MYSESSIONID.MY_JWT_TOKEN/out/v1/00c6ff982d404e2f940b48495b243b3c/index.m3u8", 25 | "headers":{ 26 | "host":{ 27 | "value":"dklf7fsi4gpzd.cloudfront.net" 28 | }, 29 | "user-agent":{ 30 | "value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0" 31 | }, 32 | "referer":{ 33 | "value":"https://d26xf765ycwwd4.cloudfront.net/" 34 | }, 35 | "origin":{ 36 | "value":"https://d26xf765ycwwd4.cloudfront.net" 37 | } 38 | } 39 | } 40 | }; 41 | var result = cff.handler(cffEvent); 42 | expect(result.statusCode).toBe(401); 43 | expect(result.statusDescription).toBe("Unauthorized"); 44 | }); 45 | 46 | test('Malformed token event - too many segments', () => { 47 | // arrange and act 48 | var cffEvent = { 49 | "version":"1.0", 50 | "viewer":{ 51 | "ip":"MY_IP" 52 | }, 53 | "request":{ 54 | "method":"GET", 55 | "uri":"/MYSESSIONID.MY_JWT_TOKEN.MY_JWT_TOKEN.MY_JWT_TOKEN/out/v1/00c6ff982d404e2f940b48495b243b3c/index.m3u8", 56 | "headers":{ 57 | "host":{ 58 | "value":"dklf7fsi4gpzd.cloudfront.net" 59 | }, 60 | "user-agent":{ 61 | "value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0" 62 | }, 63 | "referer":{ 64 | "value":"https://d26xf765ycwwd4.cloudfront.net/" 65 | }, 66 | "origin":{ 67 | "value":"https://d26xf765ycwwd4.cloudfront.net" 68 | } 69 | } 70 | } 71 | }; 72 | var result = cff.handler(cffEvent); 73 | expect(result.statusCode).toBe(401); 74 | expect(result.statusDescription).toBe("Unauthorized"); 75 | }); 76 | 77 | test('Malformed token headerSeg' , () => { 78 | // arrange and act 79 | var cffEvent = { 80 | "version": "1.0", 81 | "viewer": { 82 | "ip": "54.240.197.233" 83 | }, 84 | "request": { 85 | "method": "GET", 86 | "uri": "/abcde.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/out/v1/00c6ff982d404e2f940b48495b243b3c/index.m3u", 87 | "headers": { 88 | "host": { 89 | "value": "dklf7fsi4gpzd.cloudfront.net" 90 | }, 91 | "user-agent": { 92 | "value": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0' 93 | }, 94 | "referer": { 95 | "value": 'https://mycloudfrontdomainname.cloudfront.net' 96 | }, 97 | "origin": { 98 | "value": "https://d26xf765ycwwd4.cloudfront.net" 99 | } 100 | } 101 | } 102 | }; 103 | var result = cff.handler(cffEvent); 104 | expect(result.statusCode).toBe(401); 105 | expect(result.statusDescription).toBe("Unauthorized"); 106 | }); 107 | 108 | 109 | 110 | test('Expired token' , () => { 111 | 112 | var cffEvent = { 113 | "version": "1.0", 114 | "viewer": { 115 | "ip": "54.240.197.233" 116 | }, 117 | "request": { 118 | "method": "GET", 119 | "uri": "/cKyrFyOVnjza.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNlY3JldDFfa2V5X3RvX3JlcGxhY2UifQ.eyJpcCI6ZmFsc2UsImNvIjpmYWxzZSwiY3R5IjpmYWxzZSwicmVnIjpmYWxzZSwic3NuIjp0cnVlLCJleHAiOjE2NTgzMTg3ODQsImhlYWRlcnMiOlsidXNlci1hZ2VudCJdLCJxcyI6W10sImludHNpZyI6Il8wR205T01QUnpFS1JMUldTaVM5SVJ0Z3o5RHJSV3MyNjAwV3duQkllY0UiLCJwYXRocyI6WyIvb3V0L3YxLzAwYzZmZjk4MmQ0MDRlMmY5NDBiNDg0OTViMjQzYjNjLyJdLCJleGMiOlsiL2Fkcy8iXSwibmJmIjoxNjQ1MDAwMDAwLCJpYXQiOjE2NTgzMDc5ODR9.oPYrdIJbJkAQvw1ST87mgxrzLnzjEuH5ds3H9kl5upE/out/v1/00c6ff982d404e2f940b48495b243b3c/index.m3u", 120 | "headers": { 121 | "host": { 122 | "value": "dklf7fsi4gpzd.cloudfront.net" 123 | }, 124 | "user-agent": { 125 | "value": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0' 126 | }, 127 | "referer": { 128 | "value": 'https://mycloudfrontdomainname.cloudfront.net' 129 | }, 130 | "origin": { 131 | "value": "https://d26xf765ycwwd4.cloudfront.net" 132 | } 133 | } 134 | } 135 | }; 136 | var result = cff.handler(cffEvent); 137 | console.log(JSON.stringify(result)) 138 | expect(result.statusCode).toBe(401); 139 | expect(result.statusDescription).toBe("Unauthorized"); 140 | 141 | }); 142 | 143 | }); 144 | 145 | -------------------------------------------------------------------------------- /source/test/cr_create_le_rule.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { Stack 6 | } from 'aws-cdk-lib'; 7 | import { CRCreateLEWafRule } from '../lib/custom_resources/cr_create_le_rule'; 8 | 9 | test('Create L@E - DEPLOY_LE=true', () => { 10 | const stack = new Stack(); 11 | // WHEN 12 | 13 | new CRCreateLEWafRule(stack, 'Secrets', { 14 | WCU: "1", 15 | LAMBDA_EDGE_VERSION_SSM_PARAM: "LAMBDA_EDGE_VERSION_SSM_PARAM", 16 | WAF_RULE_NAME_SSM_PARAM: "WAF_RULE_NAME_SSM_PARAM", 17 | WAF_RULE_ID_SSM_PARAM: "WAF_RULE_ID_SSM_PARAM", 18 | DEPLOY_LE: true, 19 | METRICS: true, 20 | SOLUTION_IDENTIFIER: `AwsSolution/SO0195/1.0.1`, 21 | 22 | }) 23 | 24 | // THEN 25 | 26 | const template = Template.fromStack(stack); 27 | template.resourceCountIs("AWS::Lambda::Function", 2); 28 | 29 | 30 | }); 31 | 32 | test('Create L@E - DEPLOY_LE=false', () => { 33 | const stack = new Stack(); 34 | // WHEN 35 | 36 | new CRCreateLEWafRule(stack, 'Secrets', { 37 | WCU: "1", 38 | LAMBDA_EDGE_VERSION_SSM_PARAM: "LAMBDA_EDGE_VERSION_SSM_PARAM", 39 | WAF_RULE_NAME_SSM_PARAM: "WAF_RULE_NAME_SSM_PARAM", 40 | WAF_RULE_ID_SSM_PARAM: "WAF_RULE_ID_SSM_PARAM", 41 | DEPLOY_LE: false, 42 | METRICS: true, 43 | SOLUTION_IDENTIFIER: `AwsSolution/SO0195/1.0.1`, 44 | 45 | }) 46 | 47 | // THEN 48 | 49 | const template = Template.fromStack(stack); 50 | template.resourceCountIs("AWS::Lambda::Function", 2); 51 | 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /source/test/cr_init_secrets.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import * as cdk from 'aws-cdk-lib'; 6 | import { CrInitSecrets } from '../lib/custom_resources/cr_init_secrets'; 7 | 8 | test('Init secrets', () => { 9 | const stack = new cdk.Stack(); 10 | // WHEN 11 | new CrInitSecrets(stack, 'Secrets', { 12 | functionArn: "functionArn", 13 | functionName : "functionName" 14 | }) 15 | // THEN 16 | 17 | const template = Template.fromStack(stack); 18 | template.resourceCountIs("Custom::AWS", 1); 19 | }); 20 | -------------------------------------------------------------------------------- /source/test/cr_load_assets_table.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { CrLoadAssetsTable } from '../lib/custom_resources/cr_load_assets_table'; 6 | import { IConfiguration } from '../helpers/validators/configuration'; 7 | import { aws_dynamodb as dynamodb, Stack 8 | } from 'aws-cdk-lib'; 9 | 10 | test('Load assets', () => { 11 | const stack = new Stack(); 12 | // WHEN 13 | 14 | const myConfig = { 15 | "main": { 16 | "stack_name": "MYSTREAM", 17 | "assets_bucket_name" : "MY_ASSETS_BUCKET_NAME", 18 | "rotate_secrets_frequency": "1m", 19 | "rotate_secrets_pattern": "P", 20 | "wcu": "100", 21 | "retention": "14" 22 | }, 23 | "api": { 24 | "demo": true 25 | 26 | }, 27 | "hls": { 28 | "hostname": "H", 29 | "url_path": "U", 30 | "ttl": "+3h" 31 | }, 32 | "dash": { 33 | "hostname": "H", 34 | "url_path": "U", 35 | "ttl": "+24h" 36 | } 37 | }; 38 | const myTable = new dynamodb.Table(stack, 'Table', { 39 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } 40 | }); 41 | new CrLoadAssetsTable(stack, 'LoadAssetsTable', { 42 | table: myTable, 43 | configuration: myConfig as IConfiguration 44 | }) 45 | // THEN 46 | 47 | const template = Template.fromStack(stack); 48 | template.resourceCountIs("Custom::AWS", 1); 49 | }); 50 | -------------------------------------------------------------------------------- /source/test/cr_load_athena_config_table.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { CrLoadAssetsTable } from '../lib/custom_resources/cr_load_assets_table'; 6 | import { IConfiguration } from '../helpers/validators/configuration'; 7 | import { aws_dynamodb as dynamodb, Stack 8 | } from 'aws-cdk-lib'; 9 | import { CrLoadSqlParams } from '../lib/custom_resources/cr_load_athena_config_table'; 10 | 11 | test('Load athena config', () => { 12 | const stack = new Stack(); 13 | // WHEN 14 | 15 | const myTable = new dynamodb.Table(stack, 'Table', { 16 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING } 17 | }); 18 | 19 | const myConfig = { 20 | "main": { 21 | "stack_name": "MYSTACK", 22 | "wcu": "100", 23 | "retention": "14", 24 | "rotate_secrets_frequency": "m" 25 | }, 26 | "api": { 27 | "demo": false 28 | }, 29 | "sessionRevocation": { 30 | "trigger_workflow_frequency": 0, 31 | "db_name": "default", 32 | "table_name": "cloudfront_logs", 33 | "request_ip_column": "requestip", 34 | "ua_column_name": "useragent", 35 | "referer_column_name": "referrer", 36 | "uri_column_name": "uri", 37 | "status_column_name": "status", 38 | "response_bytes_column_name": "bytes", 39 | "date_column_name": "date", 40 | "time_column_name": "time", 41 | "lookback_period": 10, 42 | "ip_penalty": 1, 43 | "ip_rate": 1, 44 | "referer_penalty": 1, 45 | "ua_penalty": 1, 46 | "min_sessions_number": 3, 47 | "min_session_duration": 30, 48 | "score_threshold": 2.2, 49 | "partitioned": 0 50 | } 51 | }; 52 | 53 | 54 | new CrLoadSqlParams(stack, 'LoatAthenaConfig', { 55 | table: myTable, 56 | configuration: myConfig as IConfiguration 57 | }) 58 | // THEN 59 | 60 | const template = Template.fromStack(stack); 61 | template.resourceCountIs("Custom::AWS", 1); 62 | }); 63 | -------------------------------------------------------------------------------- /source/test/custom_resource_us_east_1.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /* This test fails locally unless you have access to tmp folder, so 5 | run unit tests script with sudo */ 6 | 7 | import fs from 'fs'; 8 | import AwsSdkMock from './__mocks__/aws-sdk-mock'; 9 | const cr = require('../lambda/custom_resource_us_east_1/index.js'); 10 | 11 | jest.spyOn(fs, 'copyFileSync').mockImplementation(() => { 12 | return ``; 13 | }); 14 | 15 | describe('Custom resource', () => { 16 | let mocks: any[] = []; 17 | const env = process.env 18 | 19 | beforeEach(() => { 20 | let data = "Mocked content of my file"; 21 | 22 | fs.writeFileSync("/tmp/le.js", data); 23 | 24 | mocks = AwsSdkMock.mockAllAWSClients(); 25 | 26 | process.env = { 27 | ROLE_ARN: "MyRoleArn", 28 | STACK_NAME: "MyStackName", 29 | LAMBDA_VERSION: "MyLambdaVersion", 30 | WCU: "100", 31 | RULE_ID: "MyRuleID", 32 | RULE_NAME: "MyRuleName", 33 | DEPLOY_LE: "1" 34 | }; 35 | }) 36 | 37 | afterEach(() => { 38 | process.env = env; 39 | AwsSdkMock.reseMocks(mocks); 40 | }) 41 | 42 | 43 | test('Deploy LE - result OK', async () => { 44 | 45 | var result = await cr.handler({}); 46 | 47 | expect(result).toHaveLength; 48 | }); 49 | 50 | test('Do not deploy LE - result OK', async () => { 51 | 52 | process.env = { 53 | ROLE_ARN: "MyRoleArn", 54 | STACK_NAME: "MyStackName", 55 | LAMBDA_VERSION: "MyLambdaVersion", 56 | WCU: "100", 57 | RULE_ID: "MyRuleID", 58 | RULE_NAME: "MyRuleName", 59 | DEPLOY_LE: "0" 60 | }; 61 | 62 | var result = await cr.handler({}); 63 | 64 | expect(result).toHaveLength; 65 | 66 | }); 67 | 68 | 69 | 70 | }) 71 | 72 | -------------------------------------------------------------------------------- /source/test/dashboard.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { 6 | aws_lambda as lambda, Stack 7 | } from "aws-cdk-lib"; 8 | import { CWDashboard } from '../lib/main/dashboard'; 9 | import { Aws } from 'aws-cdk-lib'; 10 | 11 | 12 | test('Create CW dashboard with widgets', () => { 13 | const stack = new Stack(); 14 | // WHEN 15 | const myDashboard = new CWDashboard(stack, 'Dashboard') 16 | 17 | 18 | const generateToken = new lambda.Function(stack, "GenerateToken", { 19 | functionName: Aws.STACK_NAME + "_GenerateToken", 20 | runtime: lambda.Runtime.NODEJS_18_X, 21 | code: lambda.Code.fromAsset("lambda/generate_token/nodejs"), 22 | handler: "index.handler", 23 | 24 | }); 25 | 26 | myDashboard.buildApiDashboard({ 27 | lambdaFunctionName: generateToken.functionName, 28 | region: Aws.REGION, 29 | }); 30 | 31 | myDashboard.buildApiDashboard({ 32 | lambdaFunctionName: generateToken.functionName, 33 | region: Aws.REGION, 34 | }); 35 | 36 | // THEN 37 | 38 | const template = Template.fromStack(stack); 39 | template.resourceCountIs("AWS::CloudWatch::Dashboard", 1); 40 | }); 41 | -------------------------------------------------------------------------------- /source/test/endpoints.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { 6 | aws_lambda as lambda, 7 | Stack 8 | } from "aws-cdk-lib"; 9 | import { Endpoints } from '../lib/api/endpoints'; 10 | 11 | 12 | test('Create endpoints - demoWebsite=true', () => { 13 | const stack = new Stack(); 14 | // WHEN 15 | 16 | const myLmbda = new lambda.Function(stack, "MyLambda", { 17 | runtime: lambda.Runtime.NODEJS_18_X, 18 | code: lambda.Code.fromAsset("lambda/generate_token/nodejs"), 19 | handler: "index.handler", 20 | }); 21 | 22 | new Endpoints(stack, "Endpoints", { 23 | generateTokenLambdaFunction: myLmbda, 24 | saveSessionToDDBLambdaFunction: myLmbda, 25 | updateTokenLambdaFunction: myLmbda, 26 | sig4LambdaVersionParamName: "sig4LambdaVersionParamName", 27 | sig4LambdaRoleArn: "sig4LambdaRoleArn", 28 | demo: true 29 | }); 30 | // THEN 31 | 32 | const template = Template.fromStack(stack); 33 | template.resourceCountIs("Custom::AWS", 1); 34 | template.resourceCountIs("AWS::CloudFront::Distribution", 1); 35 | template.resourceCountIs("AWS::ApiGatewayV2::Api", 1); 36 | template.resourceCountIs("AWS::S3::Bucket", 2); 37 | template.resourceCountIs("AWS::Logs::LogGroup", 1); 38 | 39 | 40 | 41 | 42 | }); 43 | 44 | test('Create endpoints - demoWebsite=false', () => { 45 | const stack = new Stack(); 46 | // WHEN 47 | 48 | const myLmbda = new lambda.Function(stack, "MyLambda", { 49 | runtime: lambda.Runtime.NODEJS_18_X, 50 | code: lambda.Code.fromAsset("lambda/generate_token/nodejs"), 51 | handler: "index.handler", 52 | }); 53 | 54 | new Endpoints(stack, "Endpoints", { 55 | generateTokenLambdaFunction: myLmbda, 56 | saveSessionToDDBLambdaFunction: myLmbda, 57 | updateTokenLambdaFunction: myLmbda, 58 | sig4LambdaVersionParamName: "sig4LambdaVersionParamName", 59 | sig4LambdaRoleArn: "sig4LambdaRoleArn", 60 | demo: false 61 | }); 62 | // THEN 63 | 64 | const template = Template.fromStack(stack); 65 | template.resourceCountIs("Custom::AWS", 1); 66 | template.resourceCountIs("AWS::CloudFront::Distribution", 1); 67 | template.resourceCountIs("AWS::ApiGatewayV2::Api", 1); 68 | template.resourceCountIs("AWS::S3::Bucket", 2); 69 | template.resourceCountIs("AWS::Logs::LogGroup", 1); 70 | 71 | 72 | 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /source/test/generate_token.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const generateTokenHandler = require('../lambda/generate_token/nodejs/index.js'); 5 | import awsSdkMock from "./__mocks__/aws-sdk-mock"; 6 | 7 | describe("Generate a token", () => { 8 | let mocks: any[] = []; 9 | 10 | beforeEach(() => { 11 | mocks = awsSdkMock.mockAllAWSClients(); 12 | }); 13 | 14 | afterEach(() => { 15 | awsSdkMock.reseMocks(mocks); 16 | }); 17 | 18 | test('generate token - result 200', async () => { 19 | // arrange and act 20 | var myEvent = { 21 | version: '2.0', 22 | routeKey: 'GET /tokengenerate', 23 | rawPath: '/tokengenerate', 24 | rawQueryString: 'id=1', 25 | headers: { 26 | authorization: 'AWS4-HMAC-SHA256 Credential=ASIA4T2JZHUEPDUT7YH4/20220708/eu-west-1/execute-api/aws4_request, SignedHeaders=host;x-amz-cf-id;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=5e5daea6b47e98ff3c3987c5de178837d2882f0ba8f21c7407cdd87ecba62370', 27 | 'cloudfront-viewer-address': '52.94.36.25:29281', 28 | 'cloudfront-viewer-country': 'GB', 29 | 'cloudfront-viewer-country-region': 'Ile_France', 30 | 'cloudfront-viewer-city': 'AA', 31 | 'content-length': '0', 32 | host: 'f2utpitubd.execute-api.eu-west-1.amazonaws.com', 33 | referer: 'https://d3pzxppvzp3dd9.cloudfront.net/', 34 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0', 35 | }, 36 | queryStringParameters: { id: '1' }, 37 | }; 38 | 39 | var result = await generateTokenHandler.handler(myEvent); 40 | 41 | expect(result.playback_url).toHaveLength; 42 | 43 | }); 44 | 45 | test('generate token - result 400', async () => { 46 | // arrange and act 47 | var myEvent = { 48 | version: '2.0', 49 | routeKey: 'GET /tokengenerate', 50 | rawPath: '/tokengenerate', 51 | headers: { 52 | authorization: 'AWS4-HMAC-SHA256 Credential=ASIA4T2JZHUEPDUT7YH4/20220708/eu-west-1/execute-api/aws4_request, SignedHeaders=host;x-amz-cf-id;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=5e5daea6b47e98ff3c3987c5de178837d2882f0ba8f21c7407cdd87ecba62370', 53 | 'cloudfront-viewer-address': '52.94.36.25:29281', 54 | 'cloudfront-viewer-country': 'GB', 55 | 'content-length': '0', 56 | host: 'f2utpitubd.execute-api.eu-west-1.amazonaws.com', 57 | referer: 'https://d3pzxppvzp3dd9.cloudfront.net/', 58 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0', 59 | }, 60 | }; 61 | 62 | var result = await generateTokenHandler.handler(myEvent); 63 | 64 | expect(result.statusCode).toBe(400); 65 | 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /source/test/generate_update_secrets.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const generateSecrets = require('../lambda/generate_secret_update_cff/index.js'); 5 | import awsSdkMock from './__mocks__/aws-sdk-mock'; 6 | 7 | import fs from 'fs'; 8 | 9 | jest.spyOn(fs, 'readFileSync').mockImplementation(() => { 10 | return ` 11 | var secrets = { "secret1_key_to_replace": "secret1_value_to_replace", "secret2_key_to_replace": "secret2_value_to_replace"}; 12 | function _base64urlDecode(str) { 13 | return exports.decodeString(str);//'exports' non supported by CFF. Only used to run unit tests. Removed before deployment. 14 | } 15 | 16 | `; 17 | }); 18 | 19 | describe('process.env', () => { 20 | const env = process.env; 21 | let mocks: any[] = []; 22 | 23 | beforeEach(() => { 24 | mocks = awsSdkMock.mockAllAWSClients(); 25 | process.env = { 26 | TEMPORARY_KEY_NAME: "MyTemporarySecretKey", 27 | PRIMARY_KEY_NAME: "MyPrimarySecretKey", 28 | SECONDARY_KEY_NAME: "MySecondarySecretKey", 29 | CFF_NAME: "MyCffName", 30 | }; 31 | }) 32 | 33 | afterEach(() => { 34 | process.env = env; 35 | awsSdkMock.reseMocks(mocks); 36 | }) 37 | 38 | 39 | 40 | test('Generate new temporary secret - result OK', async () => { 41 | 42 | var result = await generateSecrets.handler({}); 43 | expect(result).toEqual("OK"); 44 | 45 | }); 46 | 47 | 48 | test('Initialize all secrets - result OK', async () => { 49 | 50 | var result = await generateSecrets.handler({"initialize": true}); 51 | console.log("my result="+JSON.stringify(result)); 52 | expect(result).toEqual("OK"); 53 | 54 | }); 55 | 56 | 57 | 58 | 59 | 60 | }) 61 | 62 | -------------------------------------------------------------------------------- /source/test/prepare_query.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const sybmitQuery = require('../lambda/prepare_query/index.js'); 5 | 6 | describe('process.env', () => { 7 | const env = process.env 8 | 9 | beforeEach(() => { 10 | jest.resetModules(); 11 | 12 | process.env = { 13 | ip_penalty: '1', 14 | referer_penalty: '2', 15 | ua_penalty: '2', 16 | ip_rate: '2', 17 | uri_column_name: 'uri', 18 | referer_column_name: 'referer_column_name', 19 | ua_column_name: 'ua_column_name', 20 | request_ip_column: 'request_ip_column', 21 | status_column_name: 'status_column_name', 22 | response_bytes_column_name: 'response_bytes_column_name', 23 | date_column_name: 'date_column_name', 24 | time_column_name: 'time_column_name', 25 | db_name: 'env.db_name', 26 | table_name: 'table_name', 27 | min_sessions_number : '10', 28 | min_session_duration: '1', 29 | score_threshold: '2.2', 30 | partitioned: '1', 31 | lookback_period: '10' 32 | }; 33 | }) 34 | 35 | afterEach(() => { 36 | process.env = env 37 | }) 38 | 39 | test('Submit query- Same day - result OK', async () => { 40 | 41 | jest 42 | .useFakeTimers() 43 | .setSystemTime(new Date('2020-02-02 10:05:00')); 44 | 45 | var result = await sybmitQuery.handler({}); 46 | expect(result).toHaveLength; 47 | 48 | 49 | }); 50 | 51 | test('Submit query- Different day, same year - result OK', async () => { 52 | 53 | jest 54 | .useFakeTimers() 55 | .setSystemTime(new Date('2020-02-02 00:05:00')); 56 | 57 | var result = await sybmitQuery.handler({}); 58 | expect(result).toHaveLength; 59 | 60 | 61 | }); 62 | 63 | test('Submit query- Different day, same year, different month and different day - result OK', async () => { 64 | 65 | jest 66 | .useFakeTimers() 67 | .setSystemTime(new Date('2020-02-01 00:05:00')); 68 | 69 | var result = await sybmitQuery.handler({}); 70 | expect(result).toHaveLength; 71 | 72 | 73 | }); 74 | 75 | test('Submit query- Different years - result OK', async () => { 76 | 77 | jest 78 | .useFakeTimers() 79 | .setSystemTime(new Date('2020-01-01 00:05:00')); 80 | 81 | var result = await sybmitQuery.handler({}); 82 | expect(result).toHaveLength; 83 | 84 | 85 | }); 86 | 87 | test('Submit query- Different years, partitioned = 0 - result OK', async () => { 88 | 89 | jest 90 | .useFakeTimers() 91 | .setSystemTime(new Date('2020-01-01 00:05:00')); 92 | 93 | process.env = { 94 | ip_penalty: '1', 95 | referer_penalty: '2', 96 | ua_penalty: '2', 97 | ip_rate: '2', 98 | uri_column_name: 'uri', 99 | referer_column_name: 'referer_column_name', 100 | ua_column_name: 'ua_column_name', 101 | request_ip_column: 'request_ip_column', 102 | status_column_name: 'status_column_name', 103 | response_bytes_column_name: 'response_bytes_column_name', 104 | date_column_name: 'date_column_name', 105 | time_column_name: 'time_column_name', 106 | db_name: 'env.db_name', 107 | table_name: 'table_name', 108 | min_sessions_number : '10', 109 | min_session_duration: '1', 110 | score_threshold: '2.2', 111 | partitioned: '0', 112 | lookback_period: '10' 113 | }; 114 | 115 | var result = await sybmitQuery.handler({}); 116 | expect(result).toHaveLength; 117 | 118 | 119 | }); 120 | 121 | 122 | 123 | }) 124 | 125 | -------------------------------------------------------------------------------- /source/test/rotate_secrets_workflow.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { Aws, aws_cloudfront as cloudfront, Stack, 6 | } from 'aws-cdk-lib'; 7 | import { RotateSecretsWorkflow } from '../lib/main/rotate_secrets_workflow'; 8 | import { Secrets } from '../lib/main/secrets'; 9 | import { IConfiguration } from '../helpers/validators/configuration'; 10 | 11 | test('Rotate secrets workflow', () => { 12 | const stack = new Stack(); 13 | // WHEN 14 | const secrets = new Secrets(stack, "Secrets"); 15 | 16 | const checkToken = new cloudfront.Function(stack, "CheckJWTTokenFunction", { 17 | code: cloudfront.FunctionCode.fromFile({ 18 | filePath: "lambda/generate_secret_update_cff/index.js", 19 | }), 20 | functionName: Aws.STACK_NAME + "_checkJWTToken", 21 | comment: 22 | "CloudFront Function used to check a JWT token", 23 | }); 24 | 25 | const myConfig = { 26 | "main": { 27 | "stack_name": "CCCC1", 28 | "wcu": "100", 29 | "retention": "14", 30 | "rotate_secrets_frequency": "m" 31 | } 32 | }; 33 | new RotateSecretsWorkflow(stack, 'RotateSecrets', 34 | { 35 | secrets: secrets, 36 | checkTokenFunction: checkToken, 37 | configuration: myConfig as IConfiguration, 38 | }) 39 | // THEN 40 | 41 | const template = Template.fromStack(stack); 42 | template.resourceCountIs("AWS::StepFunctions::StateMachine", 1); 43 | template.resourceCountIs("AWS::Lambda::Function", 3); 44 | template.resourceCountIs("Custom::AWS", 1); 45 | template.resourceCountIs("AWS::Logs::LogGroup", 3); 46 | 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /source/test/save_auto_session.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const saveAutoSession = require('../lambda/save_auto_session/index.js'); 5 | import awsSdkMock from "./__mocks__/aws-sdk-mock"; 6 | 7 | describe('process.env', () => { 8 | const env = process.env; 9 | let mocks: any[] = []; 10 | 11 | beforeEach(() => { 12 | mocks = awsSdkMock.mockAllAWSClients(); 13 | process.env = { 14 | TTL: "2", 15 | TABLE_NAME: "myTable" 16 | }; 17 | }) 18 | 19 | afterEach(() => { 20 | process.env = env; 21 | awsSdkMock.reseMocks(mocks); 22 | }) 23 | 24 | test('Save auto session - non array event', async () => { 25 | 26 | try { 27 | var result = await saveAutoSession.handler({ 28 | "key1": "value1", 29 | "key2": "value2", 30 | "key3": "value3" 31 | }); 32 | 33 | } catch (e) { 34 | expect((e as Error).message).toBe("Event received must be an array with at least 2 elements"); 35 | } 36 | 37 | }); 38 | 39 | test('Save auto session - array event', async () => { 40 | 41 | var result = await saveAutoSession.handler( 42 | [ 43 | { 44 | "Data":[ 45 | { 46 | "VarCharValue":"zazz" 47 | }, 48 | 49 | ] 50 | }, 51 | { 52 | "Data":[ 53 | { 54 | "VarCharValue":"sessionid1" 55 | }, 56 | { 57 | "VarCharValue":"1234561" 58 | }, 59 | { 60 | "VarCharValue":"4567854" 61 | }, 62 | { 63 | "VarCharValue":"123332" 64 | }, 65 | { 66 | "VarCharValue":"456544" 67 | }, 68 | { 69 | "VarCharValue":"1111222" 70 | } 71 | ] 72 | } 73 | ] 74 | ); 75 | 76 | expect(result).toEqual("OK"); 77 | 78 | 79 | }); 80 | 81 | 82 | 83 | 84 | 85 | }) 86 | 87 | -------------------------------------------------------------------------------- /source/test/save_manual_session.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { afterEach } from "node:test"; 5 | import awsSdkMock from "./__mocks__/aws-sdk-mock"; 6 | const revokeSessionHandler = require('../lambda/save_manual_session/nodejs/index.js'); 7 | const awsSMD2 = require('../resources/sdk/node/v1/aws-secure-media-delivery.js'); 8 | awsSMD2.Session.initialize("MY_TABLE"); 9 | awsSMD2.Session.setDEBUG(true); 10 | 11 | describe("Save manual session", () => { 12 | let mocks: any[] = []; 13 | 14 | beforeEach(() => { 15 | mocks = awsSdkMock.mockAllAWSClients(); 16 | }); 17 | 18 | afterEach(() => { 19 | awsSdkMock.reseMocks(mocks); 20 | }) 21 | 22 | test('Save session - result 200', async () => { 23 | var myEvent = { 24 | version: '2.0', 25 | routeKey: 'GET /sessionrevoke', 26 | rawPath: '/sessionrevoke', 27 | rawQueryString: 'sessionid=abcdef', 28 | queryStringParameters: { sessionid: 'abcdef' }, 29 | }; 30 | 31 | var result = await revokeSessionHandler.handler(myEvent); 32 | 33 | expect(result.statusCode).toBe(200); 34 | 35 | }); 36 | 37 | test('Save session - result 400', async () => { 38 | var myEvent = { 39 | version: '2.0', 40 | routeKey: 'GET /sessionrevoke', 41 | }; 42 | 43 | var result = await revokeSessionHandler.handler(myEvent); 44 | 45 | expect(result.statusCode).toBe(400); 46 | 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /source/test/secrets.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import * as cdk from 'aws-cdk-lib'; 6 | import { Secrets } from '../lib/main/secrets'; 7 | 8 | test('Secrets Created', () => { 9 | const stack = new cdk.Stack(); 10 | // WHEN 11 | new Secrets(stack, 'Secrets') 12 | // THEN 13 | 14 | const template = Template.fromStack(stack); 15 | template.resourceCountIs("AWS::SecretsManager::Secret", 3); 16 | }); 17 | -------------------------------------------------------------------------------- /source/test/secure_media_delivery.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const awsSMD = require('../resources/sdk/node/v1/aws-secure-media-delivery.js'); 5 | import awsSdkMock from "./__mocks__/aws-sdk-mock"; 6 | 7 | awsSMD.Token.setDEBUG(true); 8 | awsSMD.Secret.setDEBUG(true); 9 | 10 | let secret = new awsSMD.Secret('MyStack', 4); 11 | secret.initSMClient(); 12 | let token = new awsSMD.Token(secret); 13 | 14 | 15 | describe("Check token generation", () => { 16 | let mocks: any[] = []; 17 | beforeEach(() => { 18 | mocks = awsSdkMock.mockAllAWSClients(); 19 | }); 20 | 21 | afterEach(() => { 22 | awsSdkMock.reseMocks(mocks); 23 | }); 24 | 25 | test("Without IP, token generated ", async () => { 26 | 27 | var viewer_attributes = { 28 | "ip": "192.168.1.1", 29 | "co": "FRANCE", 30 | "reg": "ILE DE FRANCE", 31 | "cty": "PARIS", 32 | "headers": { 33 | 'cloudfront-viewer-address': '54.240.197.233:31830', 34 | 'cloudfront-viewer-country': 'IE', 35 | 'content-length': '0', 36 | 'host': 'un25b5wnf5.execute-api.eu-west-1.amazonaws.com', 37 | 'referer': 'https://d19d5urzf66t53.cloudfront.net/', 38 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0', 39 | 'via': '1.1 f9e2b62bbab7f16f69e97695da81e608.cloudfront.net (CloudFront)', 40 | } 41 | }; 42 | var token_policy = 43 | { 44 | "co": false, 45 | "co_fallback": true, 46 | "cty": false, 47 | "cty_fallback": true, 48 | "exc": [ 49 | "/ads/" 50 | ], 51 | "exp": "+3h", 52 | "headers": [ 53 | "user-agent" 54 | ], 55 | "ip": false, 56 | "nbf": "1645000000", 57 | "paths": [ 58 | "/out/v1/00c6ff982d404e2f940b48495b243b3c/" 59 | ], 60 | "session_auto_generate": 12, 61 | "ssn": true 62 | }; 63 | 64 | const cloudfrontDomainName = "https://mycloudfrontdomainname.com"; 65 | const mediaUrl = "/out/v1/abcds/index.m3u"; 66 | const res = await token.generate(viewer_attributes, `${cloudfrontDomainName}${mediaUrl}`, token_policy); 67 | res.startsWith(cloudfrontDomainName) 68 | expect(res.startsWith(cloudfrontDomainName)).toBeTruthy(); 69 | expect(res.endsWith(mediaUrl)).toBeTruthy(); 70 | }, 70000); 71 | 72 | test("With IP, token generated", async () => { 73 | 74 | 75 | var viewer_attributes = { 76 | "ip": "192.168.1.1", 77 | "co": "FRANCE", 78 | "reg": "ILE DE FRANCE", 79 | "cty": "PARIS", 80 | "headers": { 81 | 'cloudfront-viewer-address': '54.240.197.233:31830', 82 | 'cloudfront-viewer-country': 'IE', 83 | 'content-length': '0', 84 | 'host': 'un25b5wnf5.execute-api.eu-west-1.amazonaws.com', 85 | 'referer': 'https://d19d5urzf66t53.cloudfront.net/', 86 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0', 87 | 'via': '1.1 f9e2b62bbab7f16f69e97695da81e608.cloudfront.net (CloudFront)', 88 | } 89 | }; 90 | var token_policy = 91 | { 92 | "co": false, 93 | "co_fallback": true, 94 | "cty": false, 95 | "cty_fallback": true, 96 | "exc": [ 97 | "/ads/" 98 | ], 99 | "exp": "+3h", 100 | "headers": [ 101 | "user-agent" 102 | ], 103 | "ip": true, 104 | "nbf": "1645000000", 105 | "paths": [ 106 | "/out/v1/00c6ff982d404e2f940b48495b243b3c/" 107 | ], 108 | "session_auto_generate": 12, 109 | "ssn": true 110 | }; 111 | 112 | const cloudfrontDomainName = "https://mycloudfrontdomainname.com"; 113 | const mediaUrl = "/out/v1/abcds/index.m3u"; 114 | const res = await token.generate(viewer_attributes, `${cloudfrontDomainName}${mediaUrl}`, token_policy); 115 | res.startsWith(cloudfrontDomainName) 116 | expect(res.startsWith(cloudfrontDomainName)).toBeTruthy(); 117 | expect(res.endsWith(mediaUrl)).toBeTruthy(); 118 | }, 70000); 119 | 120 | test("With country, token generated", async () => { 121 | 122 | 123 | var viewer_attributes = { 124 | "ip": "192.168.1.1", 125 | "co": "FRANCE", 126 | "reg": "ILE DE FRANCE", 127 | "cty": "PARIS", 128 | "headers": { 129 | 'cloudfront-viewer-address': '54.240.197.233:31830', 130 | 'cloudfront-viewer-country': 'IE', 131 | 'content-length': '0', 132 | 'host': 'un25b5wnf5.execute-api.eu-west-1.amazonaws.com', 133 | 'referer': 'https://d19d5urzf66t53.cloudfront.net/', 134 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0', 135 | 'via': '1.1 f9e2b62bbab7f16f69e97695da81e608.cloudfront.net (CloudFront)', 136 | } 137 | }; 138 | var token_policy = 139 | { 140 | "co": true, 141 | "co_fallback": true, 142 | "cty": false, 143 | "cty_fallback": true, 144 | "exc": [ 145 | "/ads/" 146 | ], 147 | "exp": "+3h", 148 | "headers": [ 149 | "user-agent" 150 | ], 151 | "ip": true, 152 | "nbf": "1645000000", 153 | "paths": [ 154 | "/out/v1/00c6ff982d404e2f940b48495b243b3c/" 155 | ], 156 | "session_auto_generate": 12, 157 | "ssn": true 158 | }; 159 | 160 | const cloudfrontDomainName = "https://mycloudfrontdomainname.com"; 161 | const mediaUrl = "/out/v1/abcds/index.m3u"; 162 | const res = await token.generate(viewer_attributes, `${cloudfrontDomainName}${mediaUrl}`, token_policy); 163 | res.startsWith(cloudfrontDomainName) 164 | expect(res.startsWith(cloudfrontDomainName)).toBeTruthy(); 165 | expect(res.endsWith(mediaUrl)).toBeTruthy(); 166 | }, 70000); 167 | 168 | 169 | 170 | }); 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /source/test/secure_media_stream_stack.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { 6 | App 7 | } from "aws-cdk-lib"; 8 | import { IConfiguration } from '../helpers/validators/configuration'; 9 | import { SecureMediaStreamingStack } from '../lib/secure_media_stream_stack'; 10 | import {getMainStackProps} from '../bin/secure_media_stream' 11 | import packageJson from '../package.json'; 12 | import cdkJson from '../cdk.json'; 13 | 14 | const packageVersion = packageJson.version; 15 | const cdkVersion = cdkJson.context.solution_version; 16 | 17 | test('Main stack', () => { 18 | 19 | // WHEN 20 | const app = new App(); 21 | const config = { 22 | "main": { 23 | "stack_name": "MYSTACK", 24 | "wcu": "100", 25 | "retention": "14", 26 | "rotate_secrets_frequency": "m" 27 | }, 28 | "api": { 29 | "demo": false 30 | }, 31 | "sessionRevocation": { 32 | "trigger_workflow_frequency": 10, 33 | "db_name": "default", 34 | "table_name": "cloudfront_logs", 35 | "request_ip_column": "requestip", 36 | "ua_column_name": "useragent", 37 | "referer_column_name": "referrer", 38 | "uri_column_name": "uri", 39 | "status_column_name": "status", 40 | "response_bytes_column_name": "bytes", 41 | "date_column_name": "date", 42 | "time_column_name": "time", 43 | "lookback_period": 10, 44 | "ip_penalty": 1, 45 | "ip_rate": 1, 46 | "referer_penalty": 1, 47 | "ua_penalty": 1, 48 | "min_sessions_number": 3, 49 | "min_session_duration": 30, 50 | "score_threshold": 2.2, 51 | "partitioned": 0 52 | } 53 | } as IConfiguration; 54 | const stack = new SecureMediaStreamingStack( app, 55 | config.main.stack_name, 56 | config, 57 | getMainStackProps(config) 58 | ); 59 | 60 | // THEN 61 | const template = Template.fromStack(stack); 62 | template.resourceCountIs("AWS::Lambda::Function", 10); 63 | template.resourceCountIs("AWS::Logs::LogGroup", 8); 64 | template.resourceCountIs("AWS::StepFunctions::StateMachine", 1); 65 | 66 | }); 67 | 68 | test('package.json and cdk.json solution versions should match', () => { 69 | expect(packageVersion).toEqual(cdkVersion); 70 | }); 71 | -------------------------------------------------------------------------------- /source/test/session_revocation.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Template } from 'aws-cdk-lib/assertions'; 5 | import { 6 | aws_dynamodb as dynamodb, 7 | Stack 8 | } from "aws-cdk-lib"; 9 | import { SessionRevocation } from '../lib/main/session_revocation'; 10 | import { IConfiguration } from '../helpers/validators/configuration'; 11 | 12 | 13 | test('Session revocation', () => { 14 | const stack = new Stack(); 15 | // WHEN 16 | 17 | const myTable = new dynamodb.Table(stack, 'Table', { 18 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, 19 | stream: dynamodb.StreamViewType.KEYS_ONLY, 20 | }); 21 | 22 | const config = { 23 | "main": { 24 | "stack_name": "MYSTACK", 25 | "wcu": "100", 26 | "retention": "14", 27 | "rotate_secrets_frequency": "m" 28 | } 29 | } as IConfiguration; 30 | 31 | new SessionRevocation(stack, "SessionRevocation", { 32 | sessionToRevoke: myTable, 33 | gsi_index_name: "GSI_NAME", 34 | ruleNameParamName: "WAF_RULE_NAME_SSM_PARAM", 35 | ruleIdParamName: "WAF_RULE_ID_SSM_PARAM", 36 | configuration: config 37 | }); 38 | 39 | // THEN 40 | 41 | const template = Template.fromStack(stack); 42 | template.resourceCountIs("Custom::AWS", 1); 43 | template.resourceCountIs("AWS::Lambda::Function", 2); 44 | template.resourceCountIs("AWS::Logs::LogGroup", 1); 45 | 46 | 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /source/test/sig4_lambda_edge.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const lambdaEdge = require('../lambda/custom_resource_us_east_1/le.js'); 5 | 6 | 7 | 8 | describe('Sign request', () => { 9 | 10 | const env = process.env 11 | 12 | beforeEach(() => { 13 | jest.resetModules() 14 | process.env = { 15 | AWS_ACCESS_KEY_ID : "MyAccessKey", 16 | AWS_SECRET_ACCESS_KEY: "MySecretKey", 17 | AWS_SESSION_TOKEN : "MySessionToken" 18 | }; 19 | }) 20 | 21 | afterEach(() => { 22 | process.env = env 23 | }) 24 | 25 | 26 | test('Sign sig4 request - result OK', async () => { 27 | var myEvent = { 28 | "Records": [ 29 | { 30 | "cf": { 31 | "config": { 32 | "distributionDomainName": "abcdefgh.cloudfront.net", 33 | "distributionId": "E1DX8M92DB5ED4", 34 | "eventType": "origin-request", 35 | "requestId": "DkgSZ2pqoTUDjmGml95zkDQ8PQU4hvfhu4S4aaG4YoFT1iDorhE84A==" 36 | }, 37 | "request": { 38 | "clientIp": "2a01:cb00:694:c800:6c3f:bb44:b79c:b3ac", 39 | "headers": { 40 | "host": [ 41 | { 42 | "key": "Host", 43 | "value": "abcdefgh.execute-api.us-east-1.amazonaws.com" 44 | } 45 | ], 46 | "x-forwarded-for": [ 47 | { 48 | "key": "X-Forwarded-For", 49 | "value": "2a01:cb00:694:c800:6c3f:bb44:b79c:b3ac" 50 | } 51 | ], 52 | "user-agent": [ 53 | { 54 | "key": "User-Agent", 55 | "value": "Amazon CloudFront" 56 | } 57 | ], 58 | "via": [ 59 | { 60 | "key": "Via", 61 | "value": "1.1 3a28bbccbd5f062ce989b39db1188300.cloudfront.net (CloudFront)" 62 | } 63 | ] 64 | }, 65 | "method": "GET", 66 | "origin": { 67 | "custom": { 68 | "customHeaders": {}, 69 | "domainName": "abcdefgh.execute-api.us-east-1.amazonaws.com", 70 | "keepaliveTimeout": 5, 71 | "path": "", 72 | "port": 443, 73 | "protocol": "https", 74 | "readTimeout": 30, 75 | "sslProtocols": [ 76 | "TLSv1.2" 77 | ] 78 | } 79 | }, 80 | "querystring": "", 81 | "uri": "/" 82 | } 83 | } 84 | } 85 | ] 86 | } 87 | var result = await lambdaEdge.handler(myEvent); 88 | expect(result).toHaveLength; 89 | 90 | }); 91 | 92 | 93 | 94 | }) 95 | 96 | -------------------------------------------------------------------------------- /source/test/swap_secrets.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const swapSecrets = require('../lambda/swap_secrets/index.js'); 5 | import awsSdkMock from "./__mocks__/aws-sdk-mock"; 6 | 7 | describe('process.env', () => { 8 | const env = process.env; 9 | let mocks: any[] = []; 10 | 11 | beforeEach(() => { 12 | mocks = awsSdkMock.mockAllAWSClients(); 13 | process.env = { 14 | TEMPORARY_KEY_NAME: "myTemporaryKey", 15 | PRIMARY_KEY_NAME: "myPrimaryKey", 16 | SECONDARY_KEY_NAME: "mySecondaryKey" 17 | }; 18 | }) 19 | 20 | afterEach(() => { 21 | process.env = env; 22 | awsSdkMock.reseMocks(mocks); 23 | }) 24 | 25 | test('swap secrets - result 200', async () => { 26 | 27 | var result = await swapSecrets.handler({ 28 | }); 29 | expect(result).toEqual("OK"); 30 | 31 | }); 32 | 33 | 34 | 35 | }) 36 | 37 | -------------------------------------------------------------------------------- /source/test/update_rulegroup.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const update_rulegroup = require('../lambda/update_rulegroup/index.js'); 5 | import awsSdkMock from "./__mocks__/aws-sdk-mock"; 6 | 7 | describe('process.env', () => { 8 | const env = process.env; 9 | let mocks: any[] = []; 10 | 11 | beforeEach(() => { 12 | mocks = awsSdkMock.mockAllAWSClients(); 13 | process.env = { 14 | RULE_ID: "ca2a976c-1df0-41b2-9234-055318508a9b", 15 | RULE_NAME: "MYDEMO1_BlockSessions", 16 | RETENTION: "10", 17 | TABLE_NAME: "myTableName", 18 | MAX_SESSIONS: "50", 19 | GSI_INDEX_NAME: "MyGsiIndex", 20 | }; 21 | }) 22 | 23 | afterEach(() => { 24 | process.env = env; 25 | awsSdkMock.reseMocks(mocks); 26 | }) 27 | 28 | 29 | 30 | test('Update rule group - Auto Session - result OK', async () => { 31 | 32 | var result = await update_rulegroup.handler({}); 33 | 34 | expect(result.statusCode).toEqual(200); 35 | 36 | 37 | 38 | }); 39 | 40 | 41 | 42 | 43 | 44 | }) 45 | 46 | -------------------------------------------------------------------------------- /source/test/update_token.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | const updateToken = require('../lambda/update_token/index.js'); 5 | import awsSdkMock from "./__mocks__/aws-sdk-mock"; 6 | 7 | describe('process.env', () => { 8 | const env = process.env; 9 | let mocks: any[] = []; 10 | beforeEach(() => { 11 | mocks = awsSdkMock.mockAllAWSClients(); 12 | process.env = { 13 | TABLE_NAME: "myTableName", 14 | }; 15 | }) 16 | 17 | afterEach(() => { 18 | process.env = env; 19 | awsSdkMock.reseMocks(mocks); 20 | }) 21 | 22 | test('Update token - result OK', async () => { 23 | const event = { 24 | "queryStringParameters":{ 25 | "id":"1", 26 | "ip":"0", 27 | "referer":"0", 28 | "ua":"0" 29 | }, 30 | } 31 | var result = await updateToken.handler(event); 32 | console.log(result) 33 | expect(result.statusCode).toEqual(200); 34 | 35 | 36 | 37 | }); 38 | 39 | 40 | 41 | 42 | 43 | }) 44 | 45 | -------------------------------------------------------------------------------- /source/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "outDir": "dist", 7 | "declaration": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization": false, 21 | "resolveJsonModule": true, 22 | "esModuleInterop": true, 23 | "typeRoots": ["./node_modules/@types"] 24 | }, 25 | "exclude": ["cdk.out", "node_modules", "dist"] 26 | } 27 | --------------------------------------------------------------------------------