Showing preview only (746K chars total). Download the full file or copy to clipboard to get everything.
Repository: aws-samples/aws-serverless-security-workshop
Branch: master
Commit: 4ec8f7471235
Files: 41
Total size: 720.5 KB
Directory structure:
gitextract_9dgerd3i/
├── .github/
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── LICENSE-SAMPLECODE
├── LICENSE-SUMMARY
├── README.md
├── bootstrap.sh
├── docs/
│ ├── 00-initial-setup/
│ │ └── README.md
│ ├── 01-add-authentication/
│ │ └── README.md
│ ├── 02-add-secrets-manager/
│ │ └── README.md
│ ├── 03-input-validation/
│ │ └── README.md
│ ├── 04-ssl-in-transit/
│ │ └── README.md
│ ├── 05-usage-plan/
│ │ └── README.md
│ ├── 06-waf/
│ │ └── README.md
│ ├── 07-dependency-vulnerability/
│ │ └── README.md
│ ├── 08-xray/
│ │ └── README.md
│ └── 10-resource-cleanup/
│ └── README.md
└── src/
├── apiclient/
│ ├── css/
│ │ ├── bootstrap.css
│ │ ├── main.css
│ │ ├── site.css
│ │ └── vendor/
│ │ └── bootstrap.css
│ ├── index.html
│ └── js/
│ └── main.js
├── app/
│ ├── assets/
│ │ └── rds-ca-2019-root.pem
│ ├── customUnicornAnalytics.js
│ ├── customizeUnicorn.js
│ ├── dbUtils.js
│ ├── httpUtil.js
│ ├── managePartners.js
│ ├── package.json
│ ├── permissions.js
│ └── unicornParts.js
├── authorizer/
│ ├── index.js
│ └── package.json
├── init/
│ ├── db/
│ │ └── queries.sql
│ └── init-template.yml
├── retired/
│ └── bootstrap.sh
├── template.yaml
└── test-events/
└── Customize_Unicorns.postman_collection.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
*Issue #, if available:*
*Description of changes:*
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# mac .DS_Store
.DS_Store
================================================
FILE: CODE_OF_CONDUCT.md
================================================
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.
================================================
FILE: CONTRIBUTING.md
================================================
# Guidelines for contributing
Thank you for your interest in contributing to AWS documentation! We greatly value feedback and contributions from our community.
Please read through this document before you submit any pull requests or issues. It will help us work together more effectively.
## What to expect when you contribute
When you submit a pull request, our team is notified and will respond as quickly as we can. We'll do our best to work with you to ensure that your pull request adheres to our style and standards. If we merge your pull request, we might make additional edits later for style or clarity.
The AWS documentation source files on GitHub aren't published directly to the official documentation website. If we merge your pull request, we'll publish your changes to the documentation website as soon as we can, but they won't appear immediately or automatically.
We look forward to receiving your pull requests for:
* New content you'd like to contribute (such as new code samples or tutorials)
* Inaccuracies in the content
* Information gaps in the content that need more detail to be complete
* Typos or grammatical errors
* Suggested rewrites that improve clarity and reduce confusion
**Note:** We all write differently, and you might not like how we've written or organized something currently. We want that feedback. But please be sure that your request for a rewrite is supported by the previous criteria. If it isn't, we might decline to merge it.
## How to contribute
To contribute, send us a pull request. For small changes, such as fixing a typo or adding a link, you can use the [GitHub Edit Button](https://blog.github.com/2011-04-26-forking-with-the-edit-button/). For larger changes:
1. [Fork the repository](https://help.github.com/articles/fork-a-repo/).
2. In your fork, make your change in a branch that's based on this repo's **master** branch.
3. Commit the change to your fork, using a clear and descriptive commit message.
4. [Create a pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/), answering any questions in the pull request form.
Before you send us a pull request, please be sure that:
1. You're working from the latest source on the **master** branch.
2. You check [existing open](https://github.com/awsdocs/aws-serverless-security-workshop/pulls), and [recently closed](https://github.com/awsdocs/aws-serverless-security-workshop/pulls?q=is%3Apr+is%3Aclosed), pull requests to be sure that someone else hasn't already addressed the problem.
3. You [create an issue](https://github.com/awsdocs/aws-serverless-security-workshop/issues/new) before working on a contribution that will take a significant amount of your time.
For contributions that will take a significant amount of time, [open a new issue](https://github.com/awsdocs/aws-serverless-security-workshop/issues/new) to pitch your idea before you get started. Explain the problem and describe the content you want to see added to the documentation. Let us know if you'll write it yourself or if you'd like us to help. We'll discuss your proposal with you and let you know whether we're likely to accept it. We don't want you to spend a lot of time on a contribution that might be outside the scope of the documentation or that's already in the works.
## Finding contributions to work on
If you'd like to contribute, but don't have a project in mind, look at the [open issues](https://github.com/awsdocs/aws-serverless-security-workshop/issues) in this repository for some ideas. Any issues with the [help wanted](https://github.com/awsdocs/aws-serverless-security-workshop/labels/help%20wanted) or [enhancement](https://github.com/awsdocs/aws-serverless-security-workshop/labels/enhancement) labels are a great place to start.
In addition to written content, we really appreciate new examples and code samples for our documentation, such as examples for different platforms or environments, and code samples in additional languages.
## Code of conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information, see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact [opensource-codeofconduct@amazon.com](mailto:opensource-codeofconduct@amazon.com) with any additional questions or comments.
## Security issue notifications
If you discover a potential security issue, please notify AWS Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public issue on GitHub.
## Licensing
See the [LICENSE](https://github.com/awsdocs/aws-serverless-security-workshop/blob/master/LICENSE) file for this project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
================================================
FILE: LICENSE
================================================
Creative Commons Attribution-ShareAlike 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
Section 1 – Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
Section 2 – Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
A. reproduce and Share the Licensed Material, in whole or in part; and
B. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. Downstream recipients.
A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
Section 3 – License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed Material:
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
Section 4 – Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
Section 5 – Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
Section 6 – Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 – Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
Section 8 – Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
================================================
FILE: LICENSE-SAMPLECODE
================================================
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: LICENSE-SUMMARY
================================================
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
The documentation is made available under the Creative Commons Attribution-ShareAlike 4.0 International License. See the LICENSE file.
The sample code within this documentation is made available under a modified MIT license. See the LICENSE-SAMPLECODE file.
================================================
FILE: README.md
================================================
# Serverless Security Workshop
> **WARNING**: The purpose of the workshop is to provide a starter API which **does NOT follow many security best practices** on purpose. The tutorial modules guide you to identify security gaps in the starter app, and implement protection measures for them.
>
> Furthermore, the modules **do not cover ALL** the security measures that should be applied. After completing all modules, we recommend you to explore additional protections, such as ensuring the principle of least privilege. See the **Extra Credit** section for more details.
In this workshop, you will learn techniques to secure a serverless application built with AWS Lambda, Amazon API Gateway and RDS Aurora. We will cover AWS services and features you can leverage to improve the security of a serverless applications in 5 domains:
1. identity & access management
1. infrastructure
1. data
1. code
1. logging & monitoring
## Getting Started
Workshop URL: [serverless-security-workshop](https://catalog.us-east-1.prod.workshops.aws/workshops/026f84fd-f589-4a59-a4d1-81dc543fcd30)
## License Summary
The documentation is made available under the Creative Commons Attribution-ShareAlike 4.0 International License. See the LICENSE file.
The sample
within this documentation is made available under a modified MIT license. See the LICENSE-SAMPLECODE file.
================================================
FILE: bootstrap.sh
================================================
#!/bin/bash
# Cloud9 Bootstrap Script
# updated 12/6/2022
# Tested on Amazon Linux 2
# Checks for AWS Event or Cloudformation setup
# 1. Installs JQ
# 2. Creates Environment Variables
# 3. NPM Installs and Deploys Application
#
# Usually takes less than one minute to complete
set -euxo pipefail
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
function _logger() {
echo -e "$(date) ${YELLOW}[*] $@ ${NC}"
}
function install_utility_tools() {
_logger "[+] Installing jq"
sudo yum install -y jq
}
function setstackname() {
_logger "[+] Setting StackName"
export stack_name=$(aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE --query 'StackSummaries[].StackName'| grep 'mod\|"Secure-Serverless"' | sed 's/[",\,]//g')
if [ "$stack_name" = "" ];
then
echo "Stack Set missing. Check out running the stack set in the instructions."
exit 0
else
echo $stack_name
fi
}
function setclustername() {
_logger "[+] Setting Auora Cluster name"
sed -i "s/secure-aurora-cluster.cluster-xxxxxxx.xxxxxxx.rds.amazonaws.com/$AuroraEndpoint/g" /Workshop/src/app/dbUtils.js
}
function setregion() {
_logger "[+] Setting region"
#echo export REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]') >> ~/.bashrc
##echo export "REGION=$(curl --silent http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region)" >> ~/.bashrc
export "REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]')" >> ~/.bashrc
echo "REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]')" >>/Workshop/scratch.txt
}
function checkfile(){
#check for file
export FILE=/Workshop/src/app/dbUtils.js
if [ -f $FILE ];
then
echo "Files cloned from Git!"
else
echo "Missing files. Please be sure to clone the file from Git: git clone https://github.com/aws-samples/aws-serverless-security-workshop.git"
exit 0
fi
}
function setcfoutput() {
# load outputs to env vars
_logger "[+] get Cloudformation outputs and set variables"
for output in $(aws cloudformation describe-stacks --stack-name $stack_name --query 'Stacks[].Outputs[].OutputKey' --output text)
do
export $output=$(aws cloudformation describe-stacks --stack-name $stack_name --query 'Stacks[].Outputs[?OutputKey==`'$output'`].OutputValue' --output text)
echo "$output=$(aws cloudformation describe-stacks --stack-name $stack_name --query 'Stacks[].Outputs[?OutputKey==`'$output'`].OutputValue' --output text)" >> ~/.bashrc
echo "$output=$(aws cloudformation describe-stacks --stack-name $stack_name --query 'Stacks[].Outputs[?OutputKey==`'$output'`].OutputValue' --output text)" >> /Workshop/scratch.txt
#eval "echo $output : \"\$$output\""
done
}
function deployapp() {
_logger "[+] Deploying app"
cd /Workshop/src/app
export UV_USE_IO_URING=0
npm install
cd /Workshop/src
sam deploy --stack-name CustomizeUnicorns --s3-bucket $DeploymentS3Bucket --region $REGION --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND || true
cd /Workshop/
}
function getapiurl(){
sam_stack_name="CustomizeUnicorns"
echo " " >> /Workshop/scratch.txt
echo "-------------------------------------------" >> /Workshop/scratch.txt
echo "API Gateway URL:" >> /Workshop/scratch.txt
echo "$(aws cloudformation describe-stacks --stack-name $sam_stack_name --query 'Stacks[].Outputs[].OutputValue' --output text)" >> /Workshop/scratch.txt
}
function changeOwner() {
_logger "[+] Changing owner of /Workshop"
sudo chown -R ec2-user:ec2-user /Workshop
}
function main() {
install_utility_tools
checkfile
setstackname
setcfoutput
setclustername
setregion
deployapp
getapiurl
changeOwner
exec ${SHELL}
}
main
================================================
FILE: docs/00-initial-setup/README.md
================================================
# Module 0: Initial Setup
In this set up module, you will deploy a simple serverless application, which you will learn to secure in the following modules. You will create an REST API endpoint so partner companies of Wild Rydes can submit unicorn customizations such as branded socks and capes to advertise their company. Below is a high level architecture of what you will be deploying:

## Prerequisites
If you are completing this workshop at an AWS-sponsored event where an AWS account is provided for you, you will be using **AWS Event Engine**. In this case, the prerequisites is already met and you can move on to next step.
If you not not using AWS Event Engine, expand below to see prerequisites:
<details>
<summary><strong> Prerequisites if you are not using AWS Event Engine </strong></summary><p>
### AWS Account
In order to complete this workshop, you'll need an AWS account and access to create and manage the AWS resources that are used in this workshop, including Cloud9, Cognito, API Gateway, Lambda, RDS, WAF, Secrets Manager, and IAM policies and roles.
The code and instructions in this workshop assume only one participant is using a given AWS account at a time. If you attempt sharing an account with another participant, you may encounter naming conflicts for certain resources. You can work around this by using distinct Regions, but the instructions do not provide details on the changes required to make this work.
Please make sure not to use a production AWS environment or account for this workshop. It is recommended to instead use a **development account** which provides **full access** to the necessary services so that you do not run into permissions issues.
### Region Selection
Use a single region for the entirety of this workshop. This workshop supports two regions in North America and 1 region in Europe. Choose one region from the launch stack links below and continue to use that region for all of the workshop activities.
</details>
## Module-0A: Create a VPC and Cloud9 environment required for this workshop
A VPC is required for our workshop so we can:
* Leverage a Cloud9 environment as our IDE (integrated development environment)
* Use an RDS Aurora MySQL database as the backend database for our serverless application.
A CloudFormation setup has been prepared to spin up these resources:
* A **VPC** with 4 subnets, 2 private and 2 public.
* A **Cloud9** environment where you will be developing and launching the rest of the workshop resources from.
* A **MySQL Aurora RDS database** (the primary DB instance may reside in either of the 2 private subnets)

In addition, it also creates the below resources
* A **S3 bucket** you will later use for packaging and uploading lambda function code
* A **Security Group** that will be used by the lambda functions
**Choose and click on the option below according to your situation and follow its instructions:**
If you are completing this workshop at an AWS-sponsored event where an AWS account is provided for you, you will be using **AWS Event Engine**. Choose **Option 1** below. Otherwise, choose **Option 2**.
<details>
<summary><strong> Option 1: If you are using AWS Event Engine </strong></summary><p>
If you are using AWS Event Engine, an AWS CloudFormation stack should be automatically created for you.
1. Go to [https://dashboard.eventengine.run](https://dashboard.eventengine.run)
1. In the next screen, put in the hash code you received from the event organizer, and click **Proceed**

1. Log into the the AWS console in the event engine account by clicking on **AWS Console**

1. Click on **Open AWS Console** or use the **Copy Login Link** button and open the copied URL in **Chrome** or **Firefox**

1. Type in `CloudFormation` in the **Find Services** search bar to go to the CloudFormation console
1. You should see 2 stacks that have been created:
* one named something like `mod-3269ecbd5edf43ac` This is the ***main setup stack*** containing the setup resources.
* one with name similar to `aws-cloud9-Secure-Serverless-Cloud9-<alphanumeric-letters>`. This is a nested stack responsible for creating the Cloud9 environment.
1. Select the ***main setup stack*** (name starting with `mod-`), go to the **Outputs** tab. Keep this browser tab open as you go through rest of the workshop.

</details>
<details>
<summary><strong> Option 2: If you are working in your own AWS account</strong></summary><p>
If you are working in your own AWS account, follow the steps below to launch a CloudFormation template that will set up initial resources for you
1. Select the desired region. Since we are going to use services like Aurora or Cloud9, please choose one of these following and click the corresponding **Launch Stack** link
💡 **When clicking on any link in this instruction, hold the ⌘ (mac) or Ctrl (Windows) so the links open in a new tab** 💡
Region| Code | Launch
------|------|-------
EU (Ireland) | <span style="font-family:'Courier';">eu-west-1</span> | [](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=Secure-Serverless&templateURL=https://s3.amazonaws.com/wildrydes-us-east-1/Security/init-template.yml)
US West (Oregon) | <span style="font-family:'Courier';">us-west-2</span> | [](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=Secure-Serverless&templateURL=https://s3.amazonaws.com/wildrydes-us-east-1/Security/init-template.yml)
US East (N. Virginia) | <span style="font-family:'Courier';">us-east-1</span> | [](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=Secure-Serverless&templateURL=https://s3.amazonaws.com/wildrydes-us-east-1/Security/init-template.yml)
1. Click **Next**
1. In the **Step 2: Specify stack details** page:
* name you stack ***`Secure-Serverless`***
* for the database password, use ***`Corp123!`***
and click **Next**
> Note: you can specify a different password here if you prefer. However, the password must be at least 8 character long. And if you do this, you would later need to change the lambda function code in module-0D to use the password you specified in the `src/app/dbUtils.js` file.
1. In the **Step 3:Configure stack options** page, accept the default configurations and click **Next**
1. Review the configuration and click **Create stack**
1. While you are waiting for the completion of the CloudFormation stack creation, check if you have **PostMan** installed on your laptop. If not, download and install it at: [https://www.getpostman.com](https://www.getpostman.com), we will need to use it later.
1. It will take a few minutes for the Stack to create. Choose the **Stack Info** tab to go to the overall stack status page and wait until the stack is fully launched and shows a status of *CREATE_COMPLETE*. Click the refresh icon periodically to see progress update.
> Note: When you launch the stack, CloudFormation deploys a nested CloudFormation stack to launch the Cloud9 resources. You can safely ignore that template which is prefixed with "aws-cloud9-Secure-Serverless-".
1. Once the CloudFormation creation completes, go to the **Outputs** tab and copy the **AuroraEndpoint** to a text editor. You will need it to connect to the Aurora database in the next step. (**Keeping this browser tab open throughout this workshop is also highly recommended**)

</details>
## Module-0B: Access Cloud9
As part of the above step, an [Cloud9 IDE instance](https://aws.amazon.com/cloud9/) is created. All of the coding and commands in this workshop should be run inside the Cloud9 IDE environment.
1. Open a new browser tab and go to the Cloud9 console: `https://console.aws.amazon.com/cloud9/home` (You can also find the Cloud9 console in the AWS console by clicking on **Services** in the navigation bar on the top, and search for `cloud9` and enter)
1. Click on ***Your environments*** (you may need to expand the left sidebar)
<img src="images/0B-cloud9-environments.png" width="80%" />
1. Under the *Secure-Serverless-Cloud9* environment, click on ***Open IDE***

If you have trouble opening cloud9, ensure you are using:
* Either **Chrome** or **Firefox** browser
* Refer to the troubleshooting guide [**here**](https://docs.aws.amazon.com/cloud9/latest/user-guide/troubleshooting.html#troubleshooting-env-loading) to ensure third-party cookies is enabled
1. You should now see an integrated development environment (IDE) environment as shown below. AWS Cloud9 is a cloud-based IDE that lets you write, run, and debug your code with just a browser. You can run shell commands in the terminal section just like you would on your local computers

Keep your AWS Cloud9 IDE opened in a tab throughout this workshop as you'll be using it for most all activities.
1. We need to get the content of this workshop in this environment. In the Cloud9 terminal window, run the following command to clone this repository (bottom of the page):
`git clone https://github.com/aws-samples/aws-serverless-security-workshop.git`

:bulb:**Tip:** Keep an open scratch pad in Cloud9 for notes on resource IDs, etc. that you will need for future steps:
1. Create a new file in Cloud9

1. Copy/paste the resource IDs from the browser tab with the CloudFormation console open, copy the content under **Outputs**, and save it as `scratch.txt`

## Module-0C: Prepare your database
We need to create some tables and insert some initial values to the Aurora database. In Module-0A, a Aurora database is setup in private subnet so the database is not reachable directly from the Internet.
Because your Cloud9 instance and the Aurora database is in the same VPC, you can administer the database from the Cloud9 instance (The security group of the database the have been configured to allow the traffic):
To initialize your database:
1. In the cloud9 terminal window, go into the folder of the repo:
```
cd aws-serverless-security-workshop/
```

1. Connect to your cluster with the following command. Replace the Aurora endpoint with the one you copied into your scratch pad.
`mysql -h <YOUR-AURORA-SERVERLESS-ENDPOINT> -u admin -p`
You should be prompted with a password. Use *`Corp123!`* (If during Module-0A, you customized the password to something else, use the one you specified).
1. Within the mysql command prompt (`mysql> `), enter the following command:
`source src/init/db/queries.sql`
You should see an output such as this:
``` bash
mysql> source src/init/db/queries.sql
Query OK, 1 row affected (0.01 sec)
Database changed
Query OK, 0 rows affected (0.02 sec)
Query OK, 0 rows affected (0.02 sec)
Query OK, 0 rows affected (0.02 sec)
Query OK, 0 rows affected (0.02 sec)
Query OK, 0 rows affected (0.02 sec)
Query OK, 0 rows affected (0.03 sec)
Query OK, 1 row affected, 1 warning (0.00 sec)
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
Query OK, 8 rows affected (0.01 sec)
Records: 8 Duplicates: 0 Warnings: 0
Query OK, 7 rows affected (0.00 sec)
Records: 7 Duplicates: 0 Warnings: 0
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql>
```
1. You can explore the database tables created by running the following SQL query:
```sql
SHOW tables;
```
You should see something like this
```sql
mysql> SHOW tables;
+---------------------------------+
| Tables_in_unicorn_customization |
+---------------------------------+
| Capes |
| Companies |
| Custom_Unicorns |
| Glasses |
| Horns |
| Socks |
+---------------------------------+
6 rows in set (0.00 sec)
```
Explore the content of the tables using
```sql
SELECT * FROM Capes;
```
You should see something like this
```sql
mysql> SELECT * FROM Capes;
+----+--------------------+-------+
| ID | NAME | PRICE |
+----+--------------------+-------+
| 1 | White | 0.00 |
| 2 | Rainbow | 2.00 |
| 3 | Branded on White | 3.00 |
| 4 | Branded on Rainbow | 4.00 |
+----+--------------------+-------+
4 rows in set (0.00 sec)
```
1. After that, you can use the command `exit` to drop the mysql connection.
## Module-0D: The starting code for the serverless application
The code for the lambda functions resides within the path `aws-serverless-security-workshop/src/app`. The first thing you need to do is install node dependencies by navigating to this folder and using the following command:
```sh
$ cd ~/environment/aws-serverless-security-workshop/src/app
$ npm install
```
> Note: If you see this warning
>
> <img src="images/0D-vulnerability.png" width="65%"/>
>
> Don't worry. We will be addressing the dependency vulnerability in [**module 7**](../07-dependency-vulnerability/README.md) :)
The `src/app` folder has a few files:
- **unicornParts.js**: Main file for the lambda function that lists unicorn customization options.
- **customizeUnicorn.js**: Main file for the lambda function that handles the create/describe/delete operations for a unicorn customization configuration.
- **dbUtils.js**: This file contains all the database/query logic of the application. It also contains all the connection requirements in plain text (that's suspicious!)
Review them by navigating the file explorer sidebar in Cloud9:

In addition, these additional files reside in the folder. No need to review them closely at this point:
- **httpUtils.js**: This file contains the http response logic from your application.
- **managePartners.js**: Main file for the lambda function that handles the logic to register a new partner company. We will go into details on this one in Module 1.
- **package.json**: Nodejs project manifest, including listing dependencies of the code
In addition to the lambda code, the configurations for Lambda function and the REST APIs are spelled out in `template.yaml` as a **AWS SAM** (Serverless Application Model) template.
[AWS SAM](https://github.com/awslabs/serverless-application-model) allows you to define serverless applications in simple and clean syntax. In the `template.yaml`, you can see we have defined 3 lambda functions, and it maps to a set of REST APIs defined in a Swagger template:
<table>
<tr>
<th>Lambda Function</th>
<th>Main handler code</th>
<th>API resource</th>
<th>HTTP Verb</th>
<th>Description</th>
</tr>
<tr>
<td rowspan="4">UnicornPartsFunction</td>
<td rowspan="4">unicornParts.js</td>
<td>/horns</td>
<td>GET</td>
<td>List customization options for horns</td>
</tr>
<tr>
<td>/glasses</td>
<td>GET</td>
<td>List customization options for glasses</td>
</tr>
<tr>
<td>/socks</td>
<td>GET</td>
<td>List customization options for socks</td>
</tr>
<tr>
<td>/capes</td>
<td>GET</td>
<td>List customization options for capes</td>
</tr>
<tr>
<td rowspan="4">CustomizeUnicornFunction</td>
<td rowspan="4">customizeUnicorn.js</td>
<td>/customizations</td>
<td>POST</td>
<td>Create unicorn customization</td>
</tr>
<tr>
<td>/customizations</td>
<td>GET</td>
<td>List unicorn customization</td>
</tr>
<tr>
<td>/customizations/{id}</td>
<td>GET</td>
<td>Describe a unicorn customization</td>
</tr>
<tr>
<td>/customizations/{id}</td>
<td>DELETE</td>
<td>Delete a unicorn customization</td>
</tr>
<tr>
<td>ManagePartnerFunction</td>
<td>managePartners.js</td>
<td>/partners</td>
<td>POST</td>
<td>Register a new partner company</td>
</tr>
</table>
## Module-0E: Run your serverless application locally with SAM Local
1. After reviewing the code, under **src/app/dbUtils.js**, replace the *host* with the Aurora endpoint. Then save the file (⌘+s for Mac or Ctrl+s for Windows or File -> Save)
<img src="images/0D-db-endpoint-in-code.png" width="70%" />
:bulb: when you have unsaved changes in a file, cloud9 will show a grey dot next to the file name:
<img src="images/0E-unsaved.png" width="50%" />
When you successfully save the changes, the dot will turn green and then disappear.
After doing this, it's time to test your API locally using SAM Local.
1. On the **right panel**, click on **AWS Resources**.
<img src="images/0D-aws-resource-bar.png" width="80%" />
1. You should see a folder tree with the name *Local Functions (1)*.
1. Select **UnicornPartsFunction** under the `src` folder
1. Once you have selected the function, click on the dropdown on the panel on the top, and select **Run APIGateway Local**
<img src="images/0D-run-apigateway-local.png" width="40%" />
1. Then, click on the play icon. You will get a new panel to test the API locally.
1. In the **Path** parameter of this new panel, you should see it filled as `/socks`. If not, pick any of the unicorn parts (e.g `/socks`, `/glasses`, `/capes`, `/horns`) and click **Run**.
> The first time you test the API locally, it could take up to 1-2 minutes to fully initialize due to Docker being setup with a Docker image being pulled down.
You should be able to get a `200 OK` response with values back for the body part you queried.
Example screenshot:

This indicates that the application run successfully within your Cloud9 environment (locally). Now it's time to deploy your Serverless application!
## Module-0F: Deploy and test your Serverless application in the cloud
1. Retrieve the name of the S3 bucket the CloudFormation stack has created earlier:
* If you copied the CloudFormation output content in the cloud9 scratch pad, find the value of **DeploymentS3Bucket**

* Otherwise, find the value of **DeploymentS3Bucket** from the Cloudformation console **Output** tab

1. In the terminal, set the bash variables:
```
REGION=`ec2-metadata -z | awk '{print $2}' | sed 's/[a-z]$//'`
BUCKET=<use the DeploymentS3Bucket from the CloudFormation output>
```
1. Ensure you are in the `src` folder:
```
cd ~/environment/aws-serverless-security-workshop/src
```
1. Run the following to package up the lambda code and upload it to S3, and update the CloudFormation template to reference the S3 paths that hosts the code:
```
aws cloudformation package --template-file template.yaml --s3-bucket $BUCKET --output-template packaged.yaml
```
1. Deploy the serverless API using the following command. Note that this template references the output from the setup CloudFormation stack (`Secure-Serverless`) for things like subnet IDs.
```
aws cloudformation deploy --template-file packaged.yaml --stack-name CustomizeUnicorns --region $REGION --capabilities CAPABILITY_IAM --parameter-overrides InitResourceStack=Secure-Serverless
```
1. Wait until you see the stack is successfully deployed:
```
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - CustomizeUnicorns
```
1. You can gather the base endpoint of the serverless API we just deployed from the output of the CloudFormation stack.
To do it from commandline:
```
aws cloudformation describe-stacks --region $REGION --stack-name CustomizeUnicorns --query "Stacks[0].Outputs[0].OutputValue" --output text
```
e.g.
```
$ aws cloudformation describe-stacks --region $REGION --stack-name CustomizeUnicorns --query "Stacks[0].Outputs[0].OutputValue" --output text
https://rs86gmk5bf.execute-api.us-west-2.amazonaws.com/dev/
```
Alternatively, you can go to the [CloudFormation Console](https://console.aws.amazon.com/cloudformation/home), find the `CustomizeUnicorns` stack and look in the **Output** tab
1. You can test in your browser (or `curl`) for the following APIs. Remember to append the API path (e.g. `/socks`) to the endpoint
<table>
<tr>
<th>API</th>
<th>HTTP Verb</th>
<th>path</th>
</tr>
<tr>
<td>List customization options and prices for horns</td>
<td>GET</td>
<td>/horns</td>
</tr>
<tr>
<td> List customization options and prices for glasses </td>
<td>GET </td>
<td>/glasses</td>
</tr>
<tr>
<td> List customization options and prices for capes </td>
<td>GET</td>
<td>/capes </td>
</tr>
<tr>
<td>List customization options and prices for socks </td>
<td> GET </td>
<td>/socks </td>
</tr>
</table>
For example:

## Module-0G: Set up Postman to test the API
We will use [**Postman**](https://www.getpostman.com/) for the rest of the workshop for testing API requests.
1. If you don't have installed yet on your laptop, please download it at: [https://www.getpostman.com/](https://www.getpostman.com/)
1. To save you time, we created a Postman collection that you can use to test each of the APIs we are working with today.
* click on the **Import** button in postman
* Then use **Import from Link** and supply the below link:
`https://raw.githubusercontent.com/aws-samples/aws-serverless-security-workshop/master/src/test-events/Customize_Unicorns.postman_collection.json`
* Click on **Import**
<img src="images/0F-import-postman.png" width="50%" />
1. You should now see a collection called `Customize_Unicorns` imported in postman on the left hand side
<img src="images/0F-postman-after-import.png" width="60%" />
1. We need to set the `base_url` variable by creating a environment in postman.
1. Click the ⚙ icon (“Manage Environments”) in the upper right corner of the Postman app.
<img src="images/0F-postman-manage-env.png" width="90%" />
1. Create a new environment by clicking the **Add** button.
1. Enter an environment name, e.g. `dev`
1. Add an variable `base_url` and use the base API endpoint we deployed earlier.
⚠ **Ensure to leave out the trailing `/`!** ⚠
See example screenshot below
<img src="images/0F-postman-environment.png" width="70%" />
> See documentation from Postman on [managing environments](https://www.getpostman.com/docs/v6/postman/environments_and_globals/manage_environments) if you want to learn more.
1. Click **Add** to create the `dev` environment and exit out the Manage Environments by clicking the **X**
1. Select `dev` on the environment drop down menu.
<img src="images/0F-select-dev-env.png" width="90%" />
1. Now, you are ready to test the API using postman. In the left sidebar, click on the `Customize_Unicorns` collection, expand the `List customization options` folder. Select an API in the folder and test sending an request by clicking on the **Send** button

## Next step
To start securing the serverless application you just deployed, return to the workshop [landing page](../../README.md) to pick a module to work on!
================================================
FILE: docs/01-add-authentication/README.md
================================================
# Module 1: Add Authentication and Authorization
As you have probably noticed, the serverless app we just deployed is now open to anyone in the world to access. Attackers can submit any number of unicorn customizations and we have no way of knowing who really made the request.
To lock down access to the API to only trusted partners, we must add authentication and authorization to the API.
Given that our use case is for 3rd party companies to programmatically access the API, there are a few options we can take to implement auth:
* Use **API Keys** - this is the simplest option, but it doesn't provide lots of flexibility. For example, it requires us to build our own authorization system if fine-grained access control is needed (e.g. different clients may require different access to different APIs).
* Use **OAuth** [**Client Credentials Flow**](https://tools.ietf.org/html/rfc6749#section-4.4) This is a well-defined standard where the client authenticates with an authorization server (in our example, Amazon Cognito) using client credentials, get a access token, then calls the API Gateway with the access token.
<img src="images/10-oauth-flow.png" alt="oauth flow" width="60%">
We chose to use the Oauth Client Credentials option for the workshop today, because compared to using API keys, using the client credentials flow allow us to:
* Easily define different authorization [**scopes**](https://www.oauth.com/oauth2-servers/scope/) so different kinds of clients may have access to different APIs and resources
* In the future, we may have use cases where the API will be accessed through a web UI managed by Wild Rydes instead of programmatic access. Or we may want to allow 3rd party companies to build custom apps that makes requests on behalf of riders/unicorns (see [example walkthrough](https://github.com/aws-samples/aws-serverless-workshops/tree/master/WebApplication/5_OAuth) for that use case). Designing our APIs to authenticate with Oauth access tokens give us flexibility to support these options down the line.
In this module, you will use **Amazon Cognito** to act as the **Authorization server**, and leverage [**Lambda authorizer**](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) for API Gateway to inspect the token and determine the access policies based on the token. By the end of the module, your architecture will look like this:

> Once you decide to do this module, you won't be able to use Cloud9 Local testing as we haven't configured the correct permissions to test locally this functionality.
## Module 1 instructions
Quick links to submodules:
* [**Module 1A**](#1A): Configure Cognito User Pool with a hosted domain
* [**Module 1B**](#1B): Create the authorization scopes in the Cognito User Pool
* [**Module 1C**](#1C): Create client credentials for internal admin account
* [**Module 1D**](#1D): Deploy Custom Authorizer lambda
* [**Module 1E**](#1E): Use the admin client to register new partner companies
* [**Module 1F**](#1F): Use the partner company client credentials to customize unicorns
### <a name="1A"></a>Module 1A: Create a Cognito User Pool and hosted domain
**[Amazon Cognito](https://aws.amazon.com/cognito)** provides a managed service for simplifying identity management for your apps. To enable our clients to authenticate against Cognito, we need to configure a Cognito user pool and an associated domain name.
1. As a preparation for this module, we have already provisioned a Cognito User Pool. Review it under the **Resources** section of `src/template.yaml`:
```
CognitoUserPool:
Type: "AWS::Cognito::UserPool"
Properties:
UserPoolName: !Sub '${AWS::StackName}-users'
```
1. To configure the Cognito User Pool with a domain, go to the [Cognito management console](https://console.aws.amazon.com/cognito/home), and click on **Manage User Pools**
1. Click on the user pool created by the SAM Template (`src/template.yaml`). It should be named "**CustomizeUnicorns-users**"
1. Under **App Integration**, go to the **Domain Name** tab to set up an unique Cognito domain our API consumers will use for authentication requests.
**You must pick a unique custom domain name**! For example `custom-unicorn-johndoe`
1. Make sure that the domain name is available and then click **Save changes**

1. Note down the domain name (In the format of `https://<your-custom-name>.auth.<aws-region>.amazoncognito.com`), you will need this later.
**Tip 1**: You can copy the full domain name by going to the **App Integration** tab.
**Tip 2**: You can copy the full domain name into a text editor on your laptop or create a new file in the cloud9 IDE environment to use as scratch pad for storing such values.
### <a name="1B"></a>Module 1B: Create the authorization scopes in the Cognito User Pool
Amazon Cognito User Pools lets you declare custom resource servers. Custom resource servers have a unique identifier - normally the server uri - and can declare custom <a href="https://www.oauth.com/oauth2-servers/scope/" target="blank"> **scopes** </a>. Scopes allow you to limit the access of an app to a smaller subset of all the available APIs and resources.
For this app, we will start with defining 2 scopes:
* **CustomizeUnicorn** - used by 3rd party partners that allows listing unicorn outfit customization options and create/describe/delete unicorn customizations.
* **ManagePartners** - used by internal apps/admin to register partner companies
To achieve this:
1. Go to the **Resource Servers** tab under **App integration**
1. In the resource servers screen, click **Add a resource server**.
1. Specify `WildRydes` as the Name.
1. Use `WildRydes` as the Identifier for the custom resource server.
1. In the Scopes section, declare 2 new scopes:
* `CustomizeUnicorn` - used by 3rd party partners to customize unicorns
* `ManagePartners` - used by internal apps/admin to register partner companies
then click **Save changes**

### <a name="1C"></a> Module 1C: Create client credentials for internal admin account
Each new company that signs up to customize unicorns will need to be provisioned a set of client credentials (client ID and client secret) they can use with their request. This means we would need a process to create and distribute these client credentials.
In real life, you would probably want to do this in a web developer portal. The partner companies sign in with some user name and password to the web portal, then they can request a set of client credentials to use to make programmatic requests with. However, due to limited time for the workshop, we will just simplify this by having a **POST /partner** API that you as the admin working for Wild Rydes can use to sign up partner companies.
So now, let's get you a set of admin credentials with the `WildRydes/ManagePartners` OAuth scope, so you can start signing up other companies!
1. In the Cognito console, go to the **App Clients** tab under **General Settings**
1. Click **Add an app client**
1. Use `Admin` for app client name (For the **Auth Flows Configuration** section, you can either uncheck the ALLOW_CUSTOM_AUTH and ALLOW_USER_SRP_AUTH or leave it enabled)

1. Click **Create app client**
1. This generates a **App client id** and **App client secret** for the admin app client. Click on **Show Details** to see both values, copy them down for later use.

1. Go to **App client settings** tab under **App integration**
1. For the Admin client we just created, enable the **Client credentials** Oauth Flow and select the Custom Scopes for **WildRydes/ManagePartners**, and click **Save changes**

### <a name="1D"></a> Module 1D: Deploy Custom Authorizer lambda
We need to configure a [**Lambda authorizer**](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) for API Gateway. As you can see in the below diagram, a lambda function is needed to inspect the access token in the request, and determine the identity and the corresponding access policy of the caller.

1. Switch back to the browser tab with your Cloud9 IDE environment or reopen it from the [Cloud9 console](https://console.aws.amazon.com/cloud9/home)
1. In the serverless API we have deployed, the backend logic has an identifier for 3rd party companies (The primary key of the `Companies` MySQL table and a foreign key constraint for the `Custom_Unicorns` table.) When the lambda function inspects the access token, it can parse out the OAuth ClientID from it. To be able to tell the backend lambda which company is making the request, we need a lookup table that maps the ClientID to the company IDs in the backend database. In this case, we chose to use a separate DynamoDB table to store this mapping to separate the auth functionality from the backend system:

1. As a preparation for this module, we have also already provisioned a DynamoDB table to store this mapping. Review it under the **Resources** section of `template.yaml`:
```
PartnerDDBTable:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: ClientID
Type: String
TableName: !Sub '${AWS::StackName}-WildRydePartners'
```
1. Next, we need the code for lambda authorizer! Review the code in the `src/authorizer` folder.
Here's a high level summary of what the authorizer logic contains:
* Download the public key from the Cognito user pool, if not already cached
* Decode the JWT token and validate the signature with the public key
* Based on the claims parsed from the JWT token, generate a response policy that specifies API resources and actions the caller is permitted to access
1. Install nodejs dependencies in the `src/authorizer` folder:
```
cd ~/environment/aws-serverless-security-workshop/src/authorizer
npm install
```
1. Add the authorizer lambda to the **Resources** section of `template.yaml`:
```
CustomAuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: authorizer/
Runtime: nodejs14.x
Handler: index.handler
Policies:
Statement:
- Effect: Allow
Action:
- "dynamodb:*"
Resource: "*"
Environment:
Variables:
USER_POOL_ID: !Ref CognitoUserPool
PARTNER_DDB_TABLE: !Ref PartnerDDBTable
```
1. API gateway require an IAM role to invoke the custom authorizer. Add it to the SAM template as another **Resource** object:
```
ApiGatewayAuthorizerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal:
Service:
- "apigateway.amazonaws.com"
Action:
- sts:AssumeRole
Policies:
-
PolicyName: "InvokeAuthorizerFunction"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- lambda:InvokeAsync
- lambda:InvokeFunction
Resource:
Fn::Sub: ${CustomAuthorizerFunction.Arn}
```
1. Find the swagger definition of the API gateway in the SAM template
```
UnicornApi:
Type: AWS::Serverless::Api
Properties:
StageName: dev
DefinitionBody:
swagger: "2.0"
info:
title:
Ref: AWS::StackName
description: My API that uses custom authorizer
version: 1.0.0
### TODO: add authorizer
paths:
....
```
1. Replace the `### TODO: add authorizer` section with
```
securityDefinitions:
CustomAuthorizer:
type: apiKey
name: Authorization
in: header
x-amazon-apigateway-authtype: custom
x-amazon-apigateway-authorizer:
type: token
authorizerUri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CustomAuthorizerFunction.Arn}/invocations
authorizerCredentials:
Fn::Sub: ${ApiGatewayAuthorizerRole.Arn}
authorizerResultTtlInSeconds: 60
```
**Caution:** Ensure the `securityDefinitions` section you pasted is at the same indentation level as `info` and `paths`
1. In the `paths` section of the Swagger template, uncomment the
```
# security:
# - CustomAuthorizer: []
```
lines for each API method. For example:
Change
```
"/socks":
get:
# security:
# - CustomAuthorizer: []
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${UnicornPartsFunction.Arn}/invocations
responses: {}
...
```
Into:
```
paths:
"/socks":
get:
security:
- CustomAuthorizer: []
x-amazon-apigateway-integration:
httpMethod: POST
type: aws_proxy
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${UnicornPartsFunction.Arn}/invocations
responses: {}
...
```
1. Save the `template.yaml` file
1. Now we need to validate the template.
First, ensure we are back in the `src` folder in the terminal
```
cd ..
```
Then run
```
sam validate -t template.yaml
```
If the SAM template has no errors, you should see
```
2018-10-08 16:00:56 Starting new HTTPS connection (1): iam.amazonaws.com
<your directory>/src/template.yaml is a valid SAM Template
```
otherwise, fix the syntax error before continuing to next step
1. Deploy the updates by running the same commands we used in module 0 to deploy the application:
```
aws cloudformation package --output-template-file packaged.yaml --template-file template.yaml --s3-bucket $BUCKET --s3-prefix securityworkshop --region $REGION && aws cloudformation deploy --template-file packaged.yaml --stack-name CustomizeUnicorns --region $REGION --capabilities CAPABILITY_IAM --parameter-overrides InitResourceStack=Secure-Serverless
```
1. After the SAM template has finished updating, go to the [API Gateway console](https://console.aws.amazon.com/apigateway) and click into the API we just updated. Under **Resources**, choose any method in the API, and you should see **Auth: CustomAuthorizer** under **Method Request**:

1. Now go back to Postman and test sending API requests (any API in the collection). You should now get **401 Unauthorized** errors from the APIs now.
> Make sure you didn't miss any APIs!
### <a name="1E"></a>Module 1E: Use the admin client to register new partner companies
Now we have configured our API so only authenticated requests can get through to our protected resources. Our next step is getting some credentials to make authenticated requests with!
To make authenticated requests using the admin client credentials we just created in Module 1C, we can use PostMan:
1. In Postman, right click on the **Manage Partner** folder and click **edit**
1. In the Edit Folder window that pops up, go to **Authorization** tab, and change the Auth **Type** to `OAuth 2.0`, then click **Get New Access Token**

1. Configure the token request:
* **Name**: `admin`
* **Grant Type**: `Client Credentials`
* **Access Token URL**: Remember the cognito domain we created in [module 1A](#1A)? use that and append `/oauth2/token` to the end
> The full URL should look like `https://custom-unicorn-johndoe.auth.us-west-2.amazoncognito.com/oauth2/token`
* **Client ID**: this the clientID of the admin we created in Module 1D
* **Client Secret**: this the client secret of the admin we created in Module 1D
* **Scope**: it's optional (the token will be scoped anyways) we can leave it blank

And click **Request Token**
1. Now you should see the new token returned from Cognito. scroll down and click **Use Token**
1. Back to the Edit Folder window, click **update**
1. Now, go to the **POST Create Partner** API in the **Manage Partner** Folder in the left hand toolbar
1. In the **Body** tab, fill in the name of the partner company to register, and click **Send**. You should get in the response the client ID and secret for the company you registered.

1. Note down the `ClientId` and `ClientSecret` from the output in your text editor. This is the client credentials "Cherry Corp" will use to customize unicorns!
### <a name="1F"></a>Module 1F: Use the partner company client credentials to customize unicorns
Now we have a set of client credentials for the partner company you just registered. Let's pretend to be "Cherry company" (or whatever company you just registered) and submit some unicorn customizations!
1. Request an access token from the new company client credentials you just generated. (You will notice this is very similar steps as you did in [module 1E](#1E)!
1. Right click on the **Customize_Unicorns** collection and **edit**
> Ensure to right click on the overarching **Customize_Unicorns** collection rather than any of the subfolders. Doing so will set the default authorization header to use for any API in the collection, unless overridden by the sub-folders (as we just did in module 1E)
1. Go to **Authorization** tab, pick Oauth2.0
1. Use the same Cognito token url (hint: Cognito domain + `/oauth2/token`)
1. Use the Client ID generated from the **POST /partner** API you just created from step [module 1E](#1E)
> Tip: if you forget the client ID/secret, you can also retrieve it from the Cognito User pool console under **App clients**

1. Test making a request for describing sock customziation options again. It should succeed this time!
1. Now you can create a unicorn customziation! Choose the **POST create Custom_Unicorn** API from the collection, in the **Body** tab, enter
```javascript
{
"name":"Cherry-themed unicorn",
"imageUrl":"https://en.wikipedia.org/wiki/Cherry#/media/File:Cherry_Stella444.jpg",
"sock":"1",
"horn":"2",
"glasses":"3",
"cape":"4"
}
```
If it's successful, you should get a response with the ID of the customization that was just submitted:
`{ "customUnicornId": X}`

1. You can also test out other APIs in the collection, e.g. LIST, GET, DELETE on Unicorn Customizations.
## Next Step
You have now completed Module 1 and added auth to your serverelss application. You can now return to the workshop [landing page](../../README.md) to pick another module to work on!
================================================
FILE: docs/02-add-secrets-manager/README.md
================================================
# Module 2: Securely storing our database credentials with AWS Secrets Manager
Hardcoding database's credentials and connection information is not a best practice. Not only from a security point of view but also operational. Any data breach within your code could expose these secrets and expose your business critical data. You also risk accidentally making your credentials public if you check in your code into a public repo or a private repo that can be accessed by a wide range of people.
From an operational perspective, when deploying your code between different stages in your CI/CD pipelines, hardcoded values make it difficult to automate and might require manual intervention slowing down your development process.
During this section we will use AWS Secrets Manager to handle our database credentials for us. Besides keeping your database safe even if people get access to read your code, AWS Secrets Manager also integrates with RDS and will automatically handle password rotations.
> Once you decide to do this module, you won't be able to use Cloud9 Local testing as we haven't configured the correct permissions to test locally this functionality.
## Module 2A: Create a secret in AWS Secrets Manager
First thing we need to do is create a secret in Secrets Manager.
1. Go to your AWS Console to AWS Secrets Manager.

1. Click on *Store a new secret*.
2. Select ***Credentials for RDS database*** type of secret. Fill it with these values:
- Username: `admin`
- Password: `Corp123!`
- Select the encryption key: `DefaultEncryptionKey`.

- Select your Aurora cluster (starts with `secure-serverless-aurora`)
<img src="images/02-secret-select-db.png" width="60%"/>
1. Click on *Next* and continue fill the wizard with the following values.
- Secret name: `secure-serverless-db-secret`
- Description: Use an optional description here.

1. Again, click on *Next* and configure your rotation.
- Click on `Enable Rotation`
- Select `30 Days` as the rotation interval.
- Choose **Create a new Lambda Function to perform rotation**
- Give the lambda function a name, e.g. `aurora-rotation`
- Select **Use this secret**

1. Then, click *Next* and, if you want, review the example code. During the next sections we will modify our code to use Secrets Manager and this code will be used as an example.
1. Finally, click *Store*.
> Be careful if you are using Firefox or Chrome extensions when performing these steps! Some extensions like *LastPass* might change the values entered before.
>
> You can review and verify the values after you create the secret by clicking **Retrieve secret value**
>
> 
## Module 2B: Add permission to Lambda function to read from secrets manager
We need to modify the execution policy on the lambda functions, so they areallowed to make API calls to Secrets Manager.
In `src/template.yaml`, look for the block below that defines policies for Secrets Manager (**You should find a total 3 occurrences**) and uncomment them.
```yaml
# - Version: '2012-10-17'
# Statement:
# - Effect: Allow
# Action:
# - "secretsmanager:GetSecretValue"
# Resource: "*"
```
⚠ **Note: ENSURE YOU REPLACE ALL 3 OCCURRENCES**! ⚠
Also, note that in the **Globals** section we are referencing the name of the secret
```
Globals:
Function:
Timeout: 30
Environment:
Variables:
SECRET_NAME: secure-serverless-db-secret # name of the RDS credentials in secrets manager
```
## Module 2C: Modify your code to use the secret
Once you have created the secret, you will have to modify the application code to use Secrets Manager. Go to the file `src/app/dbUtils.js`. Here is where the connection information is stored.
At the beginning of the file, add the following lines to create the required variables to use AWS Secrets Manager. You can add them just after the line `const PARTNER_COMPANY_TABLE = "Companies";`.
```javascript
// Load the AWS SDK
const AWS = require('aws-sdk');
const secretName = process.env.SECRET_NAME;
var secret;
// Create a Secrets Manager client
const client = new AWS.SecretsManager();
```
Taking a look at this code you might notice that we are using Environment Variables. To see where these variables are defined, go to `template.yaml` and check on *Global*. These have been defined from the beginning. Verify that they follow the previous step work.
Now, it's time to modify how we set the configuration of our connection to the database. The method that does this is called **getDbConfig** within *dbUtils.js*.
This method returns a [promise](http://google.com) resolving with the JSON parameters.
```javascript
resolve({
host: host,
user: "admin",
password: "Corp123!",
database: "unicorn_customization",
multipleStatements: true
});
```
Replace the lines above with the following code:
```javascript
client.getSecretValue({SecretId: secretName}, function (err, data) {
if (err) {
console.error(err);
if (err.code === 'ResourceNotFoundException')
reject("The requested secret " + secretName + " was not found");
else if (err.code === 'InvalidRequestException')
reject("The request was invalid due to: " + err.message);
else if (err.code === 'InvalidParameterException')
reject("The request had invalid params: " + err.message);
else
reject(err.message);
}
else {
if (data.SecretString !== "") {
secret = data.SecretString;
resolve({
host: JSON.parse(secret).host,
user: JSON.parse(secret).username,
password: JSON.parse(secret).password,
database: "unicorn_customization",
multipleStatements: true
});
} else {
reject("Cannot parse DB credentials from secrets manager.");
}
}
});
```
Here is an example of how the method should look after the changes.

If you read the code closely, you will see that is gathering the secrets from AWS Secrets Manager service and use them to resolve the promise with the values returned by the service.
## Module 2D: Deploy and test
Now it's time to deploy again and test the application:
1. Validate the template:
```
sam validate -t template.yaml
```
1. Deploy the updates by running:
```
aws cloudformation package --output-template-file packaged.yaml --template-file template.yaml --s3-bucket $BUCKET --s3-prefix securityworkshop --region $REGION && aws cloudformation deploy --template-file packaged.yaml --stack-name CustomizeUnicorns --region $REGION --capabilities CAPABILITY_IAM --parameter-overrides InitResourceStack=Secure-Serverless
```
1. Test the API using postman. You can test it with whatever method in the API such as `/socks`, `/horns`...
## Extra credit
**Cache the secrets:**
The code currently makes a call to Secrets Manager to retrieve the secret every time the Lambda function gets invoked. This may generate lots of API calls to Secrets Manager when your API is being invoked frequently.
To reduce network traffic to Secrets Manager, you can take advantage of [Execution Context reuse](https://aws.amazon.com/blogs/compute/container-reuse-in-lambda/) and cache the secret value.
## Next Step
Return to the workshop [landing page](../../README.md) to pick another module.
================================================
FILE: docs/03-input-validation/README.md
================================================
# Module 3: Input validation on API Gateway
A quote from the OWASP website:
> "*The most common web application security weakness is the failure to properly validate input from the client or environment*."
>
> --- [**OWASP** (The Open Web Application Security Project)](https://www.owasp.org/index.php/Data_Validation)
A great [XKCD comic](https://xkcd.com/327/) demonstrate this point well:

You can configure API Gateway to perform basic validation of an API request before proceeding with the integration request. When the validation fails, API Gateway immediately fails the request, returns a 400 error response to the caller, and publishes the validation results in CloudWatch Logs. This reduces unnecessary calls to the backend. More importantly, it lets you focus on the validation efforts specific to your application.
For example, in our application, when defining an customization, we have to be sure that our new customization should have:
- A **name** for the customization object
- An url for the cape's **image** .
- A type of **socks** for our unicorn specified by an id.
- A specific id for the **horn** to use.
- An id for the pair of **glasses**.
- A type of **cape** by id.
This information should be in our request body to create a new customization that follows specific patterns. E.g. the imageUrl should be a valid URL, the IDs for socks and horns are numeric values.
By leveraging input validation on API Gateway, you can enforce required parameters and regex patterns each parameter must adhere to. This allows you to remove boilerplate validation logic from backend implementations and focus on actual business logic and deep validation.
## Module 3 - Optional: attack your API with SQL injection!
If you haven't completed **Module 6: WAF**, your serverless API is currently vulnerable to SQL injection attacks. This optional module shows how you can perform the attack.
<details>
<summary><strong>Click to expand for optional step instructions </strong></summary>
If you look at our lambda function code right now, no input validation is being performed, and with the below line specified as part of our mysql client setting (under `/src/app/dbUtils.js`):
```
multipleStatements: true
```
> **Note**: As a best practice you should set the `multipleStatements` option to false in your code (the nodejs mysql client actually defaults it false). However, this is not disabled by default in all programming languages/libraries, so we enabled it in our starter code for you to see the easiness of this attack.
we can easily embed SQL statements in the body of the request to get executed. For example, in the body of the POST customizations/ API, try using the below:
<details>
<summary><strong> If you have done module 1, use sample input here </strong></summary>
```
{
"name":"Orange-themed unicorn",
"imageUrl":"https://en.wikipedia.org/wiki/Orange_(fruit)#/media/File:Orange-Whole-%26-Split.jpg",
"sock":"1",
"horn":"2",
"glasses":"3",
"cape":"2); INSERT INTO Socks (NAME,PRICE) VALUES ('Bad color', 10000.00"
}
```
</details>
<details>
<summary><strong>If you have not done module 1, use sample input here </strong></summary>
```
{
"name":"Orange-themed unicorn",
"imageUrl":"https://en.wikipedia.org/wiki/Orange_(fruit)#/media/File:Orange-Whole-%26-Split.jpg",
"sock":"1",
"horn":"2",
"glasses":"3",
"cape":"2); INSERT INTO Socks (NAME,PRICE) VALUES ('Bad color', 10000.00",
"company":"1"
}
```
</details>

Send the request using Postman. If the request succeeds, you have now just performed a SQL injection attack!
If you look at the SQL injection statement we just performed, it's adding a bad value into the `Socks` table. We can verify that took effect by running the **GET /socks** API:

</details>
## Module 3A: Create a model for your Customizations
In API Gateway, a [**model**](https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html#models-mappings-models) defines the data structure of a payload, using the [JSON schema draft 4](https://tools.ietf.org/html/draft-zyp-json-schema-04).
When we define our model, we can ensure that the parameters we are receiving are in the format we are expecting. Furthermore, you can check them against regex expressions. A good tool to test if your regex is correct is [regexr.com](https://regexr.com/).
For our **POST /customizations** API, we are going to use the following model:
```json
{
"title": "Customizations",
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [
"imageUrl",
"sock",
"horn",
"glasses",
"cape",
"name"
],
"properties": {
"imageUrl": {
"type": "string",
"title": "The Imageurl Schema",
"pattern": "^https?:\/\/[-a-zA-Z0-9@:%_+.~#?&//=]+$"
},
"name": {
"type": "string",
"title": "The name Schema",
"pattern": "^[a-zA-Z0-9- ]+$"
},
"sock": {
"type": "string",
"title": "The Sock Schema",
"pattern": "^[0-9]*$"
},
"horn": {
"type": "string",
"title": "The Horn Schema",
"pattern": "^[0-9]*$"
},
"glasses": {
"type": "string",
"title": "The Glasses Schema",
"pattern": "^[0-9]*$"
},
"cape": {
"type": "string",
"title": "The Cape Schema",
"pattern": "^[0-9]*$"
}
}
}
```
Now, follow these steps:
1. Go to API Gateway console.
2. Click on the API **CustomizeUnicorns**
3. Click on **Models**
4. Click on **Create** and create a model with the following values:
- Model name: `CustomizationPost`
- Content type: `application/json`
1. In the model schema, use the one provided before (the *json* before this section).
1. Once everything is filled, click on **Create model**.

Once we have created our model, we need to apply it to our customizations/post method.
1. Within the API Gateway Console, click on CustomizeUnicorns, **Resources**
1. Click under /customizations --> **POST** method

1. Click on **Method Request**
1. Under **Request Validator**, click on the pencil to edit it. Select **Validate Body**. Then, click on the tick to confirm the change.
1. Under **Request Body**, click on **Add model** with the following values:
- Content type: `application/json`
- Model name: `CustomizationPost`
1. Click to the tick to confirm.

> On step number 2 you might have noticed that we can also validate query parameters and request headers in addition to request body. This is really useful when our application uses both at the same time and we want to have complex validations. If you want to find more information, [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html) is our documentation about this.
1. Now it's time to deploy and test! Go to the **Actions** menu and click on **Deploy API**. Select `dev` as the **Deployment stage** and confirm by clicking **Deploy**.
## Module 3B: Test your Validation
Use postman, you can try making requests to the **POST /customizations** API using invalid parameters and see the input validation kick in (if you get an unauthorize error message, could be caused by the expiration time of the Authentication token. You can easily refresh rthis token following these steps from module [01](../01-add-authentication/README.md#1E):
### Wrong parameters = Invalid request:
Here are some example request bodies that fail:
* Missing fields:
```javascript
{
"name":"Cherry-themed unicorn",
"imageUrl":"https://en.wikipedia.org/wiki/Cherry#/media/File:Cherry_Stella444.jpg",
"glasses": "3",
"cape": "4"
}
```
* The `imageUrl` not a valid URL:
```javascript
{
"name":"Cherry-themed unicorn",
"imageUrl":"htt://en.wikipedia.org/wiki/Cherry#/media/File:Cherry_Stella444.jpg",
"sock": "1" ,
"horn": "2" ,
"glasses": "3",
"cape": "4"
}
```
* The `cape ` parameter not a number (SQL injection attempt)
```javascript
{
"name":"Orange-themed unicorn",
"imageUrl":"https://en.wikipedia.org/wiki/Orange_(fruit)#/media/File:Orange-Whole-%26-Split.jpg",
"sock": "1",
"horn": "2",
"glasses": "3",
"cape":"2); INSERT INTO Socks (NAME,PRICE) VALUES ('Bad color', 10000.00"
}
```
You should get a 400 Bad Request response:
```javascript
{"message": "Invalid request body"}
```
### Correct parameters
Testing the **POST /customizations** API with right parameters:
<details>
<summary><strong> If you have done module 1, use sample input here </strong></summary>
```javascript
{
"name":"Cherry-themed unicorn",
"imageUrl":"https://en.wikipedia.org/wiki/Cherry#/media/File:Cherry_Stella444.jpg",
"sock": "1",
"horn": "2",
"glasses": "3",
"cape": "4"
}
```
</details>
<details>
<summary><strong> If you have not done module 1, use sample input here </strong></summary>
```javascript
{
"name":"Cherry-themed unicorn",
"imageUrl":"https://en.wikipedia.org/wiki/Cherry#/media/File:Cherry_Stella444.jpg",
"sock": "1",
"horn": "2",
"glasses": "3",
"cape": "4",
"company" : "1"
}
```
</details>
The result should be:
```bash
{"customUnicornId":<the-id-of-the-customization>}
```
## Additional input validation options
As you have now seen, API Gateway input validation gives you basic features such as type checks and regex matching. In a production application, this is often not enough and you may have additional constraints on the API input.
To gain further protection, you should consider using the below in addition to the input validation features from API Gateway:
* Add an AWS WAF ACL to your API Gateway - check out [**Module 6**](../06-waf/)
* Add further input validation logic in your lambda function code itself
## Extra credit
There is, at least, one more method that needs to be validated. Build your own json schema for that method and apply the same steps mentioned before and you should be able to validate these methods as well!
<details>
<summary><strong>Hint: In case you need some help, here is the model to be used:</strong></summary>
```json
{
"title": "PartnerPOST",
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"title": "Partner Schema",
"pattern": "^[a-zA-Z0-9- ]+$"
}
}
}
```
</details>
## Next step
You have now added basic input validation to your API and further reduced the risk of attackers using bad inputs to sabotage your API!
Return to the workshop [landing page](../../README.md) to pick another module.
================================================
FILE: docs/04-ssl-in-transit/README.md
================================================
# Module 4: Use SSL in-transit for your DB connections
Although we are using VPC and traffic is private within it, some regulations or compliance requirements might require encryption in transit. This encryption secures the data when communicating with the database.
Go to *dbUtils.js* to add a new property to your database connection. Under the method ***getDbConfig***, within the resolve object (a JSON object), add a new line to the JSON:
```
ssl: "Amazon RDS",
```
The resolve should be like this:
<details>
<summary><strong>If you haven't gone through AWS Secrets Manager step</strong></summary><p>
```javascript
resolve({
ssl: "Amazon RDS",
host: host,
user: "admin",
password: "Corp123!",
database: "unicorn_customization",
multipleStatements: true
});
```
</details>
<details>
<summary><strong>If you have gone through AWS Secrets Manager step</strong></summary><p>
```javascript
client.getSecretValue({SecretId: secretName}, function (err, data) {
if (err) {
console.error(err);
if (err.code === 'ResourceNotFoundException')
reject("The requested secret " + secretName + " was not found");
else if (err.code === 'InvalidRequestException')
reject("The request was invalid due to: " + err.message);
else if (err.code === 'InvalidParameterException')
reject("The request had invalid params: " + err.message);
else
reject(err.message);
}
else {
if (data.SecretString !== "") {
secret = data.SecretString;
resolve({
ssl: "Amazon RDS",
host: JSON.parse(secret).host,
user: JSON.parse(secret).username,
password: JSON.parse(secret).password,
database: "unicorn_customization",
multipleStatements: true
});
} else {
reject("Cannot parse DB credentials from secrets manager.");
}
}
});
```
</details>
Finally, deploy these changes:
```bash
cd ~/environment/aws-serverless-security-workshop/src
aws cloudformation package --output-template-file packaged.yaml --template-file template.yaml --s3-bucket $BUCKET --s3-prefix securityworkshop --region $REGION && aws cloudformation deploy --template-file packaged.yaml --stack-name CustomizeUnicorns --region $REGION --capabilities CAPABILITY_IAM --parameter-overrides InitResourceStack=Secure-Serverless
```
Once this is done, you should be able to connect to the database using SSL.
## Ensure SSL - Optional step
You can require SSL connections for specific users accounts\. For example, you can use one of the following statements, depending on your MySQL version, to require SSL connections on the user account `encrypted_user`\.
For MySQL 5\.7 and later:
```
ALTER USER 'encrypted_user'@'%' REQUIRE SSL;
```
For MySQL 5\.6 and earlier:
```
GRANT USAGE ON *.* TO 'encrypted_user'@'%' REQUIRE SSL;
```
For more information on SSL connections with MySQL, go to the [MySQL documentation](https://dev.mysql.com/doc/refman/5.6/en/secure-connections.html)\.
To find the MySQL version of the Aurora database, go to the RDS console and find the **Engine version** under **Configuration** tab of the database cluster:

## Next step
You have now further secured your data by enabling encryption in transit for your database connection!
Return to the workshop [landing page](../../README.md) to pick another module.
================================================
FILE: docs/05-usage-plan/README.md
================================================
# Module 5: Usage Plan
You can leverage Usage Plans with Amazon API Gateway to set limits on request rate for consumers of your API to protect it from being abused by a particular misbehaving client.
To tally the number of requests based on the caller, API Gateway uses API Keys to keep track of different consumers for your API. In our use case, requests coming from different companies can be calculated separately.
## Module 5A: Create an API Gateway usage plan
1. In the API Gateway console, go to **Usage Plans** tab, and click **Create**
1. Fill in the details for the usage plan
* **Name**: ```Basic```
* **Description** : ```Basic usage plan for Unicorn customization partners```
* **Enable throttling**: check yes
* **Throttling Rate** : ```1``` request per second
* **Throttling Burst** : 1
* **Enable Quota**: check yes and use ```100``` requests per ```month```

Click **Next**
1. Associate the API we created previously with the usage plan. Pick `dev` stage.

> The warning sign is expected because we haven't yet configured the API to require API Keys. This will be done in a later steps.
1. Click the checkmark to confirm. Then click **Next**

1. We currently don't have any API keys set up. In this step, click **Create API Key and add to Usage Plan** to create an API key for the partner company
<details>
<summary><strong> If you have not done module 1, expand for instructions here </strong></summary>
* For Name, pick any name e.g. `cherry company`.
* For API Key, select **Auto Generate**
* Click **Save**
<img src="images/5A-auto-generate-API-key.png" />
</details>
<details>
<summary><strong> If you have done module 1, expand for instructions here </strong></summary>
For our application, we are going to reuse the value of the ClientID of the customer as the value for the API Key, to keep down the number of random strings that customers have to remember.
* For Name, use the company name you created in **Module 1: Auth**.
* For API Key, select **Custom** so we can import the value
* In the inputbox that comes up, use the same value as the ClientID of the company (if you forgot it, you can retrieve it from the Cognito console and look under **App clients** tab
* Click **Save**

</details>
1. After the API key has been created, click **Done**.

## Module 5B: Update API Gateway to enforce API keys
Now, we need to modify our API gateway so requests must have an API key present.
<details>
<summary><strong> If you have done module 1, expand for instructions here </strong></summary>
1. In the API swagger definition in `template.yaml`, add the below lines to add an additional type of AWS security:
```yaml
ApiKey:
type: apiKey
name: x-api-key
in: header
```
<img src="images/5B-add-security-def.png"/>
1. Next, for the APIs in the Swagger template for customizing unicorns and listing customization options (leave out the `/partners` APIs for now), add the below
```yaml
- ApiKey: []
```
to the `security` section in each API:
<img src="images/5B-add-API-key-to-swagger.png"/>
</details>
<details>
<summary><strong> If you have not done module 1, expand for instructions here </strong></summary>
1. In the API swagger definition in `template.yaml`, find the line:
```
### TODO: add authorizer
```
add the following lines below that:
```yaml
securityDefinitions:
ApiKey:
type: apiKey
name: x-api-key
in: header
```
See screeenshot:
<img src="images/5B-add-security-def-no-module-1.png"/>
⚠ **Caution: Ensure the `securityDefinitions` section you pasted is at the same indentation level as `info` and `paths`** ⚠
1. In the `paths` section of the Swagger template, change the occurrence of each of the below
```yaml
# security:
# - CustomAuthorizer: []
```
into
```yaml
security:
- ApiKey: []
```
See screeenshot:
<img src="images/5D-api-source-authorizer-swagger-no-module-1.png" width="80%" />
⚠ **Caution: Ensure all 9 APIs are updated** ⚠
</details>
Now, deploy the changes and verify:
1. Validate the template in the terminal:
```
sam validate -t template.yaml
```
1. Deploy the updates:
```
aws cloudformation package --output-template-file packaged.yaml --template-file template.yaml --s3-bucket $BUCKET --s3-prefix securityworkshop --region $REGION && aws cloudformation deploy --template-file packaged.yaml --stack-name CustomizeUnicorns --region $REGION --parameter-overrides InitResourceStack=Secure-Serverless --capabilities CAPABILITY_IAM
```
1. Once the deployment completes, you can go the [API Gateway console](https://console.aws.amazon.com/apigateway/home), navigate to the **CustomizeUnicorns API** -> **Resources** --> Pick an method --> click on **Method Request**.
You should now see the **API Key Required** field set to `true`
<img src="images/5B-confirm-usage-plan-requirement.png"/>
## Module 5C: Test request with API keys
1. Go back to Postman. Now the API is enforcing API keys, the request will fail if you don't include the API key header.
Try sending an request using Postman like you did before. You should see the request fail with a **403 Forbidden** status code and a `{"message": "Forbidden"}` response.
> If the response is **401 Unauthorized** and if you have completed module 1, most likely your access token is expired. Use Postman to request a new access token and try again.
1. You can add the API key request header by going to the **Header** tab, and put in
* `x-api-key` for the header key
* The value for the API Key that we added to the usage plan in module 5B:
* If you have done module 1: this should be same as the Cognito app Client ID
* If you have not done module 1: you can find the auto-generated API key value by going to the **API Keys** tab in the API gateway console --> click on the API key you created in module 5B --> click **Show** next to **API Key**
<img src="images/5C-find-api-key.png" />
You should now see the request go through
<img src="images/5C-explicit-API-key.png" />
## Module 5D (Optional): Use the Lambda authorizer to provide the API key
⚠ **Caution: This optional module assumes you have completed Module 1** ⚠
If you have already completed module 1: to make the API consumer's life easier, instead of forcing them to add a separate `x-api-key` header to the request they are making, we can make API Gateway take the API Key from the lambda authorizer. Read more about the two sources of API keys supported by API gateway [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-key-source.html)
To make this work:
1. In the API swagger definition in template.yaml, add the below lines
```
x-amazon-apigateway-api-key-source: AUTHORIZER
```
to the same level as the `securityDefinitions` or `paths` field:
<img src="images/5D-api-source-authorizer-swagger.png" />
1. We also need to make the Lambda authorizer return the API Key as part of the auth response. To do so, go to `authorizer/index.js`, find the following line in the code, and uncomment the second line:
// Uncomment here to pass on the client ID as the api key in the auth response
// authResponse.usageIdentifierKey = payload["client_id"];
1. Validate the SAM template:
```
sam validate -t template.yaml
```
1. Deploy the updates:
```
aws cloudformation package --output-template-file packaged.yaml --template-file template.yaml --s3-bucket $BUCKET --s3-prefix securityworkshop --region $REGION && aws cloudformation deploy --template-file packaged.yaml --stack-name CustomizeUnicorns --region $REGION --parameter-overrides InitResourceStack=Secure-Serverless --capabilities CAPABILITY_IAM
```
1. Once the deployment finishes, test making API requests again with postman. You should now be able to remove the `x-api-key` request header and the request should be able to succeed.
## Module 5E (Optional): Test throttling behavior with postman
⚠ **Caution: This optional module assumes you have completed Module 1 and Module 5D! If you have not done those two, you would need to add the x-api-key header to each of the API in the collection first!** ⚠
You can use postman to send multiple API requests in sequence.
1. In postman, click on **Runner**
1. Pick the `List customization options` folder to run
1. Select the `dev` environment and set runner to run 10 iterations
<img src="images/5E-runner-config.png" width="50%">
1. In the test result, you should some requests getting throttled and receiving a 429 response:
<img src="images/5E-postman-throttle.png" width="90%">
## Extra credit
If you want extra credit (karma points), here are some ideas:
* Try viewing/downloading the usage data for a given client.
> **Hint**: See [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-usage-plans-with-console.html#api-gateway-usage-plan-manage-usage) on some documentation
* Try configure different throttling thresholds for different API methods
> **Hint**: See [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-throttling.html#apig-request-throttling-stage-and-method-level-limits) on some documentation
## Next Step
You have now configured throttling for your API consumers using API Gateway Usage Plans!
Return to the workshop [landing page](../../README.md) to pick another module.
================================================
FILE: docs/06-waf/README.md
================================================
# Module 6: WAF
AWS WAF is a web application firewall that helps protect your web applications from common web exploits that could affect application availability, compromise security, or consume excessive resources. For example, you can reject requests that matches **SQL injection** and **Cross-Site Scripting (XSS)**. Additionally, you can filter web requests based on **IP address**, **geographic area**, **request size**, and/or string or **regular expression** patterns using the rules. You can put these conditions on HTTP headers or body of the request itself, allowing you to create complex rules to block attacks from specific user-agents, bad bots, or content scrapers. You can also take advantage of **Managed Rules** from **AWS Marketplace** to get immediate protections for your APIs from common threats, such as OWASP Top 10 security risks and Common Vulnerabilities and Exposures (CVE).
In this module, you will create a WAF ACL and attach it to the API Gateway we created.
### Module 6 - Optional: attack your API with SQL injection!
If you have completed **Module 3: Input validation on API Gateway**, your API now has some basic input validation in place for the JSON request body. However, it turns out our application is still vulnerable to SQL injection attacks as part of the request URI. This optional module shows how you can perform the attack.
<details>
<summary><strong>Click to expand for optional step instructions </strong></summary>
1. In Postman, go to the **GET Custom_Unicorn** request. Change the request URL to include a SQL injection in the request URI:
```
{{base_url}}/customizations/1; drop table Custom_Unicorns;
```
and Click **Send**.

You may get a "`Error querying`" response back because the SQL injection messed up the database query so not all of it succeeded (you can check the Cloudwatch Logs for the **CustomizeUnicorns-CustomizeUnicornFunction** Lambda function on what SQL queries got executed). However, the injected query to drop the `Custom_Unicorns` table should have succeeded.
1. If you now try to submit some valid quests, such as LIST or POST customizations, you will now get error back, because the `Custom_Unicorns` table got dropped by our evil attack!
1. To recover from this, go to your cloud9 browser tab, connect to the database again through the mysql command line
```
cd ~/environment/aws-serverless-security-workshop/src
mysql -h <replace-with-your-aurora-cluster-endpoint> -u admin -p
```
If you have gone through Module 4 and set the DB to require the `admin` user to use
type in the DB password (if you have gone through **Module 2: Secrets Manager**, your DB password may have been rotated by Secrets Manager. You can retrieve the new password by going to the Secrets Manager and click on the **Retrieve secret value** button
1. In the MySQL cli prompt, you can run the show tables command to verify the `Custom_Unicorns` table is gone:
```
use unicorn_customization;
show tables;
```
See screenshot:

1. Rerun the DB initialization script to recreate the `Custom_Unicorns` table:
```
drop table Capes, Glasses, Horns, Socks;
source init/db/queries.sql;
```
> You should see the output includes this error message:
>```ERROR 1062 (23000): Duplicate entry 'Placeholder company' for key 'NAME'```
> This is expected because we didn't want to overwrite the `company` table. You can ignore the error message
6. List the tables again to verify the `Custom_Unicorns` table is recreated.
```
show tables;
```
</details>
### Module 6A: Create a WAF ACL
Now let's start creating an AWS WAF to give us additional protection:
1. Go to the [AWS WAF Console](https://console.aws.amazon.com/wafv2/home#/wafhome)
1. The AWS WAF console has recently released a new version: see [Introducing AWS Managed Rules for AWS WAF
](https://aws.amazon.com/about-aws/whats-new/2019/11/introducing-aws-managed-rules-for-aws-waf/). However, this workshop has not been yet adapted to the new version. Therefore, we will be using the classic version of the WAF console. You can use the **Switch to AWS WAF Classic** button to switch to classic:

1. Click on **Create web ACL** on the WAF Classic console

1. In Step 1 of the ACL creation wizard, fill in:
* **Web ACL Name**: `ProtectUnicorn`
* **CloudWatch metric name**: this should be automatically populated for you
* **Region**: select the AWS region you chose for previous steps of the workshop
* **Resource type to associate with web ACL**: Pick `API Gateway`
* **Amazon API Gateway API**: Pick the API Gateway we deployed previously, `CustomizeUnicorns`
* **Stage**: select `dev`

and click **Next**
### Module 6B: Create WAF conditions
1. Next you will create 2 different conditions. Let's start with a condition to restrict the maximum size of request body:
* Go to **Size constraint conditions** section, click **Create condition**
* Give the condition a name, like `LargeBodyMatch`
* In Filter settings, add a filer on
* **Part of the request to filter on**: body
* **Comparison operator**: Greater than
* **Size (Bytes)**: 3000
* Click **Add filter**
* After the filter is added to the condition, click **Create**

1. Next, let's add a SQL injection condition.
* Go to **SQL injection match conditions** section, click **Create condition**
* Give the condition a name, like `SQLinjectionMatch`
* Here, we want to add multiple rules to inspect multiple aspects of the request: request body, request URI and query strings
* In the **Filter settings**, add 4 filters:
<table>
<tr>
<th></th>
<th>Part of the request to filter on</th>
<th>Transformation</th>
</tr>
<tr>
<td>1</td>
<td>Body</td>
<td>None</td>
</tr>
<tr>
<td>2</td>
<td>Body</td>
<td>URL decode</td>
</tr>
<tr>
<td>3</td>
<td>URI</td>
<td>URL decode</td>
</tr>
<tr>
<td>4</td>
<td>Query string</td>
<td>URL decode</td>
</tr>
</table>
* Click **Create**

1. Click **Next** to advance to the **Create rules** page
### Module 6C: Create WAF rules
1. Next, we create **Rules** that are composed of one or more **Conditions**. Let's start by creating a rule based on the request body size condition:
* Click **Create Rule**
* Give it a name, like `LargeBodyMatchRule`
* For **Rule type**, keep `Regular rule`
* In Add conditions section, select
* `does`
* `match at least one of the filters in the size constraint condition `
* `LargeBodyMatch` -- the name of the condition we created for large request body in 6B
* Then click **Create**

1. Next, we create the rule for SQL injection.
* Click **Create Rule**
* Give it a name, like `SQLinjectionRule`
* For **Rule type**, keep `Regular rule`
* In Add conditions section, select
* `does`
* `match at least one of the filters in the SQL injection match condition `
* `SQlInjectionMatch` -- the name of the condition we created for SQL injection in 6B
* Then click **Create**

1. Lastly, we can create a rate-based rule that prevents an overwhelming number of requests (either valid or invalid) from flooding our API:
* Click **Create Rule**
* Give it a name, like `RequestFloodRule`
* For **Rule type**, select `Rate-based rule`
* For **Rate limit**, use `2000`
* Then click **Create**

1. You should now see 3 rules in like below. Ensure you select `Block` if the request matches any of the rules.
For **Default action**, select `Allow all requests that don't match any rules`

1. Click **Review and create**
1. In the next page, review the configuration and click **Confirm and Create**

You have now added a WAF to our API gateway stage!
### Module 6D: Test requests with WAF protection
1. First, send some valid requests using Postman to make sure well-behaving requests are getting through.
1. Next, we can easily test the large request body rule by sending a few **POST /customizations** requests with a giant request body. If you don't receive an error immediately after applying WAF, you might need to wait a minute to for these changes to propagate.
In Postman, choose the **POST create Custom_Unicorn** request and replace the request body with:
```
{
"name":"my custom unicorn",
"imageUrl":"https://abc.efg.com/YA3K7yOwfmKhD1SdZ0MDB9C97RnJ3vb74WmoPOGJb2crs04okE2TcghSVgMWBLZ0c7rYZA5sjPWdfU7GJsRnEexwqgVfq2c94jEYdBCyxrZA3bZY36MiBnQZDrMyMMq1I8WJ7U4otss7mNWyQON0suZFXGCV7g7Z15dh14FIemSrkw3MzBLjsoAGTaz4VW1Ftljt5FCyJG3GtCSRvIoBkJ1YNiqKDRuiyFud7RgxBTXJEj3VvpTtT5CfWKPKKwfal4q506gW6aBgTeZGlhIGWlCxuT6sIYPodrXX4xmfukCFR32wtk7VgEiqYpKKwey2uQnZNQJHqwbHFZakppNYDQAeJ6NqB0tLDhERX2KtEiXH7iEJgAXeMLd7PNWrhYIlycsbcVnNrSpCmnBwADM3uVrKVF78qNGN2DnazascF3rIFSZJMPvNocSIT4zlK8VmXxB8inJb56UEHsYn9LAfVQMFcXPU3xwmKljk2fz5lHHs6vPeDnqjDNMEm1sXFq3S4759GZXzQubDcjYHX3REqeUNPYokrMAFb28qkfQwXUvrAq4Ov5eMXhFeXMBwupfqsSpPz0CMhr8o0M8sjG7ilXvrMo0jVcEmUrfRshkTHs55gZsaXP2CXjSbK0Z6sNiIiNtngAmHBZ1UkjJplirwVYLYAarYbghRdQswrWki447NtS0iFibgOjGkDXpFYwxaqNehMhalLUnP5jfz125V7HNzQ0wX5jgm42yGEjBaGNcI8hcySPXNI4jvT8RlZYxMs8m0zeZxxHEXqVfqVFYEr63gsI33nVPXS5jnJs7x3KY4wBOmOmwzWBBb62dBYBzqMwtRKp560MsR7uOK2hMGTBSdQtNubetRtu5JClGhlqE7Zv4SQ44lraE87vat55nXTgma7xpAjpwiH6yDQW6x1EXxjfVccjvR44FJNtUFVBh0DqUwoThR57SaEuIrLlP7e2pysgBI9GsYVt5RnXVUMXSPaDknVY4dFLVEPSEoOVRAt1kMcW9R3v3jURBBvTXfsFLzXn2t4DAYA1QmhJDkt9xpUOs1sviBYqjpUdhmRaun14Fx7aQiuwKAsfyJyVPbgZgmxOkeWpkf4ohcKwGZI8T6UlVWrwiRVL6eVQOdb3stNitObefEF7c9G9THQURzwpm7DanLTcAmnjlTyZS2NOW84it8QxDZ4qFGuxSjmzoGUUai9FRSpmyTozJvjmwZUFF5Codn9UWN2RrrEfKbuufl5ErPGRdyNkL73Hw9t1RG3UObmc0sf34z0JsHaL07StwB9HIXm5SLu9aZIpGoRu4UU23YL8jgORSXVop3HdkFifVTBf2w2mXaL0r07MHwLXQC7olLdNSXvj9uqVySHhAvAnhYquF0dwartwByZWT3Zt4i5gueuCb2LgrJJTSYQeDz9HIA4oDSnWj9BaxXKDuBTyLPwAdB4ER7I2Kl7lgdKuknaHXh5z9f4ybonueDv3RBd2Hcny5256im7jE6rEIWtTIbTKCR0frmpWm2smmwfL2IQIJE0lp5kxfDroqNf5l1XrovOo9sTD4LYIf88mUI3cAbBwNnPDtSZIWTJ3XcoK8Rm88xb3xKUxCZsQNnxBJM1eWzrYe4rSzIKk887GwwBAK1NFustp8bxSS3F73e2Oh9ijpbFBaAgmlG7nb21pEMBAw3G0yK6Hb0YePZvtXCxPQXDzFg5ya5BvqVPZESIAlkxNCz7kr0lTYJgwnixLUyM6gW6BekQAznQdaTK8LHYN9xJZusRgcSsVmfmA59KZ4J4oKxSAF4G14yI1P1Hj3juHHP8A90OjHfDvzkxGgL81CduPkBhyTRlhXg5mfIcEVGCViySt3cQGSYj43uo3sJ0JD77G7s5rc8kcc9VKPJ7sm1KZwNhF5lj6Ew1IOLG45xhrOOcJ8IvWAutcbFScOU0bZqwXyAi1ZLagPeVkkUBVHQKlHDUaQsuWXYnGyEO51Q5rgv6zdeFJlc32bjO1KelHURGznCHQgMB4rUlQUff482NWtoIC97Sp2es71nH88vzJf3yEiALXPe9a2XIWq5iAJpOr5SFFYJApDQ84k7UTLp2eiv7pZObd8CoT1RM7D5HepHdULl2wuzOKzugK7rQSgFrdndZEstMwwl0Rd6QH7ecuyidcjebuxT021M98ngzHBnki0muGFpYtWeO88IrqFsvSdb7PwARLZQFERcRfkYmynJ2xLnYLsNGw3Zim8lMqBKj06OrY6obubc5oNxyk3tzYAuhJhouNj6qIMaQYSBxkmDQuMsjJ8ULTaODdJuBNxp6wYPNyR4150dKukBc3fifzhcXQZP5KRudVf1nsDLYvQILifTJzSa01vwyxEhqCAEFZXnGuhANOwPWYuB5iagGErZ1MfNmlBCqYycveVjU0M7JWxZBPvHzLDFb3K9p91lO9URPXsieywBkFiP9RY0kSdktrwF8gHBiqacxPKFoS80o4PRfjr18ZOkT3XKGFiaQ9N3ubU1JzfGa3wvguPwlt0B4xk50jVnI45qeJAMpWBwEC8niMO7DIVBwxN0ERVrLpoOwdsE97xisGz5utDMqGi7IUpHozeCne9HYYxRAbia9skHgxAdsu64O7MSunuKxNs48wP9ClaeFyu6Yq6K8pGfUz54hxuDozlMRsIeawrqzj4CFl7AlDAtHyBLLuZIoAYo4f5evKyTPkcrF32YhhtINxlKGBJzCWKr6CTr76sPrbIbCyca94ymUILS09e0OYM7hlUqbzL4BBcPAWdr76akCmemeWayWbeF5piZUN4Zx8du5QINRnBGZo6T1CQmIQYJEEKKaQYfIiykitvq9v4ITF7TukiP1STzPJpL9SIJcxOBZjFmVaCK9sJeFQasXgJu2pAgdW41h0e8t7ygrlR6VZG1mKvk6QmTSCNOhMVrM5R74ZMlBfsJzvrOFgzoed0qOJg3rV4Te3BthONwYme1h9f5vQGRsxUu3UQnbIx8tIgVCYOsAEt6Jjho58AYYyZSGC5QYwRmqX6qd2O2cl8razz8cECzrGgHHaUsWVynpnW7RyAhZ6WrjN5sXANcRtEoeyK7gSIp9M0KhrItJNh0a6M6TEfFLMKOXV19YjZkJoalv5nQoy9V7dexFzvmtEtwnPwClSSEr9HczxFkpMCKzoCJwnlNlgSqkZ2Pw4mQSt05OmNqiyhnDw1rzTQgdMv8YdbWoD9Wkiln0UXghv816dcV3ZZD2UtF5yIeU4oo39ghfHW8MoccOcp6iasAMaEiENwha9D2p9J2Z0SwsOiS9gtjwVh4KdMc4EKcJdHkQAJ2iL3ZTulpuHmBo5yczOkJh2k1EZ7qaamOccvuxCPQE3Yofh5ztwHCIMFoM9pqRJRrW2ZXY1VbHKotiNSrWXnzunOKRktCEIKHXb4kN3q3iDwpiW5Ndno3I9CDmb5HihMsTom5kUmQIwgJpWZkrSUkakNbIP0eUe9GgosjqsvGNax9is46zedXoMHqwF1Qg7MQfy23NtCAvndwkpwmaoaFmhObg9TpFmI6skEmDrPAw4pArL0LalgPFXiqxoVyOouPdgwk6U1gQaWLG0gWBRki1RPn0Ikw6j4MAvs0jIVetqBNkTnLmVPU7qxHxMv0jxiDta8xz12LfQeOSQmtvjn66W5GwBy4KAvtttUKzgApJQOEUq2ynPXoKmtzlY9UQ9TAapTHm1qQA2FoZOL4GE9lKPAY4VxjsDFk1WvyCvWVlAkVT8qIlhOmnPPw0l7o9DpnMt66Hls6OJuqIfwisBhidht2MMw5nZ76gDbSrYKq3lVfMow9MGz9xfEoxFyWxPNEFlN0VUsLFvA37NwwuiwueIt6HjnGWdYcLe1LqZLtUwtQjLuizq87OYT6WKrGxnjqMFfBhEbdGBRIzioPLlRQijBMTdh0iD2YXwU4gdRMTGyb4ZrYmry0aiKojCTEZ5wFFvnDnIeaz869chZdhSb7QxeLpOZSwjsaSSyAhSY2fU3S08GQnCM7z5qvNHzvFbyFDaCh5h1YNysdyPs6Sml5wGqu475hBQnGkQiHNYbTfNohqIMyPpHju1OlbqP8P77DLy39cFf7j8EE7ExhHwI4tiDjiV0ipIYOuTPQZGe12XBu1kAYDcy4I9YqnSIx184JIOayPTdjJOXJ6DzdBfkePpLOWhHVoYaYYGkhzZgcoaPpjxGFhkZF32s28oOKXLTZx98eE9DRd7riISYn8O7nLpIVNJlH5J7pDdobAdGshjfgY7I5SfeaiI6MiE7rDnnyBXDy0SFId8zmpWYmXyBVw5rIrvy1Q8f0JSHhP6NoPcPeF3wbMVOdJ5d5OBZZrO1QhLeWvpEMzBV03xCK9vP985NRzWvOuWkLQXHNANmGhekspm6DruDSAkgU9qpBgUonwsfszz5TiP3F2CKFDGp3BXTBbRmy9nizz3wULa6Ny3276ILHpiFHl7g6H1Bkpo9g5EROGz7PNwxOw0wBJ74UEyky31CYDzanN45kvbf3crM9V8V0Y8B4zD5VSwW1M3RqEYWcyrRkgArjpEEkaWhyyMC7dCk6DWrbdxlDS9iD6gNXMIA1frZu1UegacgPuCsakfz262CJ6Qdyk6xkN97zH8iZakMnx570lipWm4wSbAlTkQVL88NfLHAnaS3kLeSTLkZFtULiKGahy4HkusJsn55dbxu1h7AtWFF54FOhGzGa9yxxnSqbn56KYASpghTOecg0du6ttjEE7ajbYFlOnF1atHOvSKskY9WZdMuee5yBvKQKIwXvUtyrDF55v8ArlEBHl3WFJf08KoYQrF6yxIxDXxhjG3I32G2Qxlj7o6dunk7yEvkrFeKwYpwqHUYs1UlJwoxpEyIjdppOxOxMysILvdh9eSvCdiq2nufwBeLxQqWoHQKa1kDHDR8gGm4ASDcoy53fZB9WykV0ylvpbzJtsPrKIXyTEV8FLUx3FcUkCCCcBuh8t3hCMpMOuSe0EsSjBaInXtR2h0nL7MGq8lUicCIbeVBfkF4O",
"sock":"1",
"horn":"2",
"glasses":"3",
"cape":"2"
}
```
You should see your requests getting blocked with a **403 Forbidden** response
💡 **Note:** It may take a minute for WAF changes to propagate. If your test request went through successfully, retry a few times until you start receiving 403 errors as WAF kick in effect. 💡
1. Next, let's try a request with a SQL injection attack in the request URI for a **GET /customizations/{id}** request
In Postman, choose the **GET Custom_Unicorn** request and replace the URL with:
```
{{base_url}}/customizations/1; drop table Custom_Unicorns;
```
You should see your requests getting blocked with a **403 Forbidden** response
1. The WAF console gives you metrics and sample requests that are allowed/denied by the WAF rules. You can find this information by going to the WAF console, under **Web ACLs**, select the AWS region and then the WAF we just created.
**Note:** It can take a few minutes before the metrics and sample requests start showing up in the WAF console.

## Extra credit
Use a load test tool like [Artillery](https://artillery.io/docs/getting-started/) to test sending more than 2000 requests in 5 minutes to test the request flood rule.
Note that you will need to configure Artillery to send the `Authorization` headers.
If you have completed **Module 5: Usage Plan**, your API may be throttled first by the usage plan based on the API key.
## Want more?
In this module, we only explored 3 types of AWS WAF rules:
* SQL Injection
* Request size constraint
* Rate limiting
There are a lot more other types of protection you can enable, based on the types of risks you want to defend against
Check out the below to learn about other type of rules:
* AWS WAF Security Automations: [https://aws.amazon.com/solutions/aws-waf-security-automations/](https://aws.amazon.com/solutions/aws-waf-security-automations/)
* Managed WAF Rules from AWS Marketplace: [https://aws.amazon.com/marketplace/solutions/security/waf-managed-rules](https://aws.amazon.com/marketplace/solutions/security/waf-managed-rules)
## Next Step
Return to the workshop [landing page](../../README.md) to pick another module.
================================================
FILE: docs/07-dependency-vulnerability/README.md
================================================
# Module 7: Dependency Vulnerability
When building modern applications, it is common to use different libraries, modules and, in general, different dependencies. Even if we are including a simple dependency, we could end up with tens or even hundreds of sub-dependencies. Just take a look at this page:
- [http://npm.anvaka.com/#!/view/2d/request](http://npm.anvaka.com/#!/view/2d/request)
A simple module used by several applications like *request* could end up with 60 links! If you never thought about the impact a single vulnerable dependency can have, take a look at this [story](https://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/).
In this module, we will cover finding and removing publicly disclosed vulnerable dependencies. There are different tools that can help monitor dependency vulnerabilities. Depending on the programming language, below are some example tools to look into:
- [npm-audit](https://docs.npmjs.com/cli/audit)
- [OWASP Dependency Check](https://www.owasp.org/index.php/OWASP_Dependency_Check)
- [Snyk](https://snyk.io/)
- [Puresec](https://www.puresec.io)
- [Twistlock](https://www.twistlock.com/)
- [Protego](https://www.protego.io/)
During this workshop we will use the first one to review our code.
## Dependency vulnerability with *npm audit*
The tooling for dependency vulnerability checking may vary for different programming languages. With NodeJS, vulnerability checking is now a feature shipped with the `npm` package manager itself after npm acquired NSP (Node Security Platform).
Running `npm audit` command will produce a report of security vulnerabilities, and if available, commands to apply patches to resolve vulnerabilities. In fact `npm audit` automatically runs when you install a package with `npm install`.
1. In the cloud9 environment, go to the node application directory where `package-lock.json` is:
```
cd ~/environment/aws-serverless-security-workshop/src/app
```
1. Run the vulnerability audit:
```
npm audit
```
You should see something like this:

So it turns out the `minimatch:2.0.10` dependency has a known vulnerability. Reading the link on the security advisory in the report can give you more detail on how it can be exploited.
Before we attempt to patch this dependency as suggested by the report, we should ask first: is the application even using *minimatch*? This library compares two different expressions against regular expressions to find out if they match.
In fact, our application is not even using the library thus we should remove it. This can often happen in software projects when a library got pulled into the code base to experiment with something, but later the code evolved and that dependency is no longer required.
But how do we know for sure which dependencies are we using and which ones not so we can safely remove unused dependencies?
### Removing unused dependencies using static analysis
We will install another tool to review our code and report which dependencies are included in our code and are not being used. Maybe they were used in a previous point in time, but not anymore.
1. Run the following command to install [depcheck](https://www.npmjs.com/package/depcheck?activeTab=readme):
```bash
npm install -g depcheck
```
2. Run the tool with the following commands:
```bash
cd ~/environment/aws-serverless-security-workshop/src/app/
depcheck
```
The result should be something like this:
```bash
$ depcheck
Unused dependencies
* babel-core
* babel-plugin-transform-flow-strip-types
* babel-preset-es2017
* minimatch
Missing dependencies
* aws-sdk
```
Therefore, to mitigate this, we should remove these dependencies. Run the following commands:
```bash
npm uninstall babel-core --save
npm uninstall babel-preset-es2017 --save
npm uninstall minimatch --save
npm uninstall babel-plugin-transform-flow-strip-types --save
```
You may also have noticed that there are some **missing dependencies**! This is because we are using the `aws-sdk` package already installed in the [AWS Lambda runtime](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html)
To be sure we removed unused dependencies, run `depcheck` again.
Now your code is free of vulnerabilities from the dependency perspective!
> These steps should be part of your CI/CD pipeline and implemented to be run on every deployment.
## Extra credit
Before October 2019, we used to recommend a free tool called [Puresec Function Shield](https://www.puresec.io/function-shield)
It performs additional runtime protection of your lambda function:
* If not required, block outbound network traffic from your function.
* Disable `/tmp` if it's not used
* Disable the ability to launch child processes from within the Lambda container.
However, it's no longer being maintained as of October 2019 and the project incorporated into a commercial product (see [https://github.com/puresec/FunctionShield](https://github.com/puresec/FunctionShield) )
To look at other commercial product offerings in this area, check out the [Lambda security partner page](https://aws.amazon.com/lambda/partners/?partner-solutions-cards.sort-by=item.additionalFields.partnerName&partner-solutions-cards.sort-order=asc&awsf.partner-solutions-filter-partner-type=use-case%23security-identity-compliance)
## Next Step
Return to the workshop [landing page](../../README.md) to pick another module.
================================================
FILE: docs/08-xray/README.md
================================================
# Module 8: AWS X-Ray
"Insufficient Logging & Monitoring" is one of the Top 10 Application Security Risks ranked by [OWASP](https://www.owasp.org/index.php/Main_Page) in 2017.
AWS X-Ray gives you visibility into the data flow of your microservices architecture and a map of how your application’s underlying components are connected. It's a great tool to troubleshoot performance and debug errors. However, given the ephemeral nature of the infrastructure in a serverless application, this visibility into your application is also critical for the purpose of security:
* It helps you understand the "norm" of the data flow, interdependencies, and performance characteristics of your distributed serverless components. Knowing that is a prerequisite of recognizing when things are not normal.
* During an security incident or post analysis, X-Ray can give you insights into what your code is doing at runtime, what downstream dependency it's making calls to, where the code is spending its time
## Module 8A: Enable X-Ray for Lambda function
In the Cloud9 IDE environment, go to the SAM template (`template.yaml`), find the [**Globals**](https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst) section, which contains settings that all resources in the SAM template share unless explicitly overwritten.
```
Globals:
Function:
Timeout: 30
...
```
Add `Tracing: Active` to the configuration section for lambda functions:
```
Globals:
Function:
Timeout: 30
Tracing: Active
...
```
## Module 8B: Capturing AWS SDK requests with XRay
When our applications make calls to AWS services such as Secrets Manager, DynamoDB, S3 etc., the X-Ray SDK can help tracks the calls downstream and record the request timing, status, etc. about the AWS Service call.
To enable this, you can instrument all AWS SDK clients by wrapping your `aws-sdk` require statement in a call to `AWSXRay.captureAWS`
### Capturing AWS SDK requests in the Lambda authorizer
The Lambda authorizer you added in [**Module 1: Auth**](../01-add-authentication) uses the AWS SDK to look up values from a DynamoDB table. We can instrument the AWS SDK with X-Ray:
1. Install the XRay SDK in the `authorizer/` folder by running in a terminal
```bash
cd ~/environment/aws-serverless-security-workshop/src/authorizer
npm install aws-xray-sdk-core --save
```
1. In `authorizer/index.js`, find the line where the AWS SDK is imported:
```javascript
const AWS = require('aws-sdk');
```
And replace it with:
```javascript
const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
```
### Capturing AWS SDK requests in the backend lambda functions
<details>
<summary><strong>If you haven't gone through Module 2: Secrets </strong></summary><p>
The backend lambda functions currently doesn't use the AWS SDK, so no additional action needed!
</details>
<details>
<summary><strong>If you have gone through Module 2: Secrets </strong></summary><p>
If you have gone through [**Module 2: Secrets**](../02-add-secrets-manager), you would have added the AWS SDK to `dbUtils.js` so the code would retrieve the database username and password from [**AWS Secrets Manager**](https://aws.amazon.com/secrets-manager/)
1. Install the XRay SDK in the `app/` folder by running in a terminal
```bash
cd ~/environment/aws-serverless-security-workshop/src/app
npm install aws-xray-sdk-core --save
```
1. In `app/dbUtils.js`, find the line where the AWS SDK is imported:
```javascript
const AWS = require('aws-sdk');
```
And replace it with:
```javascript
const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
```
</details>
## Module 8C: Deploy lambda changes and test
1. In the terminal, validate the SAM template:
```
cd ~/environment/aws-serverless-security-workshop/src/
sam validate -t template.yaml
```
1. Deploy the updates:
```
aws cloudformation package --output-template-file packaged.yaml --template-file template.yaml --s3-bucket $BUCKET --s3-prefix securityworkshop --region $REGION && aws cloudformation deploy --template-file packaged.yaml --stack-name CustomizeUnicorns --region $REGION --parameter-overrides InitResourceStack=Secure-Serverless --capabilities CAPABILITY_IAM
```
1. Once the deployment finishes, test making API requests again with postman.
1. Go to the [**X-Ray console**](https://console.aws.amazon.com/xray/home), go to the **Service map** tab and refresh. You should start seeing some lambda requests getting captured!
## Module 8D: Enable X-Ray on API Gateway
1. Go to [API Gateway Console](https://console.aws.amazon.com/apigateway/home), and go to the `CustomizeUnicorns` API
1. Go to the **Stages** tab, click on the `dev` stage
1. Find the **Logs/Tracing** tab, check the box for **Enable X-Ray Tracing**, and **Save changes**

1. Redeploy the API by clicking on the **Resources** tab on the left hand side --> **Actions** --> **Deploy API** -> Pick the `dev` stage --> **deploy**.
1. Test making a few making API requests with postman.
1. Go to the [**X-Ray console**](https://console.aws.amazon.com/xray/home), go to the **Service map** tab and refresh

1. Explore the service map. Click on various components, and use **View traces** to see a list of request traces captured by X-Ray

1. Explore the individual traces by clicking into individual requests

## Next Step
Return to the workshop [landing page](../../README.md) to pick another module.
================================================
FILE: docs/10-resource-cleanup/README.md
================================================
# Resource clean up
This page provides instructions for cleaning up the resources created during the preceding modules.
## Resource Cleanup Instructions
1. Delete Cognito User pool domain that you created if you created one in **Module 1: Auth**
<details>
<summary><strong>Click here to expand for detailed instructions </strong></summary><p>
1. Go to the [Cognito Console](https://console.aws.amazon.com/cognito/home)
1. Go to **Manage User Pools**
1. Choose `CustomizeUnicorns-users` user pool
1. Go to **Domain name** under **App integration**
1. Click **Delete domain**
1. Confirm the deletion
</details>
1. Delete API Gateway Usage plan if you created one in **Module 5: Usage Plans**
<details>
<summary><strong>Click here to expand for detailed instructions </strong></summary><p>
1. Go to the [API Gateway Console](https://console.aws.amazon.com/apigateway/home)
1. Go to **Usage plans**
1. Go to the `Basic` Usage Plan
1. In the **Details** tab under **Associated API Stages**, remove the `CustomizeUnicorns` API
1. On the upper right hand corner, click on **Actions** and choose **Delete Usage Plan**
</details>
1. Delete the secret from AWS Secrets Manager if you created one in **Module 2: Secrets**
<details>
<summary><strong>Click here to expand for detailed instructions </strong></summary><p>
1. Go to the [Secrets Manager Console](https://console.aws.amazon.com/secretsmanager/home)
1. Select the `secure-serverless-db-secret` secret
1. In **Actions** select **Delete secret**
1. Enter `7` (minimum waiting period) for waiting period and click **Schedule deletion**
</details>
1. Delete the AWS WAF if you created one in **Module 6: WAF**
<details>
<summary><strong>Click here to expand for detailed instructions </strong></summary><p>
1. Go to the [WAF Console](https://console.aws.amazon.com/waf/home)
1. In the navigation pane, choose **Web ACLs**.
1. Choose the `ProtectUnicorns` web ACL you created in the module 6
1. On the **Rules** tab in the right pane, choose Edit web ACL.
1. Remove all rules from the web ACL by choosing the **x** at the right of the row for each rule. This doesn't delete the rules from AWS WAF, it just removes the rules from this web ACL.
1. Choose **Update**
1. Dissasociate the API gateway from the WAF by going to the section **AWS resources using this web ACL** in the **Rules** tab and clicking the **x** at the right of the API gateway stage
1. On the **Web ACLs** page, confirm that the web ACL that you want to delete is selected, and then choose **Delete**.
1. In the navigation pane, choose **Rules**.
1. Go to each of the 3 rules we created, edit the rule to disassociate all the conditions for each rule
1. Delete the rules
1. Delete the 3 conditions we created in the workshop
</details>
1. Delete `CustomizeUnicorns` CloudFormation stack
<details>
<summary><strong>Click here to expand for detailed instructions </strong></summary><p>
1. Go to the [CloudFormation Console](https://console.aws.amazon.com/cloudformation/home)
1. Select the `CustomizeUnicorns` Stack
1. Under **Actions**, choose **Delete Stack**
</details>
1. Empty the deployment s3 bucket:
<details>
<summary><strong>Click here to expand for detailed instructions </strong></summary><p>
1. Go to the [S3 Console](https://console.aws.amazon.com/s3/home)
1. Search for bucket starting with `secure-serverless-deploymentss3bucket`
1. Click on the checkmark for the bucket and click on the **Empty** button

1. Type in the bucket name to confirm the empty operation
</details>
1. Delete the `Secure-Serverless` resource setup CloudFormation stack
1. CloudWatch Logs
AWS Lambda automatically creates a new log group per function in Amazon CloudWatch Logs and writes logs to it when your function is invoked. You should delete the log group for the lambda functions. (You can search for log groups starting with `/aws/lambda/CustomizeUnicorn` prefix.
1. Delete the RDS snapshot of the aurora database in the RDS console
================================================
FILE: src/apiclient/css/bootstrap.css
================================================
@charset "UTF-8";
/*!
* Bootstrap v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1, .h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1, .h1 {
font-size: 2.5rem;
}
}
h2, .h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2, .h2 {
font-size: 2rem;
}
}
h3, .h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3, .h3 {
font-size: 1.75rem;
}
}
h4, .h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4, .h4 {
font-size: 1.5rem;
}
}
h5, .h5 {
font-size: 1.25rem;
}
h6, .h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small, .small {
font-size: 0.875em;
}
mark, .mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
.lead {
font-size: 1.25rem;
font-weight: 300;
}
.display-1 {
font-size: calc(1.625rem + 4.5vw);
font-weight: 300;
line-height: 1.2;
}
@media (min-width: 1200px) {
.display-1 {
font-size: 5rem;
}
}
.display-2 {
font-size: calc(1.575rem + 3.9vw);
font-weight: 300;
line-height: 1.2;
}
@media (min-width: 1200px) {
.display-2 {
font-size: 4.5rem;
}
}
.display-3 {
font-size: calc(1.525rem + 3.3vw);
font-weight: 300;
line-height: 1.2;
}
@media (min-width: 1200px) {
.display-3 {
font-size: 4rem;
}
}
.display-4 {
font-size: calc(1.475rem + 2.7vw);
font-weight: 300;
line-height: 1.2;
}
@media (min-width: 1200px) {
.display-4 {
font-size: 3.5rem;
}
}
.display-5 {
font-size: calc(1.425rem + 2.1vw);
font-weight: 300;
line-height: 1.2;
}
@media (min-width: 1200px) {
.display-5 {
font-size: 3rem;
}
}
.display-6 {
font-size: calc(1.375rem + 1.5vw);
font-weight: 300;
line-height: 1.2;
}
@media (min-width: 1200px) {
.display-6 {
font-size: 2.5rem;
}
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.list-inline {
padding-left: 0;
list-style: none;
}
.list-inline-item {
display: inline-block;
}
.list-inline-item:not(:last-child) {
margin-right: 0.5rem;
}
.initialism {
font-size: 0.875em;
text-transform: uppercase;
}
.blockquote {
margin-bottom: 1rem;
font-size: 1.25rem;
}
.blockquote > :last-child {
margin-bottom: 0;
}
.blockquote-footer {
margin-top: -1rem;
margin-bottom: 1rem;
font-size: 0.875em;
color: #6c757d;
}
.blockquote-footer::before {
content: "— ";
}
.img-fluid {
max-width: 100%;
height: auto;
}
.img-thumbnail {
padding: 0.25rem;
background-color: var(--bs-body-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
max-width: 100%;
height: auto;
}
.figure {
display: inline-block;
}
.figure-img {
margin-bottom: 0.5rem;
line-height: 1;
}
.figure-caption {
font-size: 0.875em;
color: var(--bs-secondary-color);
}
.container,
.container-fluid,
.container-xxl,
.container-xl,
.container-lg,
.container-md,
.container-sm {
--bs-gutter-x: 1.5rem;
--bs-gutter-y: 0;
width: 100%;
padding-right: calc(var(--bs-gutter-x) * 0.5);
padding-left: calc(var(--bs-gutter-x) * 0.5);
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.container-sm, .container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container-md, .container-sm, .container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container-lg, .container-md, .container-sm, .container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container-xl, .container-lg, .container-md, .container-sm, .container {
max-width: 1140px;
}
}
@media (min-width: 1400px) {
.container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
max-width: 1320px;
}
}
:root {
--bs-breakpoint-xs: 0;
--bs-breakpoint-sm: 576px;
--bs-breakpoint-md: 768px;
--bs-breakpoint-lg: 992px;
--bs-breakpoint-xl: 1200px;
--bs-breakpoint-xxl: 1400px;
}
.row {
--bs-gutter-x: 1.5rem;
--bs-gutter-y: 0;
display: flex;
flex-wrap: wrap;
margin-top: calc(-1 * var(--bs-gutter-y));
margin-right: calc(-0.5 * var(--bs-gutter-x));
margin-left: calc(-0.5 * var(--bs-gutter-x));
}
.row > * {
flex-shrink: 0;
width: 100%;
max-width: 100%;
padding-right: calc(var(--bs-gutter-x) * 0.5);
padding-left: calc(var(--bs-gutter-x) * 0.5);
margin-top: var(--bs-gutter-y);
}
.col {
flex: 1 0 0%;
}
.row-cols-auto > * {
flex: 0 0 auto;
width: auto;
}
.row-cols-1 > * {
flex: 0 0 auto;
width: 100%;
}
.row-cols-2 > * {
flex: 0 0 auto;
width: 50%;
}
.row-cols-3 > * {
flex: 0 0 auto;
width: 33.33333333%;
}
.row-cols-4 > * {
flex: 0 0 auto;
width: 25%;
}
.row-cols-5 > * {
flex: 0 0 auto;
width: 20%;
}
.row-cols-6 > * {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-auto {
flex: 0 0 auto;
width: auto;
}
.col-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-3 {
flex: 0 0 auto;
width: 25%;
}
.col-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-6 {
flex: 0 0 auto;
width: 50%;
}
.col-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-9 {
flex: 0 0 auto;
width: 75%;
}
.col-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-12 {
flex: 0 0 auto;
width: 100%;
}
.offset-1 {
margin-left: 8.33333333%;
}
.offset-2 {
margin-left: 16.66666667%;
}
.offset-3 {
margin-left: 25%;
}
.offset-4 {
margin-left: 33.33333333%;
}
.offset-5 {
margin-left: 41.66666667%;
}
.offset-6 {
margin-left: 50%;
}
.offset-7 {
margin-left: 58.33333333%;
}
.offset-8 {
margin-left: 66.66666667%;
}
.offset-9 {
margin-left: 75%;
}
.offset-10 {
margin-left: 83.33333333%;
}
.offset-11 {
margin-left: 91.66666667%;
}
.g-0,
.gx-0 {
--bs-gutter-x: 0;
}
.g-0,
.gy-0 {
--bs-gutter-y: 0;
}
.g-1,
.gx-1 {
--bs-gutter-x: 0.25rem;
}
.g-1,
.gy-1 {
--bs-gutter-y: 0.25rem;
}
.g-2,
.gx-2 {
--bs-gutter-x: 0.5rem;
}
.g-2,
.gy-2 {
--bs-gutter-y: 0.5rem;
}
.g-3,
.gx-3 {
--bs-gutter-x: 1rem;
}
.g-3,
.gy-3 {
--bs-gutter-y: 1rem;
}
.g-4,
.gx-4 {
--bs-gutter-x: 1.5rem;
}
.g-4,
.gy-4 {
--bs-gutter-y: 1.5rem;
}
.g-5,
.gx-5 {
--bs-gutter-x: 3rem;
}
.g-5,
.gy-5 {
--bs-gutter-y: 3rem;
}
@media (min-width: 576px) {
.col-sm {
flex: 1 0 0%;
}
.row-cols-sm-auto > * {
flex: 0 0 auto;
width: auto;
}
.row-cols-sm-1 > * {
flex: 0 0 auto;
width: 100%;
}
.row-cols-sm-2 > * {
flex: 0 0 auto;
width: 50%;
}
.row-cols-sm-3 > * {
flex: 0 0 auto;
width: 33.33333333%;
}
.row-cols-sm-4 > * {
flex: 0 0 auto;
width: 25%;
}
.row-cols-sm-5 > * {
flex: 0 0 auto;
width: 20%;
}
.row-cols-sm-6 > * {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-sm-auto {
flex: 0 0 auto;
width: auto;
}
.col-sm-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-sm-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-sm-3 {
flex: 0 0 auto;
width: 25%;
}
.col-sm-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-sm-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-sm-6 {
flex: 0 0 auto;
width: 50%;
}
.col-sm-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-sm-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-sm-9 {
flex: 0 0 auto;
width: 75%;
}
.col-sm-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-sm-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-sm-12 {
flex: 0 0 auto;
width: 100%;
}
.offset-sm-0 {
margin-left: 0;
}
.offset-sm-1 {
margin-left: 8.33333333%;
}
.offset-sm-2 {
margin-left: 16.66666667%;
}
.offset-sm-3 {
margin-left: 25%;
}
.offset-sm-4 {
margin-left: 33.33333333%;
}
.offset-sm-5 {
margin-left: 41.66666667%;
}
.offset-sm-6 {
margin-left: 50%;
}
.offset-sm-7 {
margin-left: 58.33333333%;
}
.offset-sm-8 {
margin-left: 66.66666667%;
}
.offset-sm-9 {
margin-left: 75%;
}
.offset-sm-10 {
margin-left: 83.33333333%;
}
.offset-sm-11 {
margin-left: 91.66666667%;
}
.g-sm-0,
.gx-sm-0 {
--bs-gutter-x: 0;
}
.g-sm-0,
.gy-sm-0 {
--bs-gutter-y: 0;
}
.g-sm-1,
.gx-sm-1 {
--bs-gutter-x: 0.25rem;
}
.g-sm-1,
.gy-sm-1 {
--bs-gutter-y: 0.25rem;
}
.g-sm-2,
.gx-sm-2 {
--bs-gutter-x: 0.5rem;
}
.g-sm-2,
.gy-sm-2 {
--bs-gutter-y: 0.5rem;
}
.g-sm-3,
.gx-sm-3 {
--bs-gutter-x: 1rem;
}
.g-sm-3,
.gy-sm-3 {
--bs-gutter-y: 1rem;
}
.g-sm-4,
.gx-sm-4 {
--bs-gutter-x: 1.5rem;
}
.g-sm-4,
.gy-sm-4 {
--bs-gutter-y: 1.5rem;
}
.g-sm-5,
.gx-sm-5 {
--bs-gutter-x: 3rem;
}
.g-sm-5,
.gy-sm-5 {
--bs-gutter-y: 3rem;
}
}
@media (min-width: 768px) {
.col-md {
flex: 1 0 0%;
}
.row-cols-md-auto > * {
flex: 0 0 auto;
width: auto;
}
.row-cols-md-1 > * {
flex: 0 0 auto;
width: 100%;
}
.row-cols-md-2 > * {
flex: 0 0 auto;
width: 50%;
}
.row-cols-md-3 > * {
flex: 0 0 auto;
width: 33.33333333%;
}
.row-cols-md-4 > * {
flex: 0 0 auto;
width: 25%;
}
.row-cols-md-5 > * {
flex: 0 0 auto;
width: 20%;
}
.row-cols-md-6 > * {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-md-auto {
flex: 0 0 auto;
width: auto;
}
.col-md-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-md-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-md-3 {
flex: 0 0 auto;
width: 25%;
}
.col-md-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-md-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-md-6 {
flex: 0 0 auto;
width: 50%;
}
.col-md-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-md-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-md-9 {
flex: 0 0 auto;
width: 75%;
}
.col-md-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-md-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-md-12 {
flex: 0 0 auto;
width: 100%;
}
.offset-md-0 {
margin-left: 0;
}
.offset-md-1 {
margin-left: 8.33333333%;
}
.offset-md-2 {
margin-left: 16.66666667%;
}
.offset-md-3 {
margin-left: 25%;
}
.offset-md-4 {
margin-left: 33.33333333%;
}
.offset-md-5 {
margin-left: 41.66666667%;
}
.offset-md-6 {
margin-left: 50%;
}
.offset-md-7 {
margin-left: 58.33333333%;
}
.offset-md-8 {
margin-left: 66.66666667%;
}
.offset-md-9 {
margin-left: 75%;
}
.offset-md-10 {
margin-left: 83.33333333%;
}
.offset-md-11 {
margin-left: 91.66666667%;
}
.g-md-0,
.gx-md-0 {
--bs-gutter-x: 0;
}
.g-md-0,
.gy-md-0 {
--bs-gutter-y: 0;
}
.g-md-1,
.gx-md-1 {
--bs-gutter-x: 0.25rem;
}
.g-md-1,
.gy-md-1 {
--bs-gutter-y: 0.25rem;
}
.g-md-2,
.gx-md-2 {
--bs-gutter-x: 0.5rem;
}
.g-md-2,
.gy-md-2 {
--bs-gutter-y: 0.5rem;
}
.g-md-3,
.gx-md-3 {
--bs-gutter-x: 1rem;
}
.g-md-3,
.gy-md-3 {
--bs-gutter-y: 1rem;
}
.g-md-4,
.gx-md-4 {
--bs-gutter-x: 1.5rem;
}
.g-md-4,
.gy-md-4 {
--bs-gutter-y: 1.5rem;
}
.g-md-5,
.gx-md-5 {
--bs-gutter-x: 3rem;
}
.g-md-5,
.gy-md-5 {
--bs-gutter-y: 3rem;
}
}
@media (min-width: 992px) {
.col-lg {
flex: 1 0 0%;
}
.row-cols-lg-auto > * {
flex: 0 0 auto;
width: auto;
}
.row-cols-lg-1 > * {
flex: 0 0 auto;
width: 100%;
}
.row-cols-lg-2 > * {
flex: 0 0 auto;
width: 50%;
}
.row-cols-lg-3 > * {
flex: 0 0 auto;
width: 33.33333333%;
}
.row-cols-lg-4 > * {
flex: 0 0 auto;
width: 25%;
}
.row-cols-lg-5 > * {
flex: 0 0 auto;
width: 20%;
}
.row-cols-lg-6 > * {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-lg-auto {
flex: 0 0 auto;
width: auto;
}
.col-lg-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-lg-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-lg-3 {
flex: 0 0 auto;
width: 25%;
}
.col-lg-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-lg-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-lg-6 {
flex: 0 0 auto;
width: 50%;
}
.col-lg-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-lg-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-lg-9 {
flex: 0 0 auto;
width: 75%;
}
.col-lg-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-lg-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-lg-12 {
flex: 0 0 auto;
width: 100%;
}
.offset-lg-0 {
margin-left: 0;
}
.offset-lg-1 {
margin-left: 8.33333333%;
}
.offset-lg-2 {
margin-left: 16.66666667%;
}
.offset-lg-3 {
margin-left: 25%;
}
.offset-lg-4 {
margin-left: 33.33333333%;
}
.offset-lg-5 {
margin-left: 41.66666667%;
}
.offset-lg-6 {
margin-left: 50%;
}
.offset-lg-7 {
margin-left: 58.33333333%;
}
.offset-lg-8 {
margin-left: 66.66666667%;
}
.offset-lg-9 {
margin-left: 75%;
}
.offset-lg-10 {
margin-left: 83.33333333%;
}
.offset-lg-11 {
margin-left: 91.66666667%;
}
.g-lg-0,
.gx-lg-0 {
--bs-gutter-x: 0;
}
.g-lg-0,
.gy-lg-0 {
--bs-gutter-y: 0;
}
.g-lg-1,
.gx-lg-1 {
--bs-gutter-x: 0.25rem;
}
.g-lg-1,
.gy-lg-1 {
--bs-gutter-y: 0.25rem;
}
.g-lg-2,
.gx-lg-2 {
--bs-gutter-x: 0.5rem;
}
.g-lg-2,
.gy-lg-2 {
--bs-gutter-y: 0.5rem;
}
.g-lg-3,
.gx-lg-3 {
--bs-gutter-x: 1rem;
}
.g-lg-3,
.gy-lg-3 {
--bs-gutter-y: 1rem;
}
.g-lg-4,
.gx-lg-4 {
--bs-gutter-x: 1.5rem;
}
.g-lg-4,
.gy-lg-4 {
--bs-gutter-y: 1.5rem;
}
.g-lg-5,
.gx-lg-5 {
--bs-gutter-x: 3rem;
}
.g-lg-5,
.gy-lg-5 {
--bs-gutter-y: 3rem;
}
}
@media (min-width: 1200px) {
.col-xl {
flex: 1 0 0%;
}
.row-cols-xl-auto > * {
flex: 0 0 auto;
width: auto;
}
.row-cols-xl-1 > * {
flex: 0 0 auto;
width: 100%;
}
.row-cols-xl-2 > * {
flex: 0 0 auto;
width: 50%;
}
.row-cols-xl-3 > * {
flex: 0 0 auto;
width: 33.33333333%;
}
.row-cols-xl-4 > * {
flex: 0 0 auto;
width: 25%;
}
.row-cols-xl-5 > * {
flex: 0 0 auto;
width: 20%;
}
.row-cols-xl-6 > * {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-xl-auto {
flex: 0 0 auto;
width: auto;
}
.col-xl-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-xl-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-xl-3 {
flex: 0 0 auto;
width: 25%;
}
.col-xl-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-xl-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-xl-6 {
flex: 0 0 auto;
width: 50%;
}
.col-xl-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-xl-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-xl-9 {
flex: 0 0 auto;
width: 75%;
}
.col-xl-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-xl-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-xl-12 {
flex: 0 0 auto;
width: 100%;
}
.offset-xl-0 {
margin-left: 0;
}
.offset-xl-1 {
margin-left: 8.33333333%;
}
.offset-xl-2 {
margin-left: 16.66666667%;
}
.offset-xl-3 {
margin-left: 25%;
}
.offset-xl-4 {
margin-left: 33.33333333%;
}
.offset-xl-5 {
margin-left: 41.66666667%;
}
.offset-xl-6 {
margin-left: 50%;
}
.offset-xl-7 {
margin-left: 58.33333333%;
}
.offset-xl-8 {
margin-left: 66.66666667%;
}
.offset-xl-9 {
margin-left: 75%;
}
.offset-xl-10 {
margin-left: 83.33333333%;
}
.offset-xl-11 {
margin-left: 91.66666667%;
}
.g-xl-0,
.gx-xl-0 {
--bs-gutter-x: 0;
}
.g-xl-0,
.gy-xl-0 {
--bs-gutter-y: 0;
}
.g-xl-1,
.gx-xl-1 {
--bs-gutter-x: 0.25rem;
}
.g-xl-1,
.gy-xl-1 {
--bs-gutter-y: 0.25rem;
}
.g-xl-2,
.gx-xl-2 {
--bs-gutter-x: 0.5rem;
}
.g-xl-2,
.gy-xl-2 {
--bs-gutter-y: 0.5rem;
}
.g-xl-3,
.gx-xl-3 {
--bs-gutter-x: 1rem;
}
.g-xl-3,
.gy-xl-3 {
--bs-gutter-y: 1rem;
}
.g-xl-4,
.gx-xl-4 {
--bs-gutter-x: 1.5rem;
}
.g-xl-4,
.gy-xl-4 {
--bs-gutter-y: 1.5rem;
}
.g-xl-5,
.gx-xl-5 {
--bs-gutter-x: 3rem;
}
.g-xl-5,
.gy-xl-5 {
--bs-gutter-y: 3rem;
}
}
@media (min-width: 1400px) {
.col-xxl {
flex: 1 0 0%;
}
.row-cols-xxl-auto > * {
flex: 0 0 auto;
width: auto;
}
.row-cols-xxl-1 > * {
flex: 0 0 auto;
width: 100%;
}
.row-cols-xxl-2 > * {
flex: 0 0 auto;
width: 50%;
}
.row-cols-xxl-3 > * {
flex: 0 0 auto;
width: 33.33333333%;
}
.row-cols-xxl-4 > * {
flex: 0 0 auto;
width: 25%;
}
.row-cols-xxl-5 > * {
flex: 0 0 auto;
width: 20%;
}
.row-cols-xxl-6 > * {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-xxl-auto {
flex: 0 0 auto;
width: auto;
}
.col-xxl-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-xxl-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-xxl-3 {
flex: 0 0 auto;
width: 25%;
}
.col-xxl-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-xxl-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-xxl-6 {
flex: 0 0 auto;
width: 50%;
}
.col-xxl-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-xxl-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-xxl-9 {
flex: 0 0 auto;
width: 75%;
}
.col-xxl-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-xxl-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-xxl-12 {
flex: 0 0 auto;
width: 100%;
}
.offset-xxl-0 {
margin-left: 0;
}
.offset-xxl-1 {
margin-left: 8.33333333%;
}
.offset-xxl-2 {
margin-left: 16.66666667%;
}
.offset-xxl-3 {
margin-left: 25%;
}
.offset-xxl-4 {
margin-left: 33.33333333%;
}
.offset-xxl-5 {
margin-left: 41.66666667%;
}
.offset-xxl-6 {
margin-left: 50%;
}
.offset-xxl-7 {
margin-left: 58.33333333%;
}
.offset-xxl-8 {
margin-left: 66.66666667%;
}
.offset-xxl-9 {
margin-left: 75%;
}
.offset-xxl-10 {
margin-left: 83.33333333%;
}
.offset-xxl-11 {
margin-left: 91.66666667%;
}
.g-xxl-0,
.gx-xxl-0 {
--bs-gutter-x: 0;
}
.g-xxl-0,
.gy-xxl-0 {
--bs-gutter-y: 0;
}
.g-xxl-1,
.gx-xxl-1 {
--bs-gutter-x: 0.25rem;
}
.g-xxl-1,
.gy-xxl-1 {
--bs-gutter-y: 0.25rem;
}
.g-xxl-2,
.gx-xxl-2 {
--bs-gutter-x: 0.5rem;
}
.g-xxl-2,
.gy-xxl-2 {
--bs-gutter-y: 0.5rem;
}
.g-xxl-3,
.gx-xxl-3 {
--bs-gutter-x: 1rem;
}
.g-xxl-3,
.gy-xxl-3 {
--bs-gutter-y: 1rem;
}
.g-xxl-4,
.gx-xxl-4 {
--bs-gutter-x: 1.5rem;
}
.g-xxl-4,
.gy-xxl-4 {
--bs-gutter-y: 1.5rem;
}
.g-xxl-5,
.gx-xxl-5 {
--bs-gutter-x: 3rem;
}
.g-xxl-5,
.gy-xxl-5 {
--bs-gutter-y: 3rem;
}
}
.table {
--bs-table-color-type: initial;
--bs-table-bg-type: initial;
--bs-table-color-state: initial;
--bs-table-bg-state: initial;
--bs-table-color: var(--bs-emphasis-color);
--bs-table-bg: var(--bs-body-bg);
--bs-table-border-color: var(--bs-border-color);
--bs-table-accent-bg: transparent;
--bs-table-striped-color: var(--bs-emphasis-color);
--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);
--bs-table-active-color: var(--bs-emphasis-color);
--bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);
--bs-table-hover-color: var(--bs-emphasis-color);
--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);
width: 100%;
margin-bottom: 1rem;
vertical-align: top;
border-color: var(--bs-table-border-color);
}
.table > :not(caption) > * > * {
padding: 0.5rem 0.5rem;
color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));
background-color: var(--bs-table-bg);
border-bottom-width: var(--bs-border-width);
box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)));
}
.table > tbody {
vertical-align: inherit;
}
.table > thead {
vertical-align: bottom;
}
.table-group-divider {
border-top: calc(var(--bs-border-width) * 2) solid currentcolor;
}
.caption-top {
caption-side: top;
}
.table-sm > :not(caption) > * > * {
padding: 0.25rem 0.25rem;
}
.table-bordered > :not(caption) > * {
border-width: var(--bs-border-width) 0;
}
.table-bordered > :not(caption) > * > * {
border-width: 0 var(--bs-border-width);
}
.table-borderless > :not(caption) > * > * {
border-bottom-width: 0;
}
.table-borderless > :not(:first-child) {
border-top-width: 0;
}
.table-striped > tbody > tr:nth-of-type(odd) > * {
--bs-table-color-type: var(--bs-table-striped-color);
--bs-table-bg-type: var(--bs-table-striped-bg);
}
.table-striped-columns > :not(caption) > tr > :nth-child(even) {
--bs-table-color-type: var(--bs-table-striped-color);
--bs-table-bg-type: var(--bs-table-striped-bg);
}
.table-active {
--bs-table-color-state: var(--bs-table-active-color);
--bs-table-bg-state: var(--bs-table-active-bg);
}
.table-hover > tbody > tr:hover > * {
--bs-table-color-state: var(--bs-table-hover-color);
--bs-table-bg-state: var(--bs-table-hover-bg);
}
.table-primary {
--bs-table-color: #000;
--bs-table-bg: #cfe2ff;
--bs-table-border-color: #a6b5cc;
--bs-table-striped-bg: #c5d7f2;
--bs-table-striped-color: #000;
--bs-table-active-bg: #bacbe6;
--bs-table-active-color: #000;
--bs-table-hover-bg: #bfd1ec;
--bs-table-hover-color: #000;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-secondary {
--bs-table-color: #000;
--bs-table-bg: #e2e3e5;
--bs-table-border-color: #b5b6b7;
--bs-table-striped-bg: #d7d8da;
--bs-table-striped-color: #000;
--bs-table-active-bg: #cbccce;
--bs-table-active-color: #000;
--bs-table-hover-bg: #d1d2d4;
--bs-table-hover-color: #000;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-success {
--bs-table-color: #000;
--bs-table-bg: #d1e7dd;
--bs-table-border-color: #a7b9b1;
--bs-table-striped-bg: #c7dbd2;
--bs-table-striped-color: #000;
--bs-table-active-bg: #bcd0c7;
--bs-table-active-color: #000;
--bs-table-hover-bg: #c1d6cc;
--bs-table-hover-color: #000;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-info {
--bs-table-color: #000;
--bs-table-bg: #cff4fc;
--bs-table-border-color: #a6c3ca;
--bs-table-striped-bg: #c5e8ef;
--bs-table-striped-color: #000;
--bs-table-active-bg: #badce3;
--bs-table-active-color: #000;
--bs-table-hover-bg: #bfe2e9;
--bs-table-hover-color: #000;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-warning {
--bs-table-color: #000;
--bs-table-bg: #fff3cd;
--bs-table-border-color: #ccc2a4;
--bs-table-striped-bg: #f2e7c3;
--bs-table-striped-color: #000;
--bs-table-active-bg: #e6dbb9;
--bs-table-active-color: #000;
--bs-table-hover-bg: #ece1be;
--bs-table-hover-color: #000;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-danger {
--bs-table-color: #000;
--bs-table-bg: #f8d7da;
--bs-table-border-color: #c6acae;
--bs-table-striped-bg: #eccccf;
--bs-table-striped-color: #000;
--bs-table-active-bg: #dfc2c4;
--bs-table-active-color: #000;
--bs-table-hover-bg: #e5c7ca;
--bs-table-hover-color: #000;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-light {
--bs-table-color: #000;
--bs-table-bg: #f8f9fa;
--bs-table-border-color: #c6c7c8;
--bs-table-striped-bg: #ecedee;
--bs-table-striped-color: #000;
--bs-table-active-bg: #dfe0e1;
--bs-table-active-color: #000;
--bs-table-hover-bg: #e5e6e7;
--bs-table-hover-color: #000;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-dark {
--bs-table-color: #fff;
--bs-table-bg: #212529;
--bs-table-border-color: #4d5154;
--bs-table-striped-bg: #2c3034;
--bs-table-striped-color: #fff;
--bs-table-active-bg: #373b3e;
--bs-table-active-color: #fff;
--bs-table-hover-bg: #323539;
--bs-table-hover-color: #fff;
color: var(--bs-table-color);
border-color: var(--bs-table-border-color);
}
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
@media (max-width: 575.98px) {
.table-responsive-sm {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 767.98px) {
.table-responsive-md {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 991.98px) {
.table-responsive-lg {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 1199.98px) {
.table-responsive-xl {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 1399.98px) {
.table-responsive-xxl {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
.form-label {
margin-bottom: 0.5rem;
}
.col-form-label {
padding-top: calc(0.375rem + var(--bs-border-width));
padding-bottom: calc(0.375rem + var(--bs-border-width));
margin-bottom: 0;
font-size: inherit;
line-height: 1.5;
}
.col-form-label-lg {
padding-top: calc(0.5rem + var(--bs-border-width));
padding-bottom: calc(0.5rem + var(--bs-border-width));
font-size: 1.25rem;
}
.col-form-label-sm {
padding-top: calc(0.25rem + var(--bs-border-width));
padding-bottom: calc(0.25rem + var(--bs-border-width));
font-size: 0.875rem;
}
.form-text {
margin-top: 0.25rem;
font-size: 0.875em;
color: var(--bs-secondary-color);
}
.form-control {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: var(--bs-body-bg);
background-clip: padding-box;
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.form-control {
transition: none;
}
}
.form-control[type=file] {
overflow: hidden;
}
.form-control[type=file]:not(:disabled):not([readonly]) {
cursor: pointer;
}
.form-control:focus {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-control::-webkit-date-and-time-value {
min-width: 85px;
height: 1.5em;
margin: 0;
}
.form-control::-webkit-datetime-edit {
display: block;
padding: 0;
}
.form-control::-moz-placeholder {
color: var(--bs-secondary-color);
opacity: 1;
}
.form-control::placeholder {
color: var(--bs-secondary-color);
opacity: 1;
}
.form-control:disabled {
background-color: var(--bs-secondary-bg);
opacity: 1;
}
.form-control::-webkit-file-upload-button {
padding: 0.375rem 0.75rem;
margin: -0.375rem -0.75rem;
-webkit-margin-end: 0.75rem;
margin-inline-end: 0.75rem;
color: var(--bs-body-color);
background-color: var(--bs-tertiary-bg);
pointer-events: none;
border-color: inherit;
border-style: solid;
border-width: 0;
border-inline-end-width: var(--bs-border-width);
border-radius: 0;
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control::file-selector-button {
padding: 0.375rem 0.75rem;
margin: -0.375rem -0.75rem;
-webkit-margin-end: 0.75rem;
margin-inline-end: 0.75rem;
color: var(--bs-body-color);
background-color: var(--bs-tertiary-bg);
pointer-events: none;
border-color: inherit;
border-style: solid;
border-width: 0;
border-inline-end-width: var(--bs-border-width);
border-radius: 0;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.form-control::-webkit-file-upload-button {
-webkit-transition: none;
transition: none;
}
.form-control::file-selector-button {
transition: none;
}
}
.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {
background-color: var(--bs-secondary-bg);
}
.form-control:hover:not(:disabled):not([readonly])::file-selector-button {
background-color: var(--bs-secondary-bg);
}
.form-control-plaintext {
display: block;
width: 100%;
padding: 0.375rem 0;
margin-bottom: 0;
line-height: 1.5;
color: var(--bs-body-color);
background-color: transparent;
border: solid transparent;
border-width: var(--bs-border-width) 0;
}
.form-control-plaintext:focus {
outline: 0;
}
.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {
padding-right: 0;
padding-left: 0;
}
.form-control-sm {
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm);
}
.form-control-sm::-webkit-file-upload-button {
padding: 0.25rem 0.5rem;
margin: -0.25rem -0.5rem;
-webkit-margin-end: 0.5rem;
margin-inline-end: 0.5rem;
}
.form-control-sm::file-selector-button {
padding: 0.25rem 0.5rem;
margin: -0.25rem -0.5rem;
-webkit-margin-end: 0.5rem;
margin-inline-end: 0.5rem;
}
.form-control-lg {
min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));
padding: 0.5rem 1rem;
font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg);
}
.form-control-lg::-webkit-file-upload-button {
padding: 0.5rem 1rem;
margin: -0.5rem -1rem;
-webkit-margin-end: 1rem;
margin-inline-end: 1rem;
}
.form-control-lg::file-selector-button {
padding: 0.5rem 1rem;
margin: -0.5rem -1rem;
-webkit-margin-end: 1rem;
margin-inline-end: 1rem;
}
textarea.form-control {
min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
}
textarea.form-control-sm {
min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
}
textarea.form-control-lg {
min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));
}
.form-control-color {
width: 3rem;
height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2));
padding: 0.375rem;
}
.form-control-color:not(:disabled):not([readonly]) {
cursor: pointer;
}
.form-control-color::-moz-color-swatch {
border: 0 !important;
border-radius: var(--bs-border-radius);
}
.form-control-color::-webkit-color-swatch {
border: 0 !important;
border-radius: var(--bs-border-radius);
}
.form-control-color.form-control-sm {
height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
}
.form-control-color.form-control-lg {
height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));
}
.form-select {
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
display: block;
width: 100%;
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: var(--bs-body-bg);
background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none);
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.form-select {
transition: none;
}
}
.form-select:focus {
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-select[multiple], .form-select[size]:not([size="1"]) {
padding-right: 0.75rem;
background-image: none;
}
.form-select:disabled {
background-color: var(--bs-secondary-bg);
}
.form-select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 var(--bs-body-color);
}
.form-select-sm {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 0.5rem;
font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm);
}
.form-select-lg {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg);
}
[data-bs-theme=dark] .form-select {
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}
.form-check {
display: block;
min-height: 1.5rem;
padding-left: 1.5em;
margin-bottom: 0.125rem;
}
.form-check .form-check-input {
float: left;
margin-left: -1.5em;
}
.form-check-reverse {
padding-right: 1.5em;
padding-left: 0;
text-align: right;
}
.form-check-reverse .form-check-input {
float: right;
margin-right: -1.5em;
margin-left: 0;
}
.form-check-input {
--bs-form-check-bg: var(--bs-body-bg);
flex-shrink: 0;
width: 1em;
height: 1em;
margin-top: 0.25em;
vertical-align: top;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: var(--bs-form-check-bg);
background-image: var(--bs-form-check-bg-image);
background-repeat: no-repeat;
background-position: center;
background-size: contain;
border: var(--bs-border-width) solid var(--bs-border-color);
-webkit-print-color-adjust: exact;
color-adjust: exact;
print-color-adjust: exact;
}
.form-check-input[type=checkbox] {
border-radius: 0.25em;
}
.form-check-input[type=radio] {
border-radius: 50%;
}
.form-check-input:active {
filter: brightness(90%);
}
.form-check-input:focus {
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.form-check-input:checked[type=checkbox] {
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
}
.form-check-input:checked[type=radio] {
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e");
}
.form-check-input[type=checkbox]:indeterminate {
background-color: #0d6efd;
border-color: #0d6efd;
--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
}
.form-check-input:disabled {
pointer-events: none;
filter: none;
opacity: 0.5;
}
.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {
cursor: default;
opacity: 0.5;
}
.form-switch {
padding-left: 2.5em;
}
.form-switch .form-check-input {
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
width: 2em;
margin-left: -2.5em;
background-image: var(--bs-form-switch-bg);
background-position: left center;
border-radius: 2em;
transition: background-position 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.form-switch .form-check-input {
transition: none;
}
}
.form-switch .form-check-input:focus {
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e");
}
.form-switch .form-check-input:checked {
background-position: right center;
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
}
.form-switch.form-check-reverse {
padding-right: 2.5em;
padding-left: 0;
}
.form-switch.form-check-reverse .form-check-input {
margin-right: -2.5em;
margin-left: 0;
}
.form-check-inline {
display: inline-block;
margin-right: 1rem;
}
.btn-check {
position: absolute;
clip: rect(0, 0, 0, 0);
pointer-events: none;
}
.btn-check[disabled] + .btn, .btn-check:disabled + .btn {
pointer-events: none;
filter: none;
opacity: 0.65;
}
[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) {
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e");
}
.form-range {
width: 100%;
height: 1.5rem;
padding: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
}
.form-range:focus {
outline: 0;
}
.form-range:focus::-webkit-slider-thumb {
box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-range:focus::-moz-range-thumb {
box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-range::-moz-focus-outer {
border: 0;
}
.form-range::-webkit-slider-thumb {
width: 1rem;
height: 1rem;
margin-top: -0.25rem;
-webkit-appearance: none;
appearance: none;
background-color: #0d6efd;
border: 0;
border-radius: 1rem;
-webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.form-range::-webkit-slider-thumb {
-webkit-transition: none;
transition: none;
}
}
.form-range::-webkit-slider-thumb:active {
background-color: #b6d4fe;
}
.form-range::-webkit-slider-runnable-track {
width: 100%;
height: 0.5rem;
color: transparent;
cursor: pointer;
background-color: var(--bs-secondary-bg);
border-color: transparent;
border-radius: 1rem;
}
.form-range::-moz-range-thumb {
width: 1rem;
height: 1rem;
-moz-appearance: none;
appearance: none;
background-color: #0d6efd;
border: 0;
border-radius: 1rem;
-moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.form-range::-moz-range-thumb {
-moz-transition: none;
transition: none;
}
}
.form-range::-moz-range-thumb:active {
background-color: #b6d4fe;
}
.form-range::-moz-range-track {
width: 100%;
height: 0.5rem;
color: transparent;
cursor: pointer;
background-color: var(--bs-secondary-bg);
border-color: transparent;
border-radius: 1rem;
}
.form-range:disabled {
pointer-events: none;
}
.form-range:disabled::-webkit-slider-thumb {
background-color: var(--bs-secondary-color);
}
.form-range:disabled::-moz-range-thumb {
background-color: var(--bs-secondary-color);
}
.form-floating {
position: relative;
}
.form-floating > .form-control,
.form-floating > .form-control-plaintext,
.form-floating > .form-select {
height: calc(3.5rem + calc(var(--bs-border-width) * 2));
min-height: calc(3.5rem + calc(var(--bs-border-width) * 2));
line-height: 1.25;
}
.form-floating > label {
position: absolute;
top: 0;
left: 0;
z-index: 2;
height: 100%;
padding: 1rem 0.75rem;
overflow: hidden;
text-align: start;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
border: var(--bs-border-width) solid transparent;
transform-origin: 0 0;
transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.form-floating > label {
transition: none;
}
}
.form-floating > .form-control,
.form-floating > .form-control-plaintext {
padding: 1rem 0.75rem;
}
.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder {
color: transparent;
}
.form-floating > .form-control::placeholder,
.form-floating > .form-control-plaintext::placeholder {
color: transparent;
}
.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) {
padding-top: 1.625rem;
padding-bottom: 0.625rem;
}
.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown),
.form-floating > .form-control-plaintext:focus,
.form-floating > .form-control-plaintext:not(:placeholder-shown) {
padding-top: 1.625rem;
padding-bottom: 0.625rem;
}
.form-floating > .form-control:-webkit-autofill,
.form-floating > .form-control-plaintext:-webkit-autofill {
padding-top: 1.625rem;
padding-bottom: 0.625rem;
}
.form-floating > .form-select {
padding-top: 1.625rem;
padding-bottom: 0.625rem;
}
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {
color: rgba(var(--bs-body-color-rgb), 0.65);
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}
.form-floating > .form-control:focus ~ label,
.form-floating > .form-control:not(:placeholder-shown) ~ label,
.form-floating > .form-control-plaintext ~ label,
.form-floating > .form-select ~ label {
color: rgba(var(--bs-body-color-rgb), 0.65);
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}
.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after {
position: absolute;
inset: 1rem 0.375rem;
z-index: -1;
height: 1.5em;
content: "";
background-color: var(--bs-body-bg);
border-radius: var(--bs-border-radius);
}
.form-floating > .form-control:focus ~ label::after,
.form-floating > .form-control:not(:placeholder-shown) ~ label::after,
.form-floating > .form-control-plaintext ~ label::after,
.form-floating > .form-select ~ label::after {
position: absolute;
inset: 1rem 0.375rem;
z-index: -1;
height: 1.5em;
content: "";
background-color: var(--bs-body-bg);
border-radius: var(--bs-border-radius);
}
.form-floating > .form-control:-webkit-autofill ~ label {
color: rgba(var(--bs-body-color-rgb), 0.65);
transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
}
.form-floating > .form-control-plaintext ~ label {
border-width: var(--bs-border-width) 0;
}
.form-floating > :disabled ~ label,
.form-floating > .form-control:disabled ~ label {
color: #6c757d;
}
.form-floating > :disabled ~ label::after,
.form-floating > .form-control:disabled ~ label::after {
background-color: var(--bs-secondary-bg);
}
.input-group {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.input-group > .form-control,
.input-group > .form-select,
.input-group > .form-floating {
position: relative;
flex: 1 1 auto;
width: 1%;
min-width: 0;
}
.input-group > .form-control:focus,
.input-group > .form-select:focus,
.input-group > .form-floating:focus-within {
z-index: 5;
}
.input-group .btn {
position: relative;
z-index: 2;
}
.input-group .btn:focus {
z-index: 5;
}
.input-group-text {
display: flex;
align-items: center;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--bs-body-color);
text-align: center;
white-space: nowrap;
background-color: var(--bs-tertiary-bg);
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
}
.input-group-lg > .form-control,
.input-group-lg > .form-select,
.input-group-lg > .input-group-text,
.input-group-lg > .btn {
padding: 0.5rem 1rem;
font-size: 1.25rem;
border-radius: var(--bs-border-radius-lg);
}
.input-group-sm > .form-control,
.input-group-sm > .form-select,
.input-group-sm > .input-group-text,
.input-group-sm > .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border-radius: var(--bs-border-radius-sm);
}
.input-group-lg > .form-select,
.input-group-sm > .form-select {
padding-right: 3rem;
}
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),
.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3),
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control,
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),
.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4),
.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control,
.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
margin-left: calc(var(--bs-border-width) * -1);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.input-group > .form-floating:not(:first-child) > .form-control,
.input-group > .form-floating:not(:first-child) > .form-select {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.valid-feedback {
display: none;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: var(--bs-form-valid-color);
}
.valid-tooltip {
position: absolute;
top: 100%;
z-index: 5;
display: none;
max-width: 100%;
padding: 0.25rem 0.5rem;
margin-top: 0.1rem;
font-size: 0.875rem;
color: #fff;
background-color: var(--bs-success);
border-radius: var(--bs-border-radius);
}
.was-validated :valid ~ .valid-feedback,
.was-validated :valid ~ .valid-tooltip,
.is-valid ~ .valid-feedback,
.is-valid ~ .valid-tooltip {
display: block;
}
.was-validated .form-control:valid, .form-control.is-valid {
border-color: var(--bs-form-valid-border-color);
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.was-validated .form-control:valid:focus, .form-control.is-valid:focus {
border-color: var(--bs-form-valid-border-color);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
}
.was-validated textarea.form-control:valid, textarea.form-control.is-valid {
padding-right: calc(1.5em + 0.75rem);
background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
}
.was-validated .form-select:valid, .form-select.is-valid {
border-color: var(--bs-form-valid-border-color);
}
.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] {
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
padding-right: 4.125rem;
background-position: right 0.75rem center, center right 2.25rem;
background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.was-validated .form-select:valid:focus, .form-select.is-valid:focus {
border-color: var(--bs-form-valid-border-color);
box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
}
.was-validated .form-control-color:valid, .form-control-color.is-valid {
width: calc(3rem + calc(1.5em + 0.75rem));
}
.was-validated .form-check-input:valid, .form-check-input.is-valid {
border-color: var(--bs-form-valid-border-color);
}
.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {
background-color: var(--bs-form-valid-color);
}
.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {
box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25);
}
.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-c
gitextract_9dgerd3i/
├── .github/
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── LICENSE-SAMPLECODE
├── LICENSE-SUMMARY
├── README.md
├── bootstrap.sh
├── docs/
│ ├── 00-initial-setup/
│ │ └── README.md
│ ├── 01-add-authentication/
│ │ └── README.md
│ ├── 02-add-secrets-manager/
│ │ └── README.md
│ ├── 03-input-validation/
│ │ └── README.md
│ ├── 04-ssl-in-transit/
│ │ └── README.md
│ ├── 05-usage-plan/
│ │ └── README.md
│ ├── 06-waf/
│ │ └── README.md
│ ├── 07-dependency-vulnerability/
│ │ └── README.md
│ ├── 08-xray/
│ │ └── README.md
│ └── 10-resource-cleanup/
│ └── README.md
└── src/
├── apiclient/
│ ├── css/
│ │ ├── bootstrap.css
│ │ ├── main.css
│ │ ├── site.css
│ │ └── vendor/
│ │ └── bootstrap.css
│ ├── index.html
│ └── js/
│ └── main.js
├── app/
│ ├── assets/
│ │ └── rds-ca-2019-root.pem
│ ├── customUnicornAnalytics.js
│ ├── customizeUnicorn.js
│ ├── dbUtils.js
│ ├── httpUtil.js
│ ├── managePartners.js
│ ├── package.json
│ ├── permissions.js
│ └── unicornParts.js
├── authorizer/
│ ├── index.js
│ └── package.json
├── init/
│ ├── db/
│ │ └── queries.sql
│ └── init-template.yml
├── retired/
│ └── bootstrap.sh
├── template.yaml
└── test-events/
└── Customize_Unicorns.postman_collection.json
SYMBOL INDEX (43 symbols across 7 files)
FILE: src/apiclient/js/main.js
function module0getCustomizations (line 1) | async function module0getCustomizations() {
function module1EgetToken (line 17) | async function module1EgetToken() {
function module1EgetClientInfo (line 29) | async function module1EgetClientInfo() {
function module1FgetToken (line 45) | async function module1FgetToken() {
function module1FgetCustomizations (line 56) | async function module1FgetCustomizations() {
function module3DgetToken (line 72) | async function module3DgetToken() {
function module3DnewPartner (line 100) | async function module3DnewPartner() {
function module5getToken (line 117) | async function module5getToken() {
function module5getCustomizations (line 127) | async function module5getCustomizations() {
function module5BgetToken (line 144) | async function module5BgetToken() {
function module5BgetCustomizations (line 154) | async function module5BgetCustomizations() {
function module9CgetToken (line 171) | async function module9CgetToken() {
function module9CgetCustomizations (line 181) | async function module9CgetCustomizations() {
function module9EgetToken (line 198) | async function module9EgetToken() {
function module9EgetCustomizations (line 208) | async function module9EgetCustomizations() {
function module10getToken (line 229) | async function module10getToken() {
function module10getCustomizations (line 239) | async function module10getCustomizations() {
function module10CgetToken (line 256) | async function module10CgetToken() {
function module10CgetCustomizations (line 266) | async function module10CgetCustomizations() {
function getToken (line 283) | async function getToken(tokenURL, clientID, clientSecret) {
function sendRequest (line 304) | async function sendRequest(apiurl, requestBody, token, apitype, apikey) {
function build3Dbody (line 384) | function build3Dbody() {
FILE: src/app/customizeUnicorn.js
function lambda_handler (line 5) | async function lambda_handler(event, context) {
FILE: src/app/dbUtils.js
constant CUSTOM_UNICORN_TABLE (line 3) | const CUSTOM_UNICORN_TABLE = "Custom_Unicorns";
constant PARTNER_COMPANY_TABLE (line 4) | const PARTNER_COMPANY_TABLE = "Companies";
class Database (line 12) | class Database {
method query (line 13) | query(sql, connection, args) {
method close (line 30) | close(connection) {
method connectToDb (line 40) | connectToDb(dbConfig) {
method getDbConfig (line 46) | getDbConfig() {
function executeDBquery (line 60) | function executeDBquery(query) {
FILE: src/app/managePartners.js
constant SCOPES (line 6) | const SCOPES = ['WildRydes/CustomizeUnicorn'];
FILE: src/app/permissions.js
constant ACTIONS (line 12) | const ACTIONS = {
FILE: src/authorizer/index.js
constant CUSTOMIZE_SCOPE (line 16) | const CUSTOMIZE_SCOPE = "WildRydes/CustomizeUnicorn";
constant PARTNER_ADMIN_SCOPE (line 18) | const PARTNER_ADMIN_SCOPE = "WildRydes/ManagePartners";
function ValidateToken (line 60) | function ValidateToken(pems, event, context, callback) {
function AuthPolicy (line 226) | function AuthPolicy(principal, awsAccountId, apiOptions) {
FILE: src/init/db/queries.sql
type Companies (line 4) | CREATE TABLE IF NOT EXISTS Companies (
type Socks (line 10) | CREATE TABLE IF NOT EXISTS Socks (
type Horns (line 16) | CREATE TABLE IF NOT EXISTS Horns (
type Glasses (line 22) | CREATE TABLE IF NOT EXISTS Glasses (
type Capes (line 28) | CREATE TABLE IF NOT EXISTS Capes (
type Custom_Unicorns (line 35) | CREATE TABLE IF NOT EXISTS Custom_Unicorns (
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (776K chars).
[
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 197,
"preview": "*Issue #, if available:*\n\n*Description of changes:*\n\n\nBy submitting this pull request, I confirm that you can use, modif"
},
{
"path": ".gitignore",
"chars": 1636,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 311,
"preview": "## Code of Conduct\nThis project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-condu"
},
{
"path": "CONTRIBUTING.md",
"chars": 4959,
"preview": "# Guidelines for contributing\n\nThank you for your interest in contributing to AWS documentation! We greatly value feedba"
},
{
"path": "LICENSE",
"chars": 14825,
"preview": "Creative Commons Attribution-ShareAlike 4.0 International Public License\n\nBy exercising the Licensed Rights (defined bel"
},
{
"path": "LICENSE-SAMPLECODE",
"chars": 931,
"preview": "Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nPermission is hereby granted, free of charge, t"
},
{
"path": "LICENSE-SUMMARY",
"chars": 333,
"preview": "Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. \n\nThe documentation is made available under the "
},
{
"path": "README.md",
"chars": 1393,
"preview": "# Serverless Security Workshop\r\n\r\n\r\n> **WARNING**: The purpose of the workshop is to provide a starter API which **does "
},
{
"path": "bootstrap.sh",
"chars": 4019,
"preview": "#!/bin/bash\n\n# Cloud9 Bootstrap Script\n# updated 12/6/2022 \n# Tested on Amazon Linux 2\n# Checks for AWS Event or Cloudfo"
},
{
"path": "docs/00-initial-setup/README.md",
"chars": 24139,
"preview": "# Module 0: Initial Setup\n\nIn this set up module, you will deploy a simple serverless application, which you will learn "
},
{
"path": "docs/01-add-authentication/README.md",
"chars": 19756,
"preview": "# Module 1: Add Authentication and Authorization\n\nAs you have probably noticed, the serverless app we just deployed is n"
},
{
"path": "docs/02-add-secrets-manager/README.md",
"chars": 8122,
"preview": "# Module 2: Securely storing our database credentials with AWS Secrets Manager\n\nHardcoding database's credentials and co"
},
{
"path": "docs/03-input-validation/README.md",
"chars": 10987,
"preview": "# Module 3: Input validation on API Gateway\n\nA quote from the OWASP website: \n\n> \"*The most common web application secur"
},
{
"path": "docs/04-ssl-in-transit/README.md",
"chars": 3901,
"preview": "# Module 4: Use SSL in-transit for your DB connections\n\nAlthough we are using VPC and traffic is private within it, some"
},
{
"path": "docs/05-usage-plan/README.md",
"chars": 9950,
"preview": "# Module 5: Usage Plan\n\nYou can leverage Usage Plans with Amazon API Gateway to set limits on request rate for consumers"
},
{
"path": "docs/06-waf/README.md",
"chars": 16162,
"preview": "# Module 6: WAF \n\nAWS WAF is a web application firewall that helps protect your web applications from common web exploit"
},
{
"path": "docs/07-dependency-vulnerability/README.md",
"chars": 5472,
"preview": "# Module 7: Dependency Vulnerability \n\nWhen building modern applications, it is common to use different libraries, modul"
},
{
"path": "docs/08-xray/README.md",
"chars": 5803,
"preview": "# Module 8: AWS X-Ray\n\n\"Insufficient Logging & Monitoring\" is one of the Top 10 Application Security Risks ranked by [OW"
},
{
"path": "docs/10-resource-cleanup/README.md",
"chars": 4077,
"preview": "# Resource clean up\n\nThis page provides instructions for cleaning up the resources created during the preceding modules."
},
{
"path": "src/apiclient/css/bootstrap.css",
"chars": 281043,
"preview": "@charset \"UTF-8\";\n/*!\n * Bootstrap v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors\n * L"
},
{
"path": "src/apiclient/css/main.css",
"chars": 666,
"preview": "table, th, td {\r\n\tborder: 1px solid black;\r\n}\r\ntable {\r\n\twidth: 20%;\r\n\tmin-width: 225px;\r\n\tmargin: 0 auto;\r\n}\r\n#status {"
},
{
"path": "src/apiclient/css/site.css",
"chars": 1372,
"preview": ".button {\r\n background-color: white;\r\n border: none;\r\n color: black;\r\n padding: 20px;\r\n text-align: center;\r\n text"
},
{
"path": "src/apiclient/css/vendor/bootstrap.css",
"chars": 195701,
"preview": "@charset \"UTF-8\";\n/*!\n * Bootstrap v5.0.2 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Co"
},
{
"path": "src/apiclient/index.html",
"chars": 26195,
"preview": "<!DOCTYPE html>\n<html lang=\"en_US\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "src/apiclient/js/main.js",
"chars": 12755,
"preview": "async function module0getCustomizations() {\r\n\r\n var apiurl = $('#apicustomizations0').val();\r\n var requestBody = '';\r\n"
},
{
"path": "src/app/assets/rds-ca-2019-root.pem",
"chars": 1456,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEBjCCAu6gAwIBAgIJAMc0ZzaSUK51MA0GCSqGSIb3DQEBCwUAMIGPMQswCQYD\nVQQGEwJVUzEQMA4GA1UEBwwHU2V"
},
{
"path": "src/app/customUnicornAnalytics.js",
"chars": 1556,
"preview": "import { DynamoDBClient, PutItemCommand } from \"@aws-sdk/client-dynamodb\"; // ES Modules import\nimport dbUtil from \"./db"
},
{
"path": "src/app/customizeUnicorn.js",
"chars": 5677,
"preview": "import dbUtil from \"./dbUtils.js\";\nimport httpUtil from \"./httpUtil.js\";\n// import { permissions } from \"./permissions.j"
},
{
"path": "src/app/dbUtils.js",
"chars": 5106,
"preview": "import mysql from 'mysql';\n\nconst CUSTOM_UNICORN_TABLE = \"Custom_Unicorns\";\nconst PARTNER_COMPANY_TABLE = \"Companies\";\n\n"
},
{
"path": "src/app/httpUtil.js",
"chars": 1812,
"preview": "export const returnFail = (message) => {\n return {\n statusCode: 500,\n headers: {\n \"Access-Co"
},
{
"path": "src/app/managePartners.js",
"chars": 3263,
"preview": "import { DynamoDBClient, PutItemCommand } from \"@aws-sdk/client-dynamodb\"; // ES Modules import\nimport { CognitoIdentity"
},
{
"path": "src/app/package.json",
"chars": 558,
"preview": "{\n \"dependencies\": {\n \"babel-core\": \"*\",\n \"babel-plugin-transform-flow-strip-types\": \"*\",\n \"babel-preset-es201"
},
{
"path": "src/app/permissions.js",
"chars": 4652,
"preview": "import { VerifiedPermissionsClient, IsAuthorizedCommand, CreatePolicyCommand, ListPoliciesCommand, DeletePolicyCommand }"
},
{
"path": "src/app/unicornParts.js",
"chars": 2130,
"preview": "import dbUtil from \"./dbUtils.js\";\nimport httpUtil from \"./httpUtil.js\";\n\nexport const lambda_handler = async (event) =>"
},
{
"path": "src/authorizer/index.js",
"chars": 19948,
"preview": "console.log('Loading function');\n\nimport jwt from 'jsonwebtoken';\nimport request from 'request';\nimport jwkToPem from 'j"
},
{
"path": "src/authorizer/package.json",
"chars": 352,
"preview": "{\n \"name\": \"customize-unicorn-authorizer\",\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"description\": \"\",\n \"main\": \"in"
},
{
"path": "src/init/db/queries.sql",
"chars": 2398,
"preview": "CREATE DATABASE IF NOT EXISTS unicorn_customization;\nUSE unicorn_customization;\n\nCREATE TABLE IF NOT EXISTS Companies (\n"
},
{
"path": "src/init/init-template.yml",
"chars": 6718,
"preview": "AWSTemplateFormatVersion: '2010-09-09'\nDescription: Initial resource setup for serverless security workshop\n\nParameters:"
},
{
"path": "src/retired/bootstrap.sh",
"chars": 3329,
"preview": "#!/bin/bash\n\n# Cloud9 Bootstrap Script\n#\n# Tested on Amazon Linux 2\n#\n# 1. Installs JQ\n# 2. Creates Environment Variable"
},
{
"path": "src/template.yaml",
"chars": 20204,
"preview": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nDescription: Third party API functionality "
},
{
"path": "src/test-events/Customize_Unicorns.postman_collection.json",
"chars": 3956,
"preview": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"7e41857e-3481-4390-821c-395ef8670d36\",\n\t\t\"name\": \"Customize_Unicorns\",\n\t\t\"schema\": \"https"
}
]
About this extraction
This page contains the full source code of the aws-samples/aws-serverless-security-workshop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (720.5 KB), approximately 219.7k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.