├── .devcontainer ├── Dockerfile ├── README.md ├── devcontainer.json ├── docker-compose.yml └── scripts │ ├── dind_add_host.sh │ ├── init_localstack │ └── localstack-entrypoint.sh │ └── terraform_apply_localstack.sh ├── .env.example ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── lambda-filter.yml ├── module-filter.yml ├── production-lambda-functions.conf └── workflows │ ├── backstage-catalog-helper.yml │ ├── build-lambda-images │ └── action.yml │ ├── conventional-commit-lint.yml │ ├── diff-comment.yml │ ├── export_github_data.yml │ ├── get-version │ └── action.yml │ ├── ossf-scorecard.yml │ ├── release_generator.yml │ ├── request-ecs-service-to-use-new-image │ └── action.yml │ ├── request-lambda-functions-to-use-new-image │ └── action.yml │ ├── s3-backup.yml │ ├── scripts │ ├── terraform-variable-check.sh │ └── wait-for-tag.sh │ ├── tag-and-push-docker-images │ └── action.yml │ ├── terraform-security-scan.yml │ ├── terraform-variable-check.yml │ ├── terraform-version-check.yml │ ├── terragrunt-apply-production.yml │ ├── terragrunt-apply-staging.yml │ ├── terragrunt-plan-all-staging.yml │ ├── terragrunt-plan-production-warn-release-exists.yml │ ├── terragrunt-plan-production.yml │ ├── terragrunt-plan-staging.yml │ └── test-lambda-code │ └── action.yml ├── .gitignore ├── .prettierrc.json ├── .release-please-manifest.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── aws ├── .checkov.yml ├── alarms │ ├── athena.tf │ ├── cloudwatch_api.tf │ ├── cloudwatch_app.tf │ ├── cloudwatch_global.tf │ ├── cloudwatch_idp.tf │ ├── dashboards.tf │ ├── dashboards │ │ └── forms_service_health.tmpl.json │ ├── inputs.tf │ ├── lambda.tf │ ├── locals.tf │ └── sns.tf ├── api │ ├── ecs.tf │ ├── inputs.tf │ └── outputs.tf ├── app │ ├── codedeploy.tf │ ├── ecs.tf │ ├── ecs_iam.tf │ ├── ecs_task │ │ └── form_viewer.json │ ├── inputs.tf │ └── outputs.tf ├── cognito │ ├── inputs.tf │ ├── kms.tf │ ├── lambda.tf │ ├── lambda_iam.tf │ ├── outputs.tf │ ├── user_pool.tf │ └── user_pool_iam.tf ├── dynamodb │ ├── dynamo.tf │ ├── inputs.tf │ └── outputs.tf ├── ecr │ ├── ecr.tf │ ├── inputs.tf │ ├── outputs.tf │ └── policy │ │ └── lifecycle.json ├── file_scanning │ ├── inputs.tf │ └── vault_scan_object.tf ├── glue │ ├── crawlers.tf │ ├── data │ │ └── historical_data.csv │ ├── databases.tf │ ├── iam.tf │ ├── inputs.tf │ ├── jobs.tf │ ├── kms.tf │ ├── locals.tf │ ├── outputs.tf │ ├── s3.tf │ └── scripts │ │ ├── historical_etl.py │ │ ├── rds_etl.py │ │ └── submissions_etl.py ├── hosted_zone │ ├── hosted_zone.tf │ └── outputs.tf ├── idp │ ├── certificate.tf │ ├── ecs.tf │ ├── inputs.tf │ ├── lb.tf │ ├── locals.tf │ ├── outputs.tf │ ├── rds.tf │ ├── route53.tf │ ├── ses.tf │ └── waf.tf ├── kms │ ├── kms.tf │ └── outputs.tf ├── lambdas │ ├── api_end_to_end_test.tf │ ├── audit_log.tf │ ├── audit_logs_archiver.tf │ ├── cloudwatch.tf │ ├── form_archiver.tf │ ├── iam.tf │ ├── iam_forms_lambda_client.tf │ ├── inputs.tf │ ├── locals.tf │ ├── nagware.tf │ ├── outputs.tf │ ├── prisma_migration.tf │ ├── reliability.tf │ ├── reliability_dlq_consumer.tf │ ├── response_archiver.tf │ ├── submission.tf │ └── vault_integrity.tf ├── load_balancer │ ├── alarms.tf │ ├── certificates.tf │ ├── cloudfront.tf │ ├── inputs.tf │ ├── kinesis.tf │ ├── lb.tf │ ├── locals.tf │ ├── outputs.tf │ ├── route53.tf │ ├── route53_health_checks.tf │ ├── s3.tf │ ├── shield.tf │ ├── static_website │ │ ├── favicon.ico │ │ ├── index-fr.html │ │ ├── index.html │ │ ├── site-unavailable.svg │ │ └── style.css │ └── waf.tf ├── load_testing │ ├── inputs.tf │ ├── lambda.tf │ └── parameters.tf ├── network │ ├── development_env │ │ ├── network.tf │ │ └── vpc_endpoints.tf │ ├── inputs.tf │ ├── network.tf │ ├── outputs.tf │ ├── security_group_api_end_to_end_test.tf │ ├── security_groups_api.tf │ ├── security_groups_app.tf │ ├── security_groups_glue.tf │ ├── security_groups_idp.tf │ ├── security_groups_lambda.tf │ ├── vpc_endpoints.tf │ └── vpc_flow_log.tf ├── oidc_roles │ ├── iam_policies.tf │ └── iam_roles.tf ├── pr_review │ ├── ecr.tf │ ├── iam.tf │ ├── inputs.tf │ └── security_groups.tf ├── rds │ ├── inputs.tf │ ├── outputs.tf │ ├── rds.tf │ └── secrets.tf ├── redis │ ├── inputs.tf │ ├── outputs.tf │ └── redis.tf ├── s3 │ ├── data_lake.tf │ ├── locals.tf │ ├── outputs.tf │ └── s3.tf ├── secrets │ ├── inputs.tf │ ├── outputs.tf │ └── secrets.tf ├── sns │ ├── inputs.tf │ ├── outputs.tf │ └── sns.tf ├── sqs │ ├── outputs.tf │ └── sqs.tf └── vpn │ ├── certificate.tf │ ├── event.tf │ ├── inputs.tf │ ├── lambda.tf │ ├── lambda │ └── code │ │ ├── main.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── yarn.lock │ └── vpn.tf ├── catalog-info.yaml ├── docs └── datalake.md ├── env ├── cloud │ ├── alarms │ │ └── terragrunt.hcl │ ├── api │ │ └── terragrunt.hcl │ ├── app │ │ └── terragrunt.hcl │ ├── cognito │ │ └── terragrunt.hcl │ ├── dynamodb │ │ └── terragrunt.hcl │ ├── ecr │ │ └── terragrunt.hcl │ ├── file_scanning │ │ └── terragrunt.hcl │ ├── glue │ │ └── terragrunt.hcl │ ├── hosted_zone │ │ └── terragrunt.hcl │ ├── idp │ │ └── terragrunt.hcl │ ├── kms │ │ └── terragrunt.hcl │ ├── lambdas │ │ └── terragrunt.hcl │ ├── load_balancer │ │ └── terragrunt.hcl │ ├── load_testing │ │ └── terragrunt.hcl │ ├── network │ │ └── terragrunt.hcl │ ├── oidc_roles │ │ └── terragrunt.hcl │ ├── pr_review │ │ └── terragrunt.hcl │ ├── rds │ │ └── terragrunt.hcl │ ├── redis │ │ └── terragrunt.hcl │ ├── s3 │ │ └── terragrunt.hcl │ ├── secrets │ │ └── terragrunt.hcl │ ├── sns │ │ └── terragrunt.hcl │ ├── sqs │ │ └── terragrunt.hcl │ └── vpn │ │ └── terragrunt.hcl ├── common │ ├── common_variables.tf │ └── provider.tf └── root.hcl ├── idp ├── Makefile └── docker │ ├── Dockerfile │ ├── config.yaml │ └── steps.yaml ├── lambda-code ├── api-end-to-end-test │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── apiClient.ts │ │ │ ├── idpClient.ts │ │ │ ├── submissionGenerator.ts │ │ │ └── utils.ts │ │ ├── main.ts │ │ ├── tests │ │ │ ├── commonUseCaseTest.ts │ │ │ └── rateLimiterTest.ts │ │ └── types │ │ │ ├── api.d.ts │ │ │ └── test.d.ts │ ├── tsconfig.json │ └── yarn.lock ├── audit-logs-archiver │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── audit-logs │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── cognito-email-sender │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── cognito-pre-sign-up │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── deps.sh ├── form-archiver │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── lib │ │ │ └── templates.ts │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── load-testing │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── main.py │ ├── requirements.txt │ └── tests │ │ ├── behaviours │ │ ├── api.py │ │ ├── idp.py │ │ └── submit.py │ │ ├── locust_test_file.py │ │ └── utils │ │ ├── data_structures.py │ │ ├── form_submission_decrypter.py │ │ ├── form_submission_generator.py │ │ ├── jwt_generator.py │ │ └── task_set.py ├── nagware │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── dynamodbDataLayer.ts │ │ │ ├── emailNotification.ts │ │ │ ├── overdueResponseCache.ts │ │ │ ├── redisConnector.ts │ │ │ └── templates.ts │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── notify-slack │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── main.ts │ │ └── utils.ts │ ├── tests │ │ └── notify-slack.test.ts │ ├── tsconfig.json │ └── yarn.lock ├── prisma-migration │ ├── Dockerfile │ ├── base_schema.prisma │ ├── bootstrap │ └── prisma.sh ├── reliability-dlq-consumer │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── reliability │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── dataLayer.ts │ │ │ ├── markdown.ts │ │ │ ├── notifyProcessing.ts │ │ │ ├── s3FileInput.ts │ │ │ ├── templates.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ └── vaultProcessing.ts │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── response-archiver │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ ├── lib │ │ │ └── fileAttachments.ts │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── submission │ ├── .yarn │ │ ├── install-state.gz │ │ └── releases │ │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock └── vault-integrity │ ├── .yarn │ ├── install-state.gz │ └── releases │ │ └── yarn-4.9.1.cjs │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── package.json │ ├── src │ └── main.ts │ ├── tsconfig.json │ └── yarn.lock ├── local_dev_files ├── build_and_deploy_lambda.sh ├── build_dev_env.sh ├── certificates │ └── client-config.ovpn ├── clean_terragrunt.sh ├── connect_vpn.sh ├── create_vpn_certs.sh ├── destroy_dev_env.sh ├── sso_login.sh └── tf │ └── blank.tf ├── readme_images ├── GSI_Vault_StatusCreatedAt.png └── Vault.png ├── release-please-config.json ├── renovate.json └── version.txt /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/base:buster@sha256:9d0efa8b32ab5a40252ed9aa3f1e8d5d989c979b627fcc9fb9e6f11db73c3a0a 2 | 3 | ARG USERNAME=vscode 4 | ARG AWS_SAM_VERSION 5 | ARG AWS_SAM_CHECKSUM 6 | 7 | # Install packages 8 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | && apt-get -y install --no-install-recommends build-essential ca-certificates curl dnsutils git gnupg2 jq libffi-dev make openssh-client python3-dev python3-pip vim zsh \ 10 | && apt-get autoremove -y && apt-get clean -y 11 | 12 | # Install aws-sam 13 | ARG AWS_SAM_SRC=https://github.com/aws/aws-sam-cli/releases/download 14 | RUN curl -Lo aws-sam-cli.zip "${AWS_SAM_SRC}/v${AWS_SAM_VERSION}/aws-sam-cli-linux-x86_64.zip" \ 15 | && echo "${AWS_SAM_CHECKSUM} aws-sam-cli.zip" | sha256sum --check \ 16 | && unzip aws-sam-cli.zip -d sam-installation \ 17 | && ./sam-installation/install \ 18 | && rm -r aws-sam* \ 19 | && rm -r sam-installation* 20 | 21 | # Setup AWS Credentials 22 | RUN mkdir -p /home/vscode/.aws 23 | 24 | RUN echo "\n\ 25 | [default]\n\ 26 | aws_access_key_id=foo\n\ 27 | aws_secret_access_key=bar\n\ 28 | " >> /home/vscode/.aws/credentials 29 | 30 | RUN echo "\n\ 31 | [default]\n\ 32 | region=ca-central-1\n\ 33 | output=json\n\ 34 | " >> /home/vscode/.aws/config 35 | 36 | ENV SHELL /bin/zsh 37 | 38 | # Setup aliases and autocomplete 39 | RUN echo "\n\ 40 | complete -C /usr/bin/aws_completer aws\n\ 41 | complete -C /usr/local/bin/terraform terraform\n\ 42 | complete -C /usr/local/bin/terraform terragrunt\n\ 43 | alias tf='terraform'\n\ 44 | alias tg='terragrunt'\n\ 45 | alias ll='la -la' \n\ 46 | alias laws='aws --endpoint-url=http://localstack:4566 --region=ca-central-1'" >> /home/vscode/.zshrc -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 1. [SSH keys](#SSH-keys) 3 | 1. [GPG commit signing](#GPG-commit-signing) 4 | 5 | ## SSH keys 6 | To make your keys available in the devcontainer, you'll need: 7 | 1. the `ssh-agent` running on the host; and 8 | 2. your key(s) added to it. 9 | 10 | To make this happen: 11 | 1. Auto-start the ssh-agent: 12 | ```sh 13 | # Update ~/.bashrc or ~/.zshrc 14 | [ -z "$SSH_AUTH_SOCK" ] && eval "$(ssh-agent -s)" 15 | ``` 16 | 2. Add your key: 17 | ```sh 18 | # Update ~/.ssh/config 19 | Host * 20 | UseKeychain yes 21 | AddKeysToAgent yes 22 | IdentityFile ~/.ssh/ 23 | 24 | # Or manually add the key 25 | ssh-add -K ~/.ssh/ 26 | ``` 27 | 28 | ## GPG commit signing 29 | You'll need a [graphical pinentry program on Mac/Windows](https://github.com/microsoft/vscode-remote-release/issues/3168#issuecomment-654637826) to capture your GPG key's password. On your host: 30 | ```sh 31 | brew install pinentry-mac 32 | echo "pinentry-program /usr/local/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf 33 | gpgconf --kill gpg-agent # force a restart to reload the config 34 | ``` 35 | Test in the devcontainer: 36 | ```sh 37 | gpg --list-secret-keys # you should see your key(s) 38 | echo "mello" | gpg --clear-sign # you should be prompted for your key's password 39 | ``` -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forms-terraform", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "iac", 5 | "workspaceFolder": "/workspace", 6 | 7 | "features": { 8 | "docker-in-docker": { 9 | "version": "latest", 10 | "moby": true 11 | }, 12 | "terraform": { 13 | "version": "1.11.2", 14 | "tflint": "latest", 15 | "terragrunt": "0.75.10" 16 | }, 17 | "aws-cli": { 18 | "version": "2.5.6" 19 | }, 20 | "node": { 21 | "version": "lts" 22 | }, 23 | "ghcr.io/devcontainers/features/python:1": { 24 | "version": "3.12", 25 | "installTools": false 26 | } 27 | }, 28 | 29 | "customizations": { 30 | "vscode": { 31 | "settings": { 32 | "[terraform]": { 33 | "editor.formatOnSave": true 34 | } 35 | }, 36 | "extensions": [ 37 | "hashicorp.terraform", 38 | "redhat.vscode-yaml", 39 | "github.copilot", 40 | "github.vscode-github-actions" 41 | ] 42 | } 43 | }, 44 | 45 | // Add the IDs of extensions you want installed when the container is created. 46 | 47 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 48 | "remoteUser": "vscode", 49 | "forwardPorts": [3001] 50 | } 51 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | iac: 5 | build: 6 | context: .. 7 | dockerfile: .devcontainer/Dockerfile 8 | args: 9 | AWS_SAM_VERSION: "1.50.0" 10 | AWS_SAM_CHECKSUM: "093fc2cc40b098321dcc8635abe468642b72f8658821c9243a9357e400734207" 11 | volumes: 12 | - ..:/workspace:cached 13 | command: sleep infinity 14 | environment: 15 | LOCALSTACK: "True" 16 | DEVCONTAINER: "True" 17 | TF_VAR_cognito_client_id: "" 18 | TF_VAR_cognito_endpoint_url: "" 19 | TF_VAR_cognito_user_pool_arn: "" 20 | TF_VAR_email_address_contact_us: "" 21 | TF_VAR_email_address_support: "" 22 | TF_VAR_localstack_host: "host.docker.internal" 23 | TF_VAR_region: "ca-central-1" 24 | 25 | localstack: 26 | image: localstack/localstack@sha256:83138e8bbd06361df943284a59c3ba5cbfec7ba1b9963c419a92a0e80820d3d5 27 | hostname: localstack 28 | volumes: 29 | - "./data:/tmp/localstack" 30 | - "/var/run/docker.sock:/var/run/docker.sock" 31 | - "./scripts/init_localstack:/docker-entrypoint-initaws.d" 32 | ports: 33 | - 4566:4566 34 | environment: 35 | - SERVICES=ec2,dynamodb,kms,sqs,s3,sns 36 | - DATA_DIR=/tmp/localstack/data 37 | - DOCKER_HOST=unix:///var/run/docker.sock` 38 | - DEBUG=1 39 | -------------------------------------------------------------------------------- /.devcontainer/scripts/dind_add_host.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Listens for Docker container `start` events and updates the new 5 | # container's `/etc/hosts` file with an entry for `host.docker.internal` 6 | # that references the host running Docker. 7 | # 8 | # This is required because the devcontainer is running the AWS SAM lambda 9 | # containers using docker-in-docker (DinD), which does not add 10 | # this host entry. 11 | 12 | HOST_IP="$(dig +short host.docker.internal)" 13 | docker events \ 14 | --filter 'event=start' \ 15 | --format '{{.ID}}' | \ 16 | while read ID; do docker exec $ID sh -c "echo '$HOST_IP host.docker.internal' >> /etc/hosts"; done; -------------------------------------------------------------------------------- /.devcontainer/scripts/init_localstack/localstack-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | printf "Configuring localstack components..." 4 | sleep 5; 5 | 6 | function laws { 7 | aws --endpoint-url=http://localstack:4566 --region=ca-central-1 "$@" 8 | } 9 | 10 | set -x 11 | 12 | cwd=$(pwd) 13 | 14 | printf "Setting Connection Info..." 15 | laws configure set aws_access_key_id foo 16 | laws configure set aws_secret_access_key bar 17 | laws configure set region ca-central-1 18 | laws configure set output json 19 | 20 | set +x 21 | -------------------------------------------------------------------------------- /.devcontainer/scripts/terraform_apply_localstack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export TF_VAR_localstack_host="localstack" 4 | basedir=$(pwd) 5 | 6 | printf "Configuring localstack components via terraform...\n" 7 | 8 | printf "Purging stale localstack related files\n" 9 | find $basedir/env/local -type d -name .terragrunt-cache -prune -exec rm -rf {} \; 10 | find $basedir/env/local -type f -name terraform.tfstate -prune -exec rm -rf {} \; 11 | rm -rf $basedir/.devcontainer/data/data 12 | 13 | printf "Setting up local KMS...\n" 14 | cd $basedir/env/local/kms 15 | terragrunt apply --terragrunt-non-interactive -auto-approve 16 | 17 | printf "Creating SQS queue...\n" 18 | cd $basedir/env/local/sqs 19 | terragrunt apply --terragrunt-non-interactive -auto-approve 20 | 21 | printf "Creating SNS queue...\n" 22 | cd $basedir/env/local/sns 23 | terragrunt apply --terragrunt-non-interactive -auto-approve 24 | 25 | printf "Creating the DynamoDB database...\n" 26 | cd $basedir/env/local/dynamodb 27 | terragrunt apply --terragrunt-non-interactive -auto-approve 28 | 29 | printf "Creating the S3 buckets...\n" 30 | cd $basedir/env/local/app 31 | terragrunt apply --terragrunt-non-interactive -auto-approve 32 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | AWS_ACCOUNT_ID={your develoment environment aws account} 2 | STAGING_AWS_ACCOUNT_ID={GCForms staging account ID} 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es2021: true, 5 | }, 6 | extends: "eslint:recommended", 7 | overrides: [], 8 | parserOptions: { 9 | ecmaVersion: "latest", 10 | sourceType: "module", 11 | }, 12 | rules: {}, 13 | }; 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Users that are allowed to approve a release PR 2 | 3 | version.txt @bryan-robitaille @dsamojlenko @timarney @craigzour 4 | -------------------------------------------------------------------------------- /.github/lambda-filter.yml: -------------------------------------------------------------------------------- 1 | audit-logs: 2 | - "lambda-code/audit-logs/**" 3 | audit-logs-archiver: 4 | - "lambda-code/audit-logs-archiver/**" 5 | cognito-email-sender: 6 | - "lambda-code/cognito-email-sender/**" 7 | cognito-pre-sign-up: 8 | - "lambda-code/cognito-pre-sign-up/**" 9 | form-archiver: 10 | - "lambda-code/form-archiver/**" 11 | load-testing: 12 | - "lambda-code/load-testing/**" 13 | nagware: 14 | - "lambda-code/nagware/**" 15 | notify-slack: 16 | - "lambda-code/notify-slack/**" 17 | reliability: 18 | - "lambda-code/reliability/**" 19 | reliability-dlq-consumer: 20 | - "lambda-code/reliability-dlq-consumer/**" 21 | response-archiver: 22 | - "lambda-code/response-archiver/**" 23 | submission: 24 | - "lambda-code/submission/**" 25 | vault-integrity: 26 | - "lambda-code/vault-integrity/**" 27 | prisma-migration: 28 | - "lambda-code/prisma-migration/**" 29 | api-end-to-end-test: 30 | - "lambda-code/api-end-to-end-test/**" 31 | -------------------------------------------------------------------------------- /.github/module-filter.yml: -------------------------------------------------------------------------------- 1 | common: &common 2 | - "env/common/**" 3 | - "env/terragrunt.hcl" 4 | - ".github/workflows/terragrunt-plan-staging.yml" 5 | - ".github/workflows/terragrunt-apply-staging.yml" 6 | alarms: 7 | - *common 8 | - "aws/alarms/**" 9 | - "env/cloud/alarms/**" 10 | api: 11 | - *common 12 | - "aws/api/**" 13 | - "env/cloud/api/**" 14 | app: 15 | - *common 16 | - "aws/app/**" 17 | - "env/cloud/app/**" 18 | cognito: 19 | - *common 20 | - "aws/cognito/**" 21 | - "env/cloud/cognito/**" 22 | dynamodb: 23 | - *common 24 | - "aws/dynamodb/**" 25 | - "env/cloud/dynamodb/**" 26 | ecr: 27 | - *common 28 | - "aws/ecr/**" 29 | - "env/cloud/ecr/**" 30 | file_scanning: 31 | - *common 32 | - "aws/file_scanning/**" 33 | - "env/cloud/file_scanning/**" 34 | glue: 35 | - *common 36 | - "aws/glue/**" 37 | - "env/cloud/glue/**" 38 | hosted_zone: 39 | - *common 40 | - "aws/hosted_zone/**" 41 | - "env/cloud/hosted_zone/**" 42 | idp: 43 | - *common 44 | - "aws/idp/**" 45 | - "env/cloud/idp/**" 46 | kms: 47 | - *common 48 | - "aws/kms/**" 49 | - "env/cloud/kms/**" 50 | lambdas: 51 | - *common 52 | - "aws/lambdas/**" 53 | - "env/cloud/lambdas/**" 54 | load_balancer: 55 | - *common 56 | - "aws/load_balancer/**" 57 | - "env/cloud/load_balancer/**" 58 | load_testing: 59 | - *common 60 | - "aws/load_testing/**" 61 | - "env/cloud/load_testing/**" 62 | network: 63 | - *common 64 | - "aws/network/**" 65 | - "env/cloud/network/**" 66 | oidc_roles: 67 | - *common 68 | - "aws/oidc_roles/**" 69 | - "env/cloud/oidc_roles/**" 70 | rds: 71 | - *common 72 | - "aws/rds/**" 73 | - "env/cloud/rds/**" 74 | redis: 75 | - *common 76 | - "aws/redis/**" 77 | - "env/cloud/redis/**" 78 | s3: 79 | - *common 80 | - "aws/s3/**" 81 | - "env/cloud/s3/**" 82 | secrets: 83 | - *common 84 | - "aws/secrets/**" 85 | - "env/cloud/secrets/**" 86 | sns: 87 | - *common 88 | - "aws/sns/**" 89 | - "env/cloud/sns/**" 90 | sqs: 91 | - *common 92 | - "aws/sqs/**" 93 | - "env/cloud/sqs/**" 94 | -------------------------------------------------------------------------------- /.github/production-lambda-functions.conf: -------------------------------------------------------------------------------- 1 | [ 2 | "audit-logs", 3 | "audit-logs-archiver", 4 | "cognito-email-sender", 5 | "cognito-pre-sign-up", 6 | "form-archiver", 7 | "nagware", 8 | "notify-slack", 9 | "reliability", 10 | "reliability-dlq-consumer", 11 | "response-archiver", 12 | "submission", 13 | "vault-integrity", 14 | "prisma-migration", 15 | "api-end-to-end-test" 16 | ] 17 | -------------------------------------------------------------------------------- /.github/workflows/backstage-catalog-helper.yml: -------------------------------------------------------------------------------- 1 | name: Backstage Catalog Info Helper 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | update-catalog-info: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Actions 12 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 13 | with: 14 | fetch-depth: 0 15 | - name: Run Backstage Catalog Info Helper 16 | uses: cds-snc/backstage-catalog-info-helper-action@v0.3.1 17 | with: 18 | github_app_id: ${{ secrets.SRE_BOT_RW_APP_ID }} 19 | github_app_private_key: ${{ secrets.SRE_BOT_RW_PRIVATE_KEY }} 20 | github_organization: cds-snc 21 | - name: impersonate Read/Write GH App 22 | uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a 23 | id: generate_token 24 | with: 25 | app_id: ${{ secrets.SRE_BOT_RW_APP_ID }} 26 | private_key: ${{ secrets.SRE_BOT_RW_PRIVATE_KEY }} 27 | - name: Create pull request 28 | uses: peter-evans/create-pull-request@6cd32fd93684475c31847837f87bb135d40a2b79 # v7.0.3 29 | with: 30 | token: ${{ steps.generate_token.outputs.token}} 31 | sign-commits: true 32 | commit-message: 'Add catalog-info.yaml' 33 | branch: 'backstage/catalog-info' 34 | title: 'Add catalog-info.yaml' 35 | body: 'Adding a basic catalog-info.yaml to start populating the backstage catalog with your components.' 36 | labels: 'backstage' 37 | add-paths: | 38 | catalog-info.yaml -------------------------------------------------------------------------------- /.github/workflows/build-lambda-images/action.yml: -------------------------------------------------------------------------------- 1 | name: Build Lambda images 2 | 3 | inputs: 4 | lambda-directory: 5 | required: true 6 | lambda-name: 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - run: docker build -t $LAMBDA_NAME-lambda . 13 | env: 14 | LAMBDA_NAME: ${{ inputs.lambda-name }} 15 | working-directory: ${{ inputs.lambda-directory }} 16 | shell: bash 17 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commit-lint.yml: -------------------------------------------------------------------------------- 1 | name: Conventional commit lint 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | conventional-commit-lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | 13 | - name: Setup Node.js 14 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 15 | with: 16 | node-version: '20.x' 17 | 18 | - name: Setup commitlint 19 | run: | 20 | npm install -g @commitlint/config-conventional @commitlint/cli 21 | 22 | - name: Validate last commit 23 | run: | 24 | npx commitlint \ 25 | --extends '@commitlint/config-conventional' \ 26 | --last \ 27 | --verbose 28 | -------------------------------------------------------------------------------- /.github/workflows/diff-comment.yml: -------------------------------------------------------------------------------- 1 | name: "Diff comment" 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | base: 7 | required: true 8 | type: string 9 | description: The base for the compare (SHA, tag or branch) 10 | ref: 11 | required: true 12 | type: string 13 | description: The reference to compare against base (SHA, tag or branch) 14 | 15 | jobs: 16 | tag-compare: 17 | runs-on: ubuntu-latest 18 | steps: 19 | 20 | - name: Delete previous comments 21 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | script: | 25 | const { data: comments } = await github.rest.issues.listComments({...context.repo, issue_number: context.issue.number}); 26 | const comment = comments.find(comment => comment.user.type === "Bot" && comment.body.indexOf("Version diff") > -1); 27 | if (comment) { 28 | await github.rest.issues.deleteComment({...context.repo, comment_id: comment.id}); 29 | } 30 | 31 | - name: Add version diff comment 32 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | script: | 36 | github.rest.issues.createComment({ 37 | ...context.repo, 38 | issue_number: context.issue.number, 39 | body: "## Version diff\nhttps://github.com/cds-snc/forms-terraform/compare/${{ inputs.base }}...${{ inputs.ref }}" 40 | }) 41 | -------------------------------------------------------------------------------- /.github/workflows/export_github_data.yml: -------------------------------------------------------------------------------- 1 | name: GitHub repository metadata exporter 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "20 7 * * *" 6 | 7 | jobs: 8 | export-data: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Audit DNS requests 12 | uses: cds-snc/dns-proxy-action@main 13 | env: 14 | DNS_PROXY_FORWARDTOSENTINEL: "true" 15 | DNS_PROXY_LOGANALYTICSWORKSPACEID: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} 16 | DNS_PROXY_LOGANALYTICSSHAREDKEY: ${{ secrets.LOG_ANALYTICS_WORKSPACE_KEY }} 17 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 18 | - name: Export Data 19 | uses: cds-snc/github-repository-metadata-exporter@main 20 | with: 21 | github-app-id: ${{ secrets.SRE_BOT_RO_APP_ID }} 22 | github-app-installation-id: ${{ secrets.SRE_BOT_RO_INSTALLATION_ID }} 23 | github-app-private-key: ${{ secrets.SRE_BOT_RO_PRIVATE_KEY }} 24 | log-analytics-workspace-id: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} 25 | log-analytics-workspace-key: ${{ secrets.LOG_ANALYTICS_WORKSPACE_KEY }} 26 | -------------------------------------------------------------------------------- /.github/workflows/get-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Get infrastructure version to deploy 2 | 3 | inputs: 4 | is-tagged: 5 | description: "Is this for the release of a tagged version? This action will wait for the tag to exist if so." 6 | required: true 7 | outputs: 8 | version: 9 | description: "Infrastructure version to deploy" 10 | value: ${{ steps.output-version.outputs.version }} 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Get version, release PR branch 16 | if: inputs.is-tagged == 'false' && startsWith(github.head_ref, 'release-please--') 17 | env: 18 | GITHUB_HEAD_REF: ${{ github.head_ref }} 19 | run: echo "version=$GITHUB_HEAD_REF" >> $GITHUB_ENV 20 | shell: bash 21 | 22 | - name: Get version, perform release of tag 23 | if: inputs.is-tagged == 'true' 24 | run: echo "version=v$(cat version.txt)" >> $GITHUB_ENV 25 | shell: bash 26 | 27 | - name: Wait for tag to exist 28 | if: inputs.is-tagged == 'true' 29 | run: ./.github/workflows/scripts/wait-for-tag.sh ${{ env.version }} 30 | shell: bash 31 | 32 | - name: Fail if no version set 33 | if: env.version == '' 34 | run: exit 1 35 | shell: bash 36 | 37 | - name: Output version 38 | id: output-version 39 | run: echo "version=${{ env.version }}" >> $GITHUB_OUTPUT 40 | shell: bash 41 | -------------------------------------------------------------------------------- /.github/workflows/ossf-scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # Weekly on Saturdays. 6 | - cron: "30 1 * * 6" 7 | push: 8 | branches: 9 | - main 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecards analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | 21 | steps: 22 | - name: "Checkout code" 23 | uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 24 | with: 25 | persist-credentials: false 26 | 27 | - name: "Run analysis" 28 | uses: ossf/scorecard-action@72803a12483ed6f4f7c34f804818169f50162e37 29 | with: 30 | results_file: ossf-results.json 31 | results_format: json 32 | publish_results: false 33 | 34 | - name: "Add metadata" 35 | run: | 36 | full_repo="${{ github.repository }}" 37 | OWNER=${full_repo%/*} 38 | REPO=${full_repo#*/} 39 | jq -c '. + {"metadata_owner": "'$OWNER'", "metadata_repo": "'$REPO'", "metadata_query": "ossf"}' ossf-results.json > ossf-results-modified.json 40 | 41 | - name: "Post results to Sentinel" 42 | uses: cds-snc/sentinel-forward-data-action@main 43 | with: 44 | file_name: ossf-results-modified.json 45 | log_type: GitHubMetadata_OSSF_Scorecard 46 | log_analytics_workspace_id: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} 47 | log_analytics_workspace_key: ${{ secrets.LOG_ANALYTICS_WORKSPACE_KEY }} 48 | -------------------------------------------------------------------------------- /.github/workflows/release_generator.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | name: Release Generator 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 17 | id: sre_app_token 18 | with: 19 | app_id: ${{ secrets.CDS_RELEASE_BOT_APP_ID }} 20 | private_key: ${{ secrets.CDS_RELEASE_BOT_PRIVATE_KEY }} 21 | 22 | - uses: googleapis/release-please-action@7987652d64b4581673a76e33ad5e98e3dd56832f # v4.1.3 23 | with: 24 | token: ${{ steps.sre_app_token.outputs.token }} 25 | config-file: release-please-config.json 26 | manifest-file: .release-please-manifest.json 27 | -------------------------------------------------------------------------------- /.github/workflows/request-lambda-functions-to-use-new-image/action.yml: -------------------------------------------------------------------------------- 1 | name: Request Lambda functions to use new image 2 | 3 | inputs: 4 | aws-role-to-assume: 5 | required: true 6 | aws-role-session-name: 7 | required: true 8 | aws-region: 9 | required: true 10 | lambda-name: 11 | required: true 12 | image-tag: 13 | required: true 14 | 15 | runs: 16 | using: "composite" 17 | steps: 18 | - name: Configure AWS credentials using OIDC 19 | uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 20 | with: 21 | role-to-assume: ${{ inputs.aws-role-to-assume }} 22 | role-session-name: ${{ inputs.aws-role-session-name }} 23 | aws-region: ${{ inputs.aws-region }} 24 | 25 | - name: Login to Staging Amazon ECR 26 | id: login-ecr-staging 27 | uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 28 | 29 | - name: Update Lambda function image 30 | env: 31 | LAMBDA_NAME: ${{ inputs.lambda-name }} 32 | IMAGE_TAG: ${{ inputs.image-tag }} 33 | ECR_REGISTRY: ${{ steps.login-ecr-staging.outputs.registry }} 34 | run: | 35 | functionName=$([ "$LAMBDA_NAME" == "submission" ] && echo "Submission" || echo "$LAMBDA_NAME") 36 | aws lambda update-function-code --function-name $functionName --image-uri $ECR_REGISTRY/$LAMBDA_NAME-lambda:$IMAGE_TAG > /dev/null 2>&1 37 | shell: bash 38 | 39 | - name: Logout of Staging Amazon ECR 40 | if: always() 41 | run: docker logout ${{ steps.login-ecr-staging.outputs.registry }} 42 | shell: bash 43 | -------------------------------------------------------------------------------- /.github/workflows/s3-backup.yml: -------------------------------------------------------------------------------- 1 | name: S3 backup 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 6 * * *" 6 | 7 | jobs: 8 | s3-backup: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | - name: Checkout 13 | uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 14 | with: 15 | fetch-depth: 0 # retrieve all history 16 | 17 | - name: Configure AWS credentials 18 | uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 19 | with: 20 | aws-access-key-id: ${{ secrets.AWS_S3_BACKUP_ACCESS_KEY_ID }} 21 | aws-secret-access-key: ${{ secrets.AWS_S3_BACKUP_SECRET_ACCESS_KEY }} 22 | aws-region: ca-central-1 23 | 24 | - name: Create ZIP bundle 25 | run: | 26 | ZIP_FILE=`basename ${{ github.repository }}`-`date '+%Y-%m-%d'`.zip 27 | zip -rq "${ZIP_FILE}" . 28 | mkdir -p ${{ github.repository }} 29 | mv "${ZIP_FILE}" ${{ github.repository }} 30 | 31 | - name: Upload to S3 bucket 32 | run: | 33 | aws s3 sync . s3://${{ secrets.AWS_S3_BACKUP_BUCKET }} --exclude='*' --include='${{ github.repository }}/*' 34 | 35 | - name: Notify Slack channel if this job failed 36 | if: ${{ failure() }} 37 | run: | 38 | json='{"text":"S3 backup failed in !"}' 39 | curl -X POST -H 'Content-type: application/json' --data "$json" ${{ secrets.SLACK_NOTIFY_WEBHOOK }} 40 | -------------------------------------------------------------------------------- /.github/workflows/scripts/terraform-variable-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # 6 | # This script checks that all the GitHub workflow Terraform variables defined as `TF_VAR_` prefixed 7 | # environment variables have a matching `variable` definition in the codebase. This is being done 8 | # to prevent accidental mismatches between the GitHub workflow and the Terraform codebase. 9 | # 10 | 11 | 12 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 13 | WORKFLOW_VARS="$(grep -r "^\s*TF_VAR" $SCRIPT_DIR/../ | awk -F ':' '{print $2}' | sort | uniq | sed 's/^[[:blank:]]*TF_VAR_//')" 14 | 15 | # Loop through all the variables in the workflow and check if they are defined in the *.tf code 16 | for VAR in $WORKFLOW_VARS; do 17 | echo "🔎 Checking variable: \"$VAR\"" 18 | grep -r --include="*.tf" "variable \"$VAR\"" "$SCRIPT_DIR/../../../" || (echo "❌ Variable \"$VAR\" is not defined as a Terraform variable" && exit 1) 19 | done 20 | -------------------------------------------------------------------------------- /.github/workflows/scripts/wait-for-tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Given a git tag name as an argument, this script will wait until the tag is available in the remote repository. 5 | # The wait is based on the values of CHECK_INTERVAL and MAX_CHECKS. 6 | # 7 | # Usage: 8 | # ./wait-for-tag.sh v1.2.3 9 | # 10 | 11 | set -euo pipefail 12 | IFS=$'\n\t' 13 | 14 | TAG_NAME="$1" 15 | CHECK_INTERVAL=5 16 | MAX_CHECKS=20 17 | 18 | for ((i=1; i<=MAX_CHECKS; i++)); do 19 | git fetch --tags > /dev/null 2>&1 20 | if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then 21 | echo "🎉 Tag $TAG_NAME exists!" 22 | exit 0 23 | fi 24 | echo "Tag $TAG_NAME not found. Checking again in $CHECK_INTERVAL seconds... (Attempt $i/$MAX_CHECKS)" 25 | sleep $CHECK_INTERVAL 26 | done 27 | 28 | echo "💀 Tag $TAG_NAME does not exist after $MAX_CHECKS attempts..." 29 | exit 1 -------------------------------------------------------------------------------- /.github/workflows/tag-and-push-docker-images/action.yml: -------------------------------------------------------------------------------- 1 | name: Tag and push Lambda images 2 | 3 | inputs: 4 | aws-role-to-assume: 5 | required: true 6 | aws-role-session-name: 7 | required: true 8 | aws-region: 9 | required: true 10 | image-name: 11 | required: true 12 | image-tag: 13 | required: true 14 | repository-suffix: 15 | default: '-lambda' 16 | required: false 17 | 18 | runs: 19 | using: "composite" 20 | steps: 21 | - name: Configure AWS credentials using OIDC 22 | uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 23 | with: 24 | role-to-assume: ${{ inputs.aws-role-to-assume }} 25 | role-session-name: ${{ inputs.aws-role-session-name }} 26 | aws-region: ${{ inputs.aws-region }} 27 | 28 | - name: Login to Staging Amazon ECR 29 | id: login-ecr-staging 30 | uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 31 | 32 | - name: Tag and push docker images 33 | env: 34 | IMAGE_NAME: ${{ inputs.image-name }} 35 | IMAGE_TAG: ${{ inputs.image-tag }} 36 | ECR_REGISTRY: ${{ steps.login-ecr-staging.outputs.registry }} 37 | run: | 38 | REPOSITORY_NAME=$IMAGE_NAME${{ inputs.repository-suffix }} 39 | docker tag $REPOSITORY_NAME $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG 40 | docker tag $REPOSITORY_NAME $ECR_REGISTRY/$REPOSITORY_NAME:latest 41 | docker push $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG 42 | docker push $ECR_REGISTRY/$REPOSITORY_NAME:latest 43 | shell: bash 44 | 45 | - name: Logout of Staging Amazon ECR 46 | if: always() 47 | run: docker logout ${{ steps.login-ecr-staging.outputs.registry }} 48 | shell: bash 49 | -------------------------------------------------------------------------------- /.github/workflows/terraform-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: "Terraform security scan" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "aws/**" 8 | - ".github/workflows/terraform-security-scan.yml" 9 | pull_request: 10 | paths: 11 | - "aws/**" 12 | - ".github/workflows/terraform-security-scan.yml" 13 | 14 | jobs: 15 | terraform-security-scan: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | 21 | - name: Checkov security scan 22 | uses: bridgecrewio/checkov-action@097919de4f8058fb4478275f36e6708d12a9f53a # latest as of December 2023 23 | with: 24 | directory: aws 25 | framework: terraform 26 | quiet: true 27 | output_format: cli 28 | soft_fail: false 29 | -------------------------------------------------------------------------------- /.github/workflows/terraform-variable-check.yml: -------------------------------------------------------------------------------- 1 | name: Terraform variable check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - "aws/**" 9 | - "env/**" 10 | - ".github/workflows/**" 11 | 12 | jobs: 13 | terraform-variable-check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | 19 | - name: Check Terraform variables are defined correctly 20 | run: | 21 | ./.github/workflows/scripts/terraform-variable-check.sh 22 | -------------------------------------------------------------------------------- /.github/workflows/terragrunt-plan-production-warn-release-exists.yml: -------------------------------------------------------------------------------- 1 | name: "Terragrunt plan PRODUCTION warn release exists" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - "version.txt" 9 | 10 | jobs: 11 | warn-release-exists: 12 | if: ${{ startsWith(github.head_ref, 'release-please--') }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | 18 | - name: Get version 19 | run: echo "version=v$(cat version.txt)" >> $GITHUB_ENV 20 | 21 | - name: Fail if release tag exists # this happens if there is a revert and the reverted release and tag is not deleted 22 | run: | 23 | git fetch --tags > /dev/null 2>&1 24 | if git rev-parse "$version" >/dev/null 2>&1; then 25 | echo "Tag $version exists..." 26 | echo "Please delete the release and tag and try again." 27 | exit 1 28 | fi 29 | -------------------------------------------------------------------------------- /.github/workflows/test-lambda-code/action.yml: -------------------------------------------------------------------------------- 1 | name: Test Lambda code 2 | 3 | inputs: 4 | lambda-directory: 5 | required: true 6 | lambda-name: 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - run: | 13 | if [ -d tests ] && [ -f yarn.lock ]; then 14 | yarn install 15 | yarn test 16 | else 17 | echo "No tests folder detected" 18 | fi 19 | working-directory: ${{ inputs.lambda-directory }} 20 | shell: bash 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Terraform lock file 9 | .terraform.lock.hcl 10 | 11 | # Terragrunt 12 | .terragrunt-cache 13 | 14 | # Crash log files 15 | crash.log 16 | 17 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 18 | # .tfvars files are managed as part of configuration and so should be included in 19 | # version control. 20 | # 21 | # example.tfvars 22 | secrets.tfvars 23 | 24 | # Ignore override files as they are usually used to override resources locally and so 25 | # are not checked in 26 | override.tf 27 | override.tf.json 28 | *_override.tf 29 | *_override.tf.json 30 | 31 | # Include override files you do wish to add to version control using negated pattern 32 | # 33 | # !example_override.tf 34 | 35 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 36 | *tfplan* 37 | tf.plan 38 | 39 | # Ignore vscode settings 40 | .vscode 41 | 42 | # Ignore node modules folders for Lambda functions 43 | **/node_modules/ 44 | 45 | # Ignore lambda builds 46 | **/dist 47 | **/build 48 | 49 | # Ignore Mac OS file types 50 | .DS_Store 51 | 52 | # Ignore local stack data file 53 | .devcontainer/data 54 | 55 | # Local environment variables 56 | .env 57 | 58 | # Ignore coverage reports 59 | **/coverage/ 60 | 61 | # Ignore IdP certificates and private keys 62 | idp/docker/*.crt 63 | idp/docker/*.key 64 | *_private_api_key.json 65 | 66 | # Python 67 | *.pyc 68 | __pychache__ 69 | 70 | # VPN configuration 71 | local_dev_files/certificates/* 72 | local_dev_files/easyrsa 73 | !local_dev_files/certificates/client-config.ovpn 74 | aws/vpn/certificates -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false 8 | } 9 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "3.35.0" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Canadian Digital Service – Service numérique canadien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /aws/.checkov.yml: -------------------------------------------------------------------------------- 1 | # Config for checkov Terraform static analysis 2 | 3 | skip-check: 4 | - CKV_AWS_23 # Security group descriptions are not required 5 | - CKV_AWS_51 # ECR `latest` image tag is used in Staging 6 | - CKV_AWS_115 # Lambda function-level concurrent execution limit not required 7 | - CKV_AWS_116 # Lambda dead-letter queue not required 8 | - CKV_AWS_117 # Lambda configured within a VPC not required 9 | - CKV_AWS_128 # RDS IAM authentication will not be used 10 | - CKV_AWS_136 # ECR default service key encryption is acceptable 11 | - CKV_AWS_144 # S3 cross-region replication no required 12 | - CKV_AWS_145 # S3 default service key encryption is acceptable 13 | - CKV_AWS_149 # SecretsManager default service key encryption is acceptable 14 | - CKV_AWS_162 # RDS IAM authentication will not be used 15 | - CKV_AWS_173 # Lambda env var default service key encryption is acceptable 16 | - CKV2_AWS_5 # Security groups are attached in seperate Terragrunt modules 17 | - CKV_AWS_29 # TODO: Elasticache investigate at-rest data encryption 18 | - CKV_AWS_30 # TODO: Elasticache investigate in-transit data encryption 19 | - CKV_AWS_31 # TODO: Elasticache investigate in-transit data encryption and auth token 20 | - CKV_AWS_91 # TODO: Load balancer enable access logging 21 | - CKV2_AWS_8 # TODO: RDS investigate if Query Logging can be enabled 22 | - CKV2_AWS_27 # TODO: RDS investigate how to setup AWS Backup plan 23 | - CKV_AWS_272 # TODO: Add code signing to the remaining lambdas 24 | - CKV_AWS_332 # 1.4.0 is the latest ECS Fargate platform version 25 | -------------------------------------------------------------------------------- /aws/alarms/cloudwatch_global.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_metric_alarm" "ip_added_to_block_list" { 2 | alarm_name = "IpAddedToBlockList" 3 | comparison_operator = "GreaterThanThreshold" 4 | evaluation_periods = 1 5 | metric_name = var.waf_ipv4_new_blocked_ip_metric_filter_name 6 | namespace = var.waf_ipv4_new_blocked_ip_metric_filter_namespace 7 | period = 900 # Waf IP blocklist is updated every 15 minutes 8 | statistic = "Sum" 9 | threshold = 1 # Alarm as soon as any IP is added to the block list 10 | treat_missing_data = "notBreaching" 11 | alarm_description = "WAF - IP(s) Has been added to the dynamic block list." 12 | 13 | alarm_actions = [var.sns_topic_alert_warning_arn] 14 | } -------------------------------------------------------------------------------- /aws/alarms/dashboards.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_dashboard" "forms_service_health" { 2 | dashboard_name = "Forms-Service-Health" 3 | dashboard_body = templatefile("${path.module}/dashboards/forms_service_health.tmpl.json", { 4 | alarm_ecs_cpu_utilization_warn = aws_cloudwatch_metric_alarm.forms_cpu_utilization_high_warn.arn, 5 | alarm_ecs_memory_utilization_warn = aws_cloudwatch_metric_alarm.forms_memory_utilization_high_warn.arn, 6 | alarm_lb_response_5xx_warn = aws_cloudwatch_metric_alarm.ELB_5xx_error_warn.arn, 7 | alarm_lb_response_time_warn = aws_cloudwatch_metric_alarm.response_time_warn.arn, 8 | alarm_lb_healthy_host_count = aws_cloudwatch_metric_alarm.ELB_healthy_hosts.arn, 9 | alarm_lb_unhealth_host_count_tg1 = var.unhealthy_host_count_for_target_group_1_alarm_arn, 10 | alarm_lb_unhealth_host_count_tg2 = var.unhealthy_host_count_for_target_group_2_alarm_arn, 11 | alarm_reliability_deadletter_queue = aws_cloudwatch_metric_alarm.reliability_dead_letter_queue_warn.arn, 12 | lb_arn_suffix = var.lb_arn_suffix, 13 | ecs_cloudwatch_log_group_name = var.ecs_cloudwatch_log_group_name, 14 | ecs_cluster_name = var.ecs_cluster_name, 15 | ecs_service_name = var.ecs_service_name, 16 | lambda_nagware_log_group_name = var.lambda_nagware_log_group_name, 17 | lambda_reliability_log_group_name = var.lambda_reliability_log_group_name, 18 | lambda_response_archiver_log_group_name = var.lambda_response_archiver_log_group_name, 19 | lambda_submission_log_group_name = var.lambda_submission_log_group_name, 20 | lambda_vault_integrity_log_group_name = var.lambda_vault_integrity_log_group_name, 21 | rds_cluster_identifier = var.rds_cluster_identifier, 22 | region = var.region 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /aws/alarms/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # Define the pattern that will be used to detect errors in the IdP logs 3 | # Any log message that contains a word from `idp_error` and does not contain a word from `idp_error_ignore` will be detected 4 | idp_error = [ 5 | "level=error", 6 | "level=ERROR" 7 | ] 8 | idp_error_ignore = [ 9 | "context canceled", # user cancels request before it completes 10 | "Errors.AuthNKey.NotFound", # user requests an access token with an invalid key 11 | "oidc.Time*cannot parse", # user sends a JWT with a malformed epoch time 12 | "token has expired" # user access token has expired 13 | ] 14 | idp_error_pattern = "[(w1=\"*${join("*\" || w1=\"*", local.idp_error)}*\") && w1!=\"*${join("*\" && w1!=\"*", local.idp_error_ignore)}*\"]" 15 | 16 | lambda_submission_expect_invocation_in_period = var.env == "production" ? var.lambda_submission_expect_invocation_in_period : 60 * 24 # expect once a day in non-prod envs 17 | } 18 | -------------------------------------------------------------------------------- /aws/api/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ecs_api_cluster_name" { 2 | description = "API's ECS cluster name" 3 | value = module.api_ecs.cluster_name 4 | } 5 | 6 | output "ecs_api_cloudwatch_log_group_name" { 7 | description = "API's ECS CloudWatch log group name" 8 | value = module.api_ecs.cloudwatch_log_group_name 9 | } 10 | 11 | output "ecs_api_service_name" { 12 | description = "API's ECS service name" 13 | value = module.api_ecs.service_name 14 | } 15 | 16 | output "ecs_api_service_port" { 17 | description = "API's ECS service port" 18 | value = module.api_ecs.service_port 19 | } 20 | -------------------------------------------------------------------------------- /aws/app/codedeploy.tf: -------------------------------------------------------------------------------- 1 | # 2 | # CodeDeploy 3 | # Provides Blue/Green deployments for ECS tasks 4 | # 5 | resource "aws_codedeploy_app" "app" { 6 | compute_platform = "ECS" 7 | name = "AppECS-${aws_ecs_cluster.forms.name}-${aws_ecs_service.form_viewer.name}" 8 | 9 | 10 | } 11 | 12 | resource "aws_codedeploy_deployment_group" "app" { 13 | app_name = aws_codedeploy_app.app.name 14 | deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" 15 | deployment_group_name = "DgpECS-${aws_ecs_cluster.forms.name}-${aws_ecs_service.form_viewer.name}" 16 | service_role_arn = aws_iam_role.codedeploy.arn 17 | 18 | auto_rollback_configuration { 19 | enabled = true 20 | events = ["DEPLOYMENT_FAILURE"] 21 | } 22 | 23 | blue_green_deployment_config { 24 | deployment_ready_option { 25 | action_on_timeout = var.codedeploy_manual_deploy_enabled ? "STOP_DEPLOYMENT" : "CONTINUE_DEPLOYMENT" 26 | } 27 | 28 | terminate_blue_instances_on_deployment_success { 29 | action = "TERMINATE" 30 | termination_wait_time_in_minutes = var.codedeploy_termination_wait_time_in_minutes 31 | } 32 | } 33 | 34 | deployment_style { 35 | deployment_option = "WITH_TRAFFIC_CONTROL" 36 | deployment_type = "BLUE_GREEN" 37 | } 38 | 39 | ecs_service { 40 | cluster_name = aws_ecs_cluster.forms.name 41 | service_name = aws_ecs_service.form_viewer.name 42 | } 43 | 44 | load_balancer_info { 45 | target_group_pair_info { 46 | prod_traffic_route { 47 | listener_arns = [var.lb_https_listener_arn] 48 | } 49 | 50 | target_group { 51 | name = var.lb_target_group_1_name 52 | } 53 | 54 | target_group { 55 | name = var.lb_target_group_2_name 56 | } 57 | } 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /aws/app/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ecs_cloudwatch_log_group_name" { 2 | description = "ECS task's CloudWatch log group name" 3 | value = aws_cloudwatch_log_group.forms.name 4 | } 5 | 6 | output "ecs_cluster_name" { 7 | description = "ECS cluster name" 8 | value = aws_ecs_cluster.forms.name 9 | } 10 | 11 | output "ecs_service_name" { 12 | description = "ECS service name" 13 | value = aws_ecs_service.form_viewer.name 14 | } 15 | 16 | output "ecs_iam_role_arn" { 17 | description = "ECS task's IAM role ARN" 18 | value = aws_iam_role.forms.arn 19 | } 20 | 21 | output "ecs_iam_forms_secrets_manager_policy_arn" { 22 | description = "IAM policy for access to Secrets Manager" 23 | value = aws_iam_policy.forms_secrets_manager.arn 24 | } 25 | 26 | output "ecs_iam_forms_kms_policy_arn" { 27 | description = "IAM policy for access to KMS" 28 | value = aws_iam_policy.forms_kms.arn 29 | } 30 | 31 | output "ecs_iam_forms_s3_policy_arn" { 32 | description = "IAM policy access to S3" 33 | value = aws_iam_policy.forms_s3.arn 34 | } 35 | 36 | output "ecs_iam_forms_dynamodb_policy_arn" { 37 | description = "IAM policy for access to DynamoDB" 38 | value = aws_iam_policy.forms_dynamodb.arn 39 | } 40 | 41 | output "ecs_iam_forms_sqs_policy_arn" { 42 | description = "IAM policy for access to SQS" 43 | value = aws_iam_policy.forms_sqs.arn 44 | } 45 | 46 | output "ecs_iam_forms_cognito_policy_arn" { 47 | description = "IAM policy for access to Cognito" 48 | value = aws_iam_policy.cognito.arn 49 | } 50 | -------------------------------------------------------------------------------- /aws/cognito/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "kms_key_cloudwatch_arn" { 2 | description = "CloudWatch KMS key ARN used to encrypt the logs" 3 | type = string 4 | } 5 | 6 | variable "cognito_code_template_id" { 7 | description = "Notify template id used by cognito" 8 | type = string 9 | } 10 | 11 | variable "notify_api_key_secret_arn" { 12 | description = "GC Notify API key arn" 13 | type = string 14 | sensitive = true 15 | } 16 | 17 | variable "ecr_repository_url_cognito_email_sender_lambda" { 18 | description = "URL of the Cognito Email Sender Lambda ECR" 19 | type = string 20 | } 21 | 22 | variable "ecr_repository_url_cognito_pre_sign_up_lambda" { 23 | description = "URL of the Cognito Pre Sign Up Lambda ECR" 24 | type = string 25 | } -------------------------------------------------------------------------------- /aws/cognito/kms.tf: -------------------------------------------------------------------------------- 1 | resource "aws_kms_key" "cognito_encryption" { 2 | description = "Key used by AWS Cognito to encrypt data sent to lambda triggers" 3 | enable_key_rotation = true 4 | policy = data.aws_iam_policy_document.kms_cognito_encryption.json 5 | 6 | 7 | } 8 | 9 | resource "aws_kms_alias" "cognito_encryption_alias" { 10 | name = "alias/cognito-encryption-key" 11 | target_key_id = aws_kms_key.cognito_encryption.key_id 12 | } 13 | 14 | data "aws_iam_policy_document" "kms_cognito_encryption" { 15 | # checkov:skip=CKV_AWS_109: `resources = ["*"]` identifies the KMS key to which the key policy is attached 16 | # checkov:skip=CKV_AWS_111: `resources = ["*"]` identifies the KMS key to which the key policy is attached 17 | # checkov:skip=CKV_AWS_356: `resources = ["*"]` identifies the KMS key to which the key policy is attached 18 | // TODO: refactor write access (then we can remove checkov:skip=CKV_AWS_111) 19 | // TODO: refactor to remove `resources = ["*"]` (then we can remove checkov:skip=CKV_AWS_356) 20 | 21 | statement { 22 | sid = "Enable IAM User Permissions" 23 | effect = "Allow" 24 | actions = ["kms:*"] 25 | resources = ["*"] 26 | 27 | principals { 28 | type = "AWS" 29 | identifiers = ["arn:aws:iam::${var.account_id}:root"] 30 | } 31 | } 32 | 33 | statement { 34 | sid = "Enable Cognito Access to KMS key" 35 | effect = "Allow" 36 | actions = [ 37 | "kms:CreateGrant" 38 | ] 39 | resources = ["*"] 40 | 41 | principals { 42 | identifiers = ["cognito-idp.amazonaws.com"] 43 | type = "Service" 44 | } 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /aws/cognito/outputs.tf: -------------------------------------------------------------------------------- 1 | output "cognito_endpoint_url" { 2 | description = "Endpoint name of the user pool." 3 | value = aws_cognito_user_pool.forms.endpoint 4 | } 5 | 6 | output "cognito_user_pool_arn" { 7 | description = "ARN for user pool" 8 | value = aws_cognito_user_pool.forms.arn 9 | } 10 | 11 | output "cognito_client_id" { 12 | description = "Client ID of the forms user pool client." 13 | value = aws_cognito_user_pool_client.forms.id 14 | } -------------------------------------------------------------------------------- /aws/cognito/user_pool_iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "cognito_userpool_import" { 2 | name = "role_for_cognito_user_pool_import" 3 | assume_role_policy = data.aws_iam_policy_document.cognito_userpool_import_assume.json 4 | 5 | 6 | } 7 | 8 | data "aws_iam_policy_document" "cognito_userpool_import_assume" { 9 | statement { 10 | actions = ["sts:AssumeRole"] 11 | effect = "Allow" 12 | 13 | principals { 14 | type = "Service" 15 | identifiers = ["cognito-idp.amazonaws.com"] 16 | } 17 | } 18 | } 19 | 20 | resource "aws_iam_policy" "cognito_userpool_import_logging" { 21 | name = "cognito_userpool_import_logging" 22 | path = "/" 23 | description = "IAM policy for logging from a cognito userpool import" 24 | policy = data.aws_iam_policy_document.cognito_userpool_import_logging.json 25 | 26 | 27 | } 28 | 29 | resource "aws_iam_role_policy_attachment" "cognito_userpool_import_logs" { 30 | role = aws_iam_role.cognito_userpool_import.name 31 | policy_arn = aws_iam_policy.cognito_userpool_import_logging.arn 32 | } 33 | 34 | 35 | data "aws_iam_policy_document" "cognito_userpool_import_logging" { 36 | statement { 37 | effect = "Allow" 38 | 39 | actions = [ 40 | "logs:CreateLogGroup", 41 | "logs:CreateLogStream", 42 | "logs:DescribeLogStreams", 43 | "logs:PutLogEvents" 44 | ] 45 | 46 | resources = [ 47 | "arn:aws:logs:*:*:*" 48 | ] 49 | } 50 | } -------------------------------------------------------------------------------- /aws/dynamodb/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "kms_key_dynamodb_arn" { 2 | description = "KMS key ARN used to encrypt DynamoDB content" 3 | type = string 4 | } 5 | -------------------------------------------------------------------------------- /aws/dynamodb/outputs.tf: -------------------------------------------------------------------------------- 1 | output "dynamodb_relability_queue_arn" { 2 | description = "Reliability queue DynamodDB table ARN" 3 | value = aws_dynamodb_table.reliability_queue.arn 4 | } 5 | 6 | output "dynamodb_vault_arn" { 7 | description = "Vault DynamodDB table ARN" 8 | value = aws_dynamodb_table.vault.arn 9 | } 10 | 11 | output "dynamodb_vault_table_name" { 12 | description = "Vault DynamodDB table name" 13 | value = aws_dynamodb_table.vault.name 14 | } 15 | 16 | output "dynamodb_vault_stream_arn" { 17 | description = "Vault DynamoDB stream ARN" 18 | value = aws_dynamodb_table.vault.stream_arn 19 | } 20 | 21 | output "dynamodb_app_audit_logs_arn" { 22 | description = "App Audit Logs table ARN" 23 | value = aws_dynamodb_table.audit_logs.arn 24 | } 25 | 26 | output "dynamodb_app_audit_logs_table_name" { 27 | description = "App Audit Logs table name" 28 | value = aws_dynamodb_table.audit_logs.name 29 | } 30 | 31 | output "dynamodb_api_audit_logs_arn" { 32 | description = "API Audit Logs table ARN" 33 | value = aws_dynamodb_table.api_audit_logs.arn 34 | } 35 | 36 | output "dynamodb_api_audit_logs_table_name" { 37 | description = "API Audit Logs table name" 38 | value = aws_dynamodb_table.api_audit_logs.name 39 | } -------------------------------------------------------------------------------- /aws/ecr/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "staging_account_id" { 2 | description = "Staging account ID" 3 | type = string 4 | } 5 | variable "cds_org_id" { 6 | description = "AWS CDS organization ID" 7 | type = string 8 | sensitive = true 9 | } 10 | 11 | variable "aws_development_accounts" { 12 | description = "List of AWS development account IDs" 13 | type = list(string) 14 | } 15 | -------------------------------------------------------------------------------- /aws/ecr/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ecr_repository_url_form_viewer" { 2 | description = "URL of the Form viewer ECR" 3 | value = aws_ecr_repository.viewer_repository.repository_url 4 | } 5 | 6 | output "ecr_repository_url_cognito_email_sender_lambda" { 7 | description = "URL of the Cognito Email Sender Lambda ECR" 8 | value = aws_ecr_repository.lambda["cognito-email-sender-lambda"].repository_url 9 | } 10 | 11 | output "ecr_repository_url_cognito_pre_sign_up_lambda" { 12 | description = "URL of the Cognito Pre Sign Up Lambda ECR" 13 | value = aws_ecr_repository.lambda["cognito-pre-sign-up-lambda"].repository_url 14 | } 15 | 16 | output "ecr_repository_url_load_testing_lambda" { 17 | description = "URL of the Load Testing Lambda ECR" 18 | value = try(aws_ecr_repository.lambda["load-testing-lambda"].repository_url, null) 19 | } 20 | 21 | 22 | output "ecr_repository_url_notify_slack_lambda" { 23 | description = "URL of the Notify Slack Lambda ECR" 24 | value = aws_ecr_repository.lambda["notify-slack-lambda"].repository_url 25 | } 26 | 27 | output "ecr_repository_url_idp" { 28 | description = "URL of the Zitadel IdP's ECR" 29 | value = aws_ecr_repository.idp.repository_url 30 | } 31 | 32 | output "ecr_repository_url_api" { 33 | description = "URL of the Forms API's ECR" 34 | value = aws_ecr_repository.api.repository_url 35 | } 36 | 37 | output "ecr_repository_lambda_urls" { 38 | description = "URL Map of the Lambda's ECR" 39 | value = var.env == "development" ? { 40 | for lambda_name, ecr_repository in aws_ecr_repository.lambda : lambda_name => replace(ecr_repository.repository_url, var.account_id, var.staging_account_id) 41 | } : { 42 | for lambda_name, ecr_repository in aws_ecr_repository.lambda : lambda_name => tostring(ecr_repository.repository_url) 43 | } 44 | } -------------------------------------------------------------------------------- /aws/ecr/policy/lifecycle.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "rulePriority": 1, 5 | "description": "Keep last 10 images", 6 | "selection": { 7 | "tagStatus": "any", 8 | "countType": "imageCountMoreThan", 9 | "countNumber": 10 10 | }, 11 | "action": { 12 | "type": "expire" 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /aws/file_scanning/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "vault_file_storage_id" { 2 | description = "S3 bucket ID for vault file storage" 3 | type = string 4 | } -------------------------------------------------------------------------------- /aws/file_scanning/vault_scan_object.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | scan_files_account = var.env == "production" ? "806545929748" : "127893201980" 3 | } 4 | 5 | module "vault_scan_object" { 6 | source = "github.com/cds-snc/terraform-modules//S3_scan_object?ref=64b19ecfc23025718cd687e24b7115777fd09666" # v10.2.1 7 | 8 | s3_upload_bucket_names = [var.vault_file_storage_id] 9 | s3_scan_object_role_arn = "arn:aws:iam::${local.scan_files_account}:role/s3-scan-object" 10 | scan_files_role_arn = "arn:aws:iam::${local.scan_files_account}:role/scan-files-api" 11 | 12 | billing_tag_value = var.billing_tag_value 13 | } 14 | -------------------------------------------------------------------------------- /aws/glue/data/historical_data.csv: -------------------------------------------------------------------------------- 1 | Unique ID,Date,Metric,Metric format (from Metric),Unit of measurement,Client name,Client email,number_value,text_value,Comment,Publishing description,Published form type,Published reason,Recorded by 2 | 270,"August 26, 2021",New live form,Number,Number,Treasury Board of Canada Secretariat / Secrétariat du Conseil du Trésor du Canada,,1.0,Protected A,"Title: ""GC DevOps Self Assessment""",,,,Stevie-Ray 3 | -------------------------------------------------------------------------------- /aws/glue/databases.tf: -------------------------------------------------------------------------------- 1 | resource "aws_glue_catalog_database" "rds_db_catalog" { 2 | name = "rds_db_catalog" 3 | description = "Source : RDS" 4 | } -------------------------------------------------------------------------------- /aws/glue/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "rds_cluster_reader_endpoint" { 2 | description = "The endpoint of the RDS database" 3 | type = string 4 | } 5 | 6 | variable "rds_port" { 7 | description = "The port of the RDS database" 8 | type = string 9 | } 10 | 11 | variable "rds_db_name" { 12 | description = "The name of the RDS database" 13 | type = string 14 | } 15 | 16 | variable "rds_cluster_instance_availability_zone" { 17 | description = "The RDS cluster instance's availability zone" 18 | type = string 19 | } 20 | 21 | variable "rds_cluster_instance_identifier" { 22 | description = "The RDS cluster instance's identifier" 23 | type = string 24 | } 25 | 26 | variable "rds_cluster_instance_subnet_id" { 27 | description = "The RDS cluster instance's subnet ID" 28 | type = string 29 | } 30 | 31 | variable "rds_connector_secret_arn" { 32 | description = "The ARN of the RDS secret that contains the database authentication credentials" 33 | type = string 34 | } 35 | 36 | variable "rds_connector_secret_name" { 37 | description = "The name of the RDS secret that contains the database authentication credentials" 38 | type = string 39 | } 40 | 41 | variable "datalake_bucket_arn" { 42 | description = "The ARN of the Raw bucket" 43 | type = string 44 | } 45 | 46 | variable "datalake_bucket_name" { 47 | description = "The name of the Raw bucket" 48 | type = string 49 | } 50 | 51 | variable "etl_bucket_arn" { 52 | description = "The ARN of the ETL bucket" 53 | type = string 54 | } 55 | 56 | variable "etl_bucket_name" { 57 | description = "The name of the ETL bucket" 58 | type = string 59 | } 60 | 61 | variable "glue_job_security_group_id" { 62 | description = "The security group ID for the Glue job" 63 | type = string 64 | } 65 | 66 | variable "s3_endpoint" { 67 | description = "The S3 endpoint" 68 | type = string 69 | } 70 | 71 | variable "submission_cloudwatch_endpoint" { 72 | description = "The name of the CloudWatch log group that handles submissions" 73 | type = string 74 | } -------------------------------------------------------------------------------- /aws/glue/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | glue_crawler_log_group_name = "/aws-glue/crawlers-role${aws_iam_role.glue_crawler.path}${aws_iam_role.glue_crawler.name}-${aws_glue_security_configuration.encryption_at_rest.name}" 3 | glue_etl_log_group_name = "/aws-glue/jobs/${aws_glue_security_configuration.encryption_at_rest.name}-role${aws_iam_role.glue_crawler.path}${aws_iam_role.glue_etl.name}" 4 | 5 | platform_data_lake_raw_s3_bucket_arn = "arn:aws:s3:::cds-data-lake-raw-production" 6 | } -------------------------------------------------------------------------------- /aws/glue/outputs.tf: -------------------------------------------------------------------------------- 1 | output "glue_crawler_log_group_name" { 2 | description = "The name of the Glue Crawler CloudWatch log group." 3 | value = local.glue_crawler_log_group_name 4 | } 5 | 6 | output "glue_etl_log_group_name" { 7 | description = "The name of the Glue ETL CloudWatch log group." 8 | value = local.glue_etl_log_group_name 9 | } 10 | 11 | output "glue_database_name" { 12 | description = "The name of the Glue database." 13 | value = aws_glue_catalog_database.rds_db_catalog.name 14 | } -------------------------------------------------------------------------------- /aws/glue/s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket_replication_configuration" "forms_s3_replicate_to_platform_data_lake" { 2 | role = aws_iam_role.forms_s3_replicate.arn 3 | bucket = var.datalake_bucket_name 4 | 5 | rule { 6 | id = "send-to-platform-data-lake" 7 | status = var.env == "production" ? "Enabled" : "Disabled" 8 | 9 | destination { 10 | bucket = local.platform_data_lake_raw_s3_bucket_arn 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /aws/glue/scripts/historical_etl.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from awsglue.transforms import * 3 | from awsglue.utils import getResolvedOptions 4 | from pyspark.context import SparkContext 5 | from awsglue.context import GlueContext 6 | from awsglue.job import Job 7 | from awsglue.dynamicframe import DynamicFrame 8 | from pyspark.sql.functions import col, from_unixtime, date_format, lit, current_timestamp 9 | from datetime import datetime 10 | 11 | # Initialize Glue context 12 | args = getResolvedOptions(sys.argv, ['JOB_NAME', 's3_bucket', 'rds_bucket']) 13 | sc = SparkContext() 14 | glueContext = GlueContext(sc) 15 | spark = glueContext.spark_session 16 | job = Job(glueContext) 17 | job.init(args['JOB_NAME'], args) 18 | logger = glueContext.get_logger() 19 | 20 | # print the arguments 21 | logger.info("Starting Script for ETL of Historical data") 22 | logger.info(args['s3_bucket']) 23 | 24 | # Load the CSV from an S3 bucket as a PySpark DataFrame 25 | historical_df = spark.read.csv(args['s3_bucket'], header=True) 26 | 27 | # Write the processed data to the target, using the "rds_db_catalog" db, and "rds_report_processed_data" table. 28 | datasink4 = glueContext.write_dynamic_frame.from_options( 29 | frame = DynamicFrame.fromDF(historical_df, glueContext, "historical_df"), 30 | connection_type = "s3", 31 | connection_options = {"path": f"s3://{args['rds_bucket']}/platform/gc-forms/historical-data/"}, 32 | format = "parquet", 33 | transformation_ctx = "datasink4" 34 | ) 35 | 36 | logger.info("Data written to S3") 37 | 38 | job.commit() 39 | -------------------------------------------------------------------------------- /aws/hosted_zone/hosted_zone.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Route53 hosted zone: 3 | # Holds the DNS records for the service 4 | # 5 | 6 | resource "aws_route53_zone" "form_viewer" { 7 | # checkov:skip=CKV2_AWS_38: Domain Name System Security Extensions signing not required 8 | # checkov:skip=CKV2_AWS_39: Domain Name System query logging not required 9 | // TODO: Implement Domain Name System Security Extensions signing 10 | // TODO: Implement Domain Name System query logging 11 | count = length(var.domains) 12 | name = var.domains[count.index] 13 | } -------------------------------------------------------------------------------- /aws/hosted_zone/outputs.tf: -------------------------------------------------------------------------------- 1 | output "hosted_zone_ids" { 2 | description = "Route53 hosted zone ID" 3 | value = aws_route53_zone.form_viewer.*.zone_id 4 | } 5 | 6 | output "hosted_zone_names" { 7 | description = "Route53 hosted zone name" 8 | value = aws_route53_zone.form_viewer.*.name 9 | } 10 | -------------------------------------------------------------------------------- /aws/idp/certificate.tf: -------------------------------------------------------------------------------- 1 | resource "aws_acm_certificate" "idp" { 2 | domain_name = var.domain_idp 3 | validation_method = "DNS" 4 | 5 | tags = local.common_tags 6 | 7 | lifecycle { 8 | create_before_destroy = true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /aws/idp/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | hosted_zone_id = var.hosted_zone_ids[0] 3 | protocol_versions = toset(["HTTP1", "HTTP2"]) 4 | common_tags = { 5 | Terraform = "true" 6 | (var.billing_tag_key) = var.billing_tag_value 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /aws/idp/outputs.tf: -------------------------------------------------------------------------------- 1 | output "ecs_idp_cluster_name" { 2 | description = "IdP's ECS cluster name" 3 | value = module.idp_ecs.cluster_name 4 | } 5 | 6 | output "ecs_idp_cloudwatch_log_group_name" { 7 | description = "IdP's ECS CloudWatch log group name" 8 | value = module.idp_ecs.cloudwatch_log_group_name 9 | } 10 | 11 | output "ecs_idp_service_name" { 12 | description = "IdP's ECS service name" 13 | value = module.idp_ecs.service_name 14 | } 15 | 16 | output "ecs_idp_service_port" { 17 | description = "IdP's ECS service port" 18 | value = module.idp_ecs.service_port 19 | } 20 | 21 | output "lb_idp_arn_suffix" { 22 | description = "IdP's load balancer ARN suffix" 23 | value = aws_lb.idp.arn_suffix 24 | } 25 | 26 | output "lb_idp_target_groups_arn_suffix" { 27 | description = "IdP's load balancer target groups ARN suffixes" 28 | value = { 29 | for version in local.protocol_versions : version => aws_lb_target_group.idp[version].arn_suffix 30 | } 31 | } 32 | 33 | output "rds_idp_cluster_identifier" { 34 | description = "IdP's RDS cluster identifier" 35 | value = module.idp_database.rds_cluster_id 36 | } 37 | -------------------------------------------------------------------------------- /aws/idp/ses.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Send email using a SES SMTP server 3 | # 4 | resource "aws_ses_domain_identity" "idp" { 5 | domain = var.domain_idp 6 | } 7 | 8 | resource "aws_ses_domain_dkim" "idp" { 9 | domain = aws_ses_domain_identity.idp.domain 10 | } 11 | 12 | resource "aws_ses_domain_identity_verification" "idp" { 13 | domain = aws_ses_domain_identity.idp.id 14 | depends_on = [aws_route53_record.idp_ses_verification_TXT] 15 | } 16 | 17 | resource "aws_iam_user" "idp_send_email" { 18 | # checkov:skip=CKV_AWS_273: SES IAM user is required to confirgure SMTP credentials 19 | name = "idp_send_email" 20 | } 21 | 22 | resource "aws_iam_group" "idp_send_email" { 23 | name = "idp_send_email" 24 | } 25 | 26 | resource "aws_iam_group_membership" "idp_send_email" { 27 | name = aws_iam_user.idp_send_email.name 28 | group = aws_iam_group.idp_send_email.name 29 | users = [ 30 | aws_iam_user.idp_send_email.name 31 | ] 32 | } 33 | 34 | resource "aws_iam_group_policy_attachment" "idp_send_email" { 35 | group = aws_iam_user.idp_send_email.name 36 | policy_arn = aws_iam_policy.idp_send_email.arn 37 | } 38 | 39 | data "aws_iam_policy_document" "idp_send_email" { 40 | statement { 41 | effect = "Allow" 42 | actions = [ 43 | "ses:SendRawEmail" 44 | ] 45 | resources = [ 46 | aws_ses_domain_identity.idp.arn 47 | ] 48 | } 49 | } 50 | 51 | resource "aws_iam_policy" "idp_send_email" { 52 | name = "idp_send_email" 53 | policy = data.aws_iam_policy_document.idp_send_email.json 54 | } 55 | 56 | resource "aws_iam_access_key" "idp_send_email" { 57 | user = aws_iam_user.idp_send_email.name 58 | } 59 | -------------------------------------------------------------------------------- /aws/kms/outputs.tf: -------------------------------------------------------------------------------- 1 | output "kms_key_cloudwatch_arn" { 2 | description = "CloudWatch KMS key ARN" 3 | value = aws_kms_key.cloudwatch.arn 4 | } 5 | 6 | output "kms_key_cloudwatch_us_east_arn" { 7 | description = "CloudWatch KMS key ARN in us-east-1" 8 | value = aws_kms_key.cloudwatch_us_east.arn 9 | } 10 | 11 | output "kms_key_dynamodb_arn" { 12 | description = "DynamoDB KMS key ARN" 13 | value = aws_kms_key.dynamo_db.arn 14 | } -------------------------------------------------------------------------------- /aws/lambdas/api_end_to_end_test.tf: -------------------------------------------------------------------------------- 1 | # 2 | # API end to end test 3 | # 4 | 5 | resource "aws_lambda_function" "api_end_to_end_test" { 6 | function_name = "api-end-to-end-test" 7 | image_uri = "${var.ecr_repository_lambda_urls["api-end-to-end-test-lambda"]}:latest" 8 | package_type = "Image" 9 | role = aws_iam_role.lambda.arn 10 | timeout = 300 11 | 12 | vpc_config { 13 | security_group_ids = [var.api_end_to_end_test_lambda_security_group_id] 14 | subnet_ids = var.private_subnet_ids 15 | } 16 | 17 | lifecycle { 18 | ignore_changes = [image_uri] 19 | } 20 | 21 | environment { 22 | variables = { 23 | IDP_TRUSTED_DOMAIN = var.domain_idp 24 | IDP_URL = "http://${var.ecs_idp_service_name}.${var.service_discovery_private_dns_namespace_ecs_local_name}:${var.ecs_idp_service_port}" 25 | IDP_PROJECT_IDENTIFIER = var.idp_project_identifier 26 | API_URL = "http://${var.ecs_api_service_name}.${var.service_discovery_private_dns_namespace_ecs_local_name}:${var.ecs_api_service_port}" 27 | FORM_ID = var.api_end_to_end_test_form_identifier 28 | API_PRIVATE_KEY = var.api_end_to_end_test_form_api_private_key 29 | } 30 | } 31 | 32 | logging_config { 33 | log_format = "Text" 34 | log_group = "/aws/lambda/API_End_To_End_Test" 35 | } 36 | 37 | tracing_config { 38 | mode = "PassThrough" 39 | } 40 | } 41 | 42 | resource "aws_cloudwatch_log_group" "api_end_to_end_test" { 43 | name = "/aws/lambda/API_End_To_End_Test" 44 | kms_key_id = var.kms_key_cloudwatch_arn 45 | retention_in_days = 731 46 | } 47 | 48 | resource "aws_lambda_permission" "allow_cloudwatch_to_run_api_end_to_end_test_lambda" { 49 | statement_id = "AllowExecutionFromCloudWatch" 50 | action = "lambda:InvokeFunction" 51 | function_name = aws_lambda_function.api_end_to_end_test.function_name 52 | principal = "events.amazonaws.com" 53 | source_arn = aws_cloudwatch_event_rule.api_end_to_end_test_lambda_trigger.arn 54 | } 55 | -------------------------------------------------------------------------------- /aws/lambdas/audit_logs_archiver.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function" "audit_logs_archiver" { 2 | function_name = "audit-logs-archiver" 3 | image_uri = "${var.ecr_repository_lambda_urls["audit-logs-archiver-lambda"]}:latest" 4 | package_type = "Image" 5 | role = aws_iam_role.lambda.arn 6 | timeout = 900 7 | 8 | lifecycle { 9 | ignore_changes = [image_uri] 10 | } 11 | 12 | dynamic "vpc_config" { 13 | for_each = local.vpc_config 14 | content { 15 | security_group_ids = vpc_config.value.security_group_ids 16 | subnet_ids = vpc_config.value.subnet_ids 17 | } 18 | } 19 | 20 | environment { 21 | variables = { 22 | REGION = var.region 23 | AUDIT_LOGS_DYNAMODB_TABLE_NAME = var.dynamodb_app_audit_logs_table_name 24 | AUDIT_LOGS_ARCHIVE_STORAGE_S3_BUCKET = var.audit_logs_archive_storage_id 25 | } 26 | } 27 | 28 | logging_config { 29 | log_format = "Text" 30 | log_group = "/aws/lambda/Audit_Logs_Archiver" 31 | } 32 | 33 | tracing_config { 34 | mode = "PassThrough" 35 | } 36 | } 37 | 38 | resource "aws_lambda_permission" "audit_logs_archiver" { 39 | statement_id = "AllowExecutionFromCloudWatch" 40 | action = "lambda:InvokeFunction" 41 | function_name = aws_lambda_function.audit_logs_archiver.function_name 42 | principal = "events.amazonaws.com" 43 | source_arn = aws_cloudwatch_event_rule.audit_logs_archiver_lambda_trigger.arn 44 | } 45 | 46 | /* 47 | * When implementing containerized Lambda we had to rename some of the functions. 48 | * In order to keep existing log groups we decided to hardcode the group name and make the Lambda write to that legacy group. 49 | */ 50 | 51 | resource "aws_cloudwatch_log_group" "audit_logs_archiver" { 52 | name = "/aws/lambda/Audit_Logs_Archiver" 53 | kms_key_id = var.kms_key_cloudwatch_arn 54 | retention_in_days = 731 55 | } 56 | -------------------------------------------------------------------------------- /aws/lambdas/form_archiver.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Archive form templates 3 | # 4 | 5 | resource "aws_lambda_function" "form_archiver" { 6 | function_name = "form-archiver" 7 | image_uri = "${var.ecr_repository_lambda_urls["form-archiver-lambda"]}:latest" 8 | package_type = "Image" 9 | role = aws_iam_role.lambda.arn 10 | timeout = 300 11 | 12 | lifecycle { 13 | ignore_changes = [image_uri] 14 | } 15 | 16 | dynamic "vpc_config" { 17 | for_each = local.vpc_config 18 | content { 19 | security_group_ids = vpc_config.value.security_group_ids 20 | subnet_ids = vpc_config.value.subnet_ids 21 | } 22 | } 23 | 24 | environment { 25 | variables = { 26 | REGION = var.region 27 | DB_ARN = var.rds_cluster_arn 28 | DB_SECRET = var.database_secret_arn 29 | DB_NAME = var.rds_db_name 30 | } 31 | } 32 | 33 | logging_config { 34 | log_format = "Text" 35 | log_group = "/aws/lambda/Archive_Form_Templates" 36 | } 37 | 38 | tracing_config { 39 | mode = "PassThrough" 40 | } 41 | } 42 | 43 | resource "aws_lambda_permission" "allow_cloudwatch_to_run_form_archiver_lambda" { 44 | statement_id = "AllowExecutionFromCloudWatch" 45 | action = "lambda:InvokeFunction" 46 | function_name = aws_lambda_function.form_archiver.function_name 47 | principal = "events.amazonaws.com" 48 | source_arn = aws_cloudwatch_event_rule.form_archiver_lambda_trigger.arn 49 | } 50 | 51 | /* 52 | * When implementing containerized Lambda we had to rename some of the functions. 53 | * In order to keep existing log groups we decided to hardcode the group name and make the Lambda write to that legacy group. 54 | */ 55 | 56 | resource "aws_cloudwatch_log_group" "archive_form_templates" { 57 | name = "/aws/lambda/Archive_Form_Templates" 58 | kms_key_id = var.kms_key_cloudwatch_arn 59 | retention_in_days = 731 60 | } 61 | -------------------------------------------------------------------------------- /aws/lambdas/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | vpc_config = var.env == "development" ? [] : [{ 3 | security_group_ids = [var.lambda_security_group_id] 4 | subnet_ids = var.private_subnet_ids 5 | }] 6 | # Use account ID instead of environment name when in local development 7 | env = var.env == "development" ? var.account_id : var.env 8 | } 9 | 10 | -------------------------------------------------------------------------------- /aws/lambdas/prisma_migration.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function" "prisma_migration" { 2 | function_name = "prisma-migration" 3 | image_uri = "${var.ecr_repository_lambda_urls["prisma-migration-lambda"]}:latest" 4 | package_type = "Image" 5 | role = aws_iam_role.lambda.arn 6 | timeout = 300 7 | memory_size = 512 8 | 9 | lifecycle { 10 | ignore_changes = [image_uri] 11 | } 12 | 13 | // Even in development mode this lambda should be attached to the VPC in order to connecto the DB 14 | vpc_config { 15 | security_group_ids = [var.lambda_security_group_id] 16 | subnet_ids = var.private_subnet_ids 17 | } 18 | 19 | environment { 20 | variables = { 21 | DB_URL_SECRET_ARN = var.database_url_secret_arn 22 | PRISMA_S3_BUCKET_NAME = var.prisma_migration_storage_id 23 | } 24 | } 25 | 26 | logging_config { 27 | log_format = "Text" 28 | log_group = "/aws/lambda/Prisma_Migration_Handler" 29 | } 30 | 31 | tracing_config { 32 | mode = "PassThrough" 33 | } 34 | } 35 | 36 | 37 | /* 38 | * When implementing containerized Lambda we had to rename some of the functions. 39 | * In order to keep existing log groups we decided to hardcode the group name and make the Lambda write to that legacy group. 40 | */ 41 | 42 | resource "aws_cloudwatch_log_group" "prisma_migration_handler" { 43 | name = "/aws/lambda/Prisma_Migration_Handler" 44 | kms_key_id = var.kms_key_cloudwatch_arn 45 | retention_in_days = 731 46 | } 47 | -------------------------------------------------------------------------------- /aws/lambdas/response_archiver.tf: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Archive form responses 4 | # 5 | 6 | resource "aws_lambda_function" "response_archiver" { 7 | function_name = "response-archiver" 8 | image_uri = "${var.ecr_repository_lambda_urls["response-archiver-lambda"]}:latest" 9 | package_type = "Image" 10 | role = aws_iam_role.lambda.arn 11 | timeout = 900 12 | 13 | lifecycle { 14 | ignore_changes = [image_uri] 15 | } 16 | 17 | dynamic "vpc_config" { 18 | for_each = local.vpc_config 19 | content { 20 | security_group_ids = vpc_config.value.security_group_ids 21 | subnet_ids = vpc_config.value.subnet_ids 22 | } 23 | } 24 | 25 | environment { 26 | variables = { 27 | REGION = var.region 28 | DYNAMODB_VAULT_TABLE_NAME = var.dynamodb_vault_table_name 29 | ARCHIVING_S3_BUCKET = var.archive_storage_id 30 | VAULT_FILE_STORAGE_S3_BUCKET = var.vault_file_storage_id 31 | } 32 | } 33 | 34 | logging_config { 35 | log_format = "Text" 36 | log_group = "/aws/lambda/Response_Archiver" 37 | } 38 | 39 | tracing_config { 40 | mode = "PassThrough" 41 | } 42 | } 43 | 44 | resource "aws_lambda_permission" "allow_cloudwatch_to_run_archive_form_responses_lambda" { 45 | statement_id = "AllowExecutionFromCloudWatch" 46 | action = "lambda:InvokeFunction" 47 | function_name = aws_lambda_function.response_archiver.function_name 48 | principal = "events.amazonaws.com" 49 | source_arn = aws_cloudwatch_event_rule.response_archiver_lambda_trigger.arn 50 | } 51 | 52 | /* 53 | * When implementing containerized Lambda we had to rename some of the functions. 54 | * In order to keep existing log groups we decided to hardcode the group name and make the Lambda write to that legacy group. 55 | */ 56 | 57 | resource "aws_cloudwatch_log_group" "response_archiver" { 58 | name = "/aws/lambda/Response_Archiver" 59 | kms_key_id = var.kms_key_cloudwatch_arn 60 | retention_in_days = 731 61 | } 62 | -------------------------------------------------------------------------------- /aws/lambdas/vault_integrity.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Vault data integrity check 3 | # 4 | 5 | resource "aws_lambda_function" "vault_integrity" { 6 | function_name = "vault-integrity" 7 | image_uri = "${var.ecr_repository_lambda_urls["vault-integrity-lambda"]}:latest" 8 | package_type = "Image" 9 | role = aws_iam_role.lambda.arn 10 | timeout = 60 11 | 12 | // This lambda does not need to be connected to the VPC 13 | // It is a read-only operation that is invoked securely through the Lambda Private Link Endpoint 14 | 15 | lifecycle { 16 | ignore_changes = [image_uri] 17 | } 18 | 19 | logging_config { 20 | log_format = "Text" 21 | log_group = "/aws/lambda/Vault_Data_Integrity_Check" 22 | } 23 | 24 | tracing_config { 25 | mode = "PassThrough" 26 | } 27 | } 28 | 29 | resource "aws_lambda_event_source_mapping" "vault_updated_item_stream" { 30 | event_source_arn = var.dynamodb_vault_stream_arn 31 | function_name = aws_lambda_function.vault_integrity.arn 32 | starting_position = "LATEST" 33 | maximum_batching_window_in_seconds = 60 # Either 1 minute of waiting or 100 events are available before the lambda is triggered 34 | maximum_retry_attempts = 3 35 | 36 | filter_criteria { 37 | filter { 38 | pattern = jsonencode({ 39 | eventName : ["INSERT", "MODIFY"] 40 | }) 41 | } 42 | } 43 | } 44 | 45 | /* 46 | * When implementing containerized Lambda we had to rename some of the functions. 47 | * In order to keep existing log groups we decided to hardcode the group name and make the Lambda write to that legacy group. 48 | */ 49 | 50 | resource "aws_cloudwatch_log_group" "vault_integrity" { 51 | name = "/aws/lambda/Vault_Data_Integrity_Check" 52 | kms_key_id = var.kms_key_cloudwatch_arn 53 | retention_in_days = 731 54 | } 55 | -------------------------------------------------------------------------------- /aws/load_balancer/alarms.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_metric_alarm" "UnHealthyHostCount-TargetGroup1" { 2 | alarm_name = "App-UnHealthyHostCount-TargetGroup1" 3 | alarm_description = "App LB Warning - unhealthy host count >= 1 in a 1 minute period for TargetGroup1" 4 | comparison_operator = "GreaterThanOrEqualToThreshold" 5 | threshold = "1" # If there is at least one unhealthy host 6 | evaluation_periods = "1" # Evaluate once 7 | metric_name = "UnHealthyHostCount" 8 | namespace = "AWS/ApplicationELB" 9 | period = "60" # Every minute 10 | statistic = "Maximum" # the highest value observed during the specified period 11 | treat_missing_data = "breaching" 12 | dimensions = { 13 | LoadBalancer = aws_lb.form_viewer.arn_suffix 14 | TargetGroup = aws_lb_target_group.form_viewer_1.arn_suffix 15 | } 16 | } 17 | 18 | resource "aws_cloudwatch_metric_alarm" "UnHealthyHostCount-TargetGroup2" { 19 | alarm_name = "App-UnHealthyHostCount-TargetGroup2" 20 | alarm_description = "App LB Warning - unhealthy host count >= 1 in a 1 minute period for TargetGroup2" 21 | comparison_operator = "GreaterThanOrEqualToThreshold" 22 | threshold = "1" # If there is at least one unhealthy host 23 | evaluation_periods = "1" # Evaluate once 24 | metric_name = "UnHealthyHostCount" 25 | namespace = "AWS/ApplicationELB" 26 | period = "60" # Every minute 27 | statistic = "Maximum" # the highest value observed during the specified period 28 | treat_missing_data = "breaching" 29 | dimensions = { 30 | LoadBalancer = aws_lb.form_viewer.arn_suffix 31 | TargetGroup = aws_lb_target_group.form_viewer_2.arn_suffix 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /aws/load_balancer/certificates.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Domain certificate 3 | # 4 | 5 | resource "aws_acm_certificate" "form_viewer" { 6 | # First entry in domain list is the primary domain 7 | domain_name = var.domains[0] 8 | validation_method = "DNS" 9 | subject_alternative_names = length(var.domains) > 1 ? setsubtract(var.domains, [var.domains[0]]) : [] 10 | 11 | lifecycle { 12 | create_before_destroy = true 13 | } 14 | } 15 | 16 | resource "aws_acm_certificate" "form_viewer_maintenance_mode" { 17 | # First entry in domain list is the primary domain 18 | domain_name = var.domains[0] 19 | validation_method = "DNS" 20 | subject_alternative_names = length(var.domains) > 1 ? setsubtract(var.domains, [var.domains[0]]) : [] 21 | 22 | provider = aws.us-east-1 23 | 24 | lifecycle { 25 | create_before_destroy = true 26 | } 27 | } 28 | 29 | resource "aws_acm_certificate" "forms_api" { 30 | domain_name = var.domain_api 31 | validation_method = "DNS" 32 | 33 | lifecycle { 34 | create_before_destroy = true 35 | } 36 | } 37 | 38 | resource "aws_acm_certificate_validation" "form_viewer_maintenance_mode_cloudfront_certificate" { 39 | certificate_arn = aws_acm_certificate.form_viewer_maintenance_mode.arn 40 | validation_record_fqdns = [for record in aws_route53_record.form_viewer_maintenance_mode_certificate_validation : record.fqdn] 41 | 42 | provider = aws.us-east-1 43 | } 44 | 45 | resource "aws_acm_certificate_validation" "forms_api" { 46 | certificate_arn = aws_acm_certificate.forms_api.arn 47 | validation_record_fqdns = [for record in aws_route53_record.forms_api_certificate_validation : record.fqdn] 48 | } 49 | -------------------------------------------------------------------------------- /aws/load_balancer/cloudfront.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | s3_origin_id = "MaintenanceMode" 3 | } 4 | 5 | resource "aws_cloudfront_origin_access_identity" "maintenance_mode" { 6 | comment = "Access Identity for the Maintenance Website" 7 | } 8 | 9 | resource "aws_cloudfront_distribution" "maintenance_mode" { 10 | # checkov:skip=CKV2_AWS_32: Access logging not required 11 | # checkov:skip=CKV_AWS_86: Access logging not required 12 | # checkov:skip=CKV_AWS_310: Origin failover configuration not required 13 | enabled = true 14 | http_version = "http2" 15 | default_root_object = "index.html" 16 | web_acl_id = aws_wafv2_web_acl.forms_maintenance_mode_acl.arn 17 | price_class = "PriceClass_100" 18 | aliases = var.domains 19 | 20 | origin { 21 | origin_id = local.s3_origin_id 22 | domain_name = aws_s3_bucket.maintenance_mode.bucket_regional_domain_name 23 | 24 | s3_origin_config { 25 | origin_access_identity = aws_cloudfront_origin_access_identity.maintenance_mode.cloudfront_access_identity_path 26 | } 27 | } 28 | 29 | default_cache_behavior { 30 | compress = true 31 | allowed_methods = ["GET", "HEAD"] 32 | cached_methods = ["GET", "HEAD"] 33 | target_origin_id = local.s3_origin_id 34 | 35 | forwarded_values { 36 | query_string = false 37 | 38 | cookies { 39 | forward = "none" 40 | } 41 | } 42 | 43 | viewer_protocol_policy = "redirect-to-https" 44 | min_ttl = 0 45 | default_ttl = 3600 46 | max_ttl = 86400 47 | } 48 | 49 | restrictions { 50 | geo_restriction { 51 | restriction_type = "none" 52 | } 53 | } 54 | 55 | viewer_certificate { 56 | acm_certificate_arn = aws_acm_certificate.form_viewer_maintenance_mode.arn 57 | minimum_protocol_version = "TLSv1.2_2019" 58 | ssl_support_method = "sni-only" 59 | } 60 | 61 | 62 | 63 | depends_on = [ 64 | aws_s3_bucket.maintenance_mode 65 | ] 66 | } -------------------------------------------------------------------------------- /aws/load_balancer/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "alb_security_group_id" { 2 | description = "Security group ID for the ALB" 3 | type = string 4 | } 5 | 6 | variable "hosted_zone_ids" { 7 | description = "Route53 hosted zone ID" 8 | type = list(string) 9 | } 10 | 11 | variable "public_subnet_ids" { 12 | description = "Public subnet IDs for the ALB" 13 | type = list(string) 14 | } 15 | 16 | variable "vpc_id" { 17 | description = "VPC ID to associate the load balancer with" 18 | type = string 19 | } -------------------------------------------------------------------------------- /aws/load_balancer/kinesis.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Kinesis Firehose 3 | # 4 | resource "aws_kinesis_firehose_delivery_stream" "firehose_waf_logs" { 5 | # checkov:skip=CKV_AWS_241: Encryption using CMK not required 6 | name = "aws-waf-logs-forms" 7 | destination = "extended_s3" 8 | 9 | server_side_encryption { 10 | enabled = true 11 | } 12 | 13 | extended_s3_configuration { 14 | role_arn = aws_iam_role.firehose_waf_logs.arn 15 | prefix = "waf_acl_logs/AWSLogs/${var.account_id}/" 16 | bucket_arn = local.cbs_satellite_bucket_arn 17 | compression_format = "GZIP" 18 | } 19 | } 20 | 21 | # 22 | # IAM role 23 | # 24 | resource "aws_iam_role" "firehose_waf_logs" { 25 | name = "firehose_waf_logs" 26 | assume_role_policy = data.aws_iam_policy_document.firehose_waf_assume.json 27 | } 28 | 29 | resource "aws_iam_role_policy" "firehose_waf_logs" { 30 | name = "firehose-waf-logs-policy" 31 | role = aws_iam_role.firehose_waf_logs.id 32 | policy = data.aws_iam_policy_document.firehose_waf_policy.json 33 | } 34 | 35 | data "aws_iam_policy_document" "firehose_waf_assume" { 36 | statement { 37 | actions = ["sts:AssumeRole"] 38 | principals { 39 | type = "Service" 40 | identifiers = ["firehose.amazonaws.com"] 41 | } 42 | } 43 | } 44 | 45 | data "aws_iam_policy_document" "firehose_waf_policy" { 46 | statement { 47 | effect = "Allow" 48 | actions = [ 49 | "s3:AbortMultipartUpload", 50 | "s3:GetBucketLocation", 51 | "s3:GetObject", 52 | "s3:ListBucket", 53 | "s3:ListBucketMultipartUploads", 54 | "s3:PutObject" 55 | ] 56 | resources = [ 57 | local.cbs_satellite_bucket_arn, 58 | "${local.cbs_satellite_bucket_arn}/*" 59 | ] 60 | } 61 | 62 | statement { 63 | effect = "Allow" 64 | actions = [ 65 | "iam:CreateServiceLinkedRole" 66 | ] 67 | resources = [ 68 | "arn:aws:iam::*:role/aws-service-role/wafv2.amazonaws.com/AWSServiceRoleForWAFV2Logging" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /aws/load_balancer/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | all_domains = concat(var.domains, [var.domain_api]) 3 | cbs_satellite_bucket_arn = "arn:aws:s3:::${var.cbs_satellite_bucket_name}" 4 | } 5 | -------------------------------------------------------------------------------- /aws/load_balancer/route53_health_checks.tf: -------------------------------------------------------------------------------- 1 | resource "aws_route53_health_check" "lb_web_app_target_group_1" { 2 | reference_name = "LB Tar Group 1 health" 3 | type = "CLOUDWATCH_METRIC" 4 | cloudwatch_alarm_name = aws_cloudwatch_metric_alarm.UnHealthyHostCount-TargetGroup1.alarm_name 5 | cloudwatch_alarm_region = var.region 6 | insufficient_data_health_status = "Unhealthy" 7 | } 8 | 9 | resource "aws_route53_health_check" "lb_web_app_target_group_2" { 10 | reference_name = "LB Tar Group 2 health" 11 | type = "CLOUDWATCH_METRIC" 12 | cloudwatch_alarm_name = aws_cloudwatch_metric_alarm.UnHealthyHostCount-TargetGroup2.alarm_name 13 | cloudwatch_alarm_region = var.region 14 | insufficient_data_health_status = "Unhealthy" 15 | } 16 | 17 | # Due to our blue green deployment strategy, one of the Web App target groups is always going to be left empty and the associated alarm 18 | # will be triggered because of that. This health check ensures that at least one target group is healthy. If not then we will be able to 19 | # serve the maintenance page to our visitors. 20 | resource "aws_route53_health_check" "lb_web_app_global_target_group" { 21 | reference_name = "Active LB Tar Group health" 22 | type = "CALCULATED" 23 | child_health_threshold = 1 24 | child_healthchecks = [ 25 | aws_route53_health_check.lb_web_app_target_group_1.id, 26 | aws_route53_health_check.lb_web_app_target_group_2.id 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /aws/load_balancer/shield.tf: -------------------------------------------------------------------------------- 1 | resource "aws_shield_protection" "alb" { 2 | name = "LoadBalancer" 3 | resource_arn = aws_lb.form_viewer.arn 4 | 5 | 6 | } 7 | 8 | resource "aws_shield_protection" "route53_hosted_zone" { 9 | count = length(var.hosted_zone_ids) 10 | name = "Route53HostedZone" 11 | resource_arn = "arn:aws:route53:::hostedzone/${var.hosted_zone_ids[count.index]}" 12 | 13 | 14 | } 15 | -------------------------------------------------------------------------------- /aws/load_balancer/static_website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/aws/load_balancer/static_website/favicon.ico -------------------------------------------------------------------------------- /aws/load_balancer/static_website/index-fr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Site actuellement indisponible 7 | 8 | 10 | 11 | 13 | 15 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 |
Site indisponible
29 |
30 |

Le site est actuellement indisponible

31 |

Nous nous efforçons de résoudre ce problème et d'améliorer votre expérience. Veuillez réessayer plus tard.

32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 | -------------------------------------------------------------------------------- /aws/load_balancer/static_website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Site currently unavailable 7 | 8 | 10 | 11 | 13 | 15 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 |
Site unavailable
29 |
30 |

The site is currently unavailable

31 |

We're focused on fixing this and improving your experience. Please try again later.

32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 | -------------------------------------------------------------------------------- /aws/load_balancer/static_website/site-unavailable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /aws/load_balancer/static_website/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | margin: 20vh auto; 9 | flex: 1; 10 | min-width: 60%; 11 | } 12 | 13 | #card { 14 | border: 1px solid #284162; 15 | border-radius: 4px; 16 | background-color: #f9fafb; 17 | padding: 36px; 18 | display: flex; 19 | } 20 | 21 | #card div.icon { 22 | margin-right: 36px; 23 | } 24 | 25 | #card div.description { 26 | font-family: "Lato"; 27 | line-height: 32px; 28 | } 29 | 30 | #card div h1 { 31 | font-size: 36px; 32 | font-weight: 700; 33 | margin: 2px 0 26px 0; 34 | } 35 | 36 | #card div p { 37 | font-size: 18px; 38 | font-weight: 400; 39 | margin: 0; 40 | } -------------------------------------------------------------------------------- /aws/load_testing/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "ecr_repository_url_load_testing_lambda" { 2 | description = "URL of the Load Testing Lambda ECR" 3 | type = string 4 | } 5 | 6 | variable "lambda_submission_function_name" { 7 | description = "Name of the Submission Lambda function." 8 | type = string 9 | } 10 | 11 | variable "load_testing_form_id" { 12 | description = "Form ID that will be used to generate, retrieve and confirm responses." 13 | type = string 14 | sensitive = true 15 | } 16 | 17 | variable "load_testing_form_private_key" { 18 | description = "Private key JSON of the form that will be used to authenticate the API requests. This must be a key from the `var.load_testing_form_id` form." 19 | type = string 20 | sensitive = true 21 | } 22 | 23 | variable "load_testing_zitadel_app_private_key" { 24 | description = "Private key JSON of the Zitadel application to perform access token introspection requests." 25 | type = string 26 | sensitive = true 27 | } 28 | -------------------------------------------------------------------------------- /aws/load_testing/parameters.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ssm_parameter" "load_testing_form_id" { 2 | # checkov:skip=CKV_AWS_337: default service encryption key is acceptable 3 | name = "/load-testing/form-id" 4 | description = "Form ID that will be used to generate, retrieve and confirm responses." 5 | type = "SecureString" 6 | value = var.load_testing_form_id 7 | } 8 | 9 | resource "aws_ssm_parameter" "load_testing_form_private_key" { 10 | # checkov:skip=CKV_AWS_337: default service encryption key is acceptable 11 | name = "/load-testing/form-private-key" 12 | description = "Private key JSON of the form that will be used to authenticate the API requests. This must be a key for the `/load-testing/form-id` form." 13 | type = "SecureString" 14 | value = var.load_testing_form_private_key 15 | } 16 | 17 | resource "aws_ssm_parameter" "load_testing_zitadel_app_private_key" { 18 | # checkov:skip=CKV_AWS_337: default service encryption key is acceptable 19 | name = "/load-testing/zitadel-app-private-key" 20 | description = "Private key JSON of the Zitadel application to perform access token introspection requests." 21 | type = "SecureString" 22 | value = var.load_testing_zitadel_app_private_key 23 | } 24 | -------------------------------------------------------------------------------- /aws/network/development_env/vpc_endpoints.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | private_subnet_ids = [aws_subnet.forms_private[0].id] 3 | } 4 | 5 | resource "aws_vpc_endpoint" "dynamodb" { 6 | vpc_id = aws_vpc.forms.id 7 | vpc_endpoint_type = "Gateway" 8 | service_name = "com.amazonaws.${var.region}.dynamodb" 9 | route_table_ids = [aws_vpc.forms.main_route_table_id] 10 | } 11 | 12 | resource "aws_vpc_endpoint" "s3" { 13 | vpc_id = aws_vpc.forms.id 14 | vpc_endpoint_type = "Gateway" 15 | service_name = "com.amazonaws.${var.region}.s3" 16 | route_table_ids = [aws_vpc.forms.main_route_table_id] 17 | } 18 | 19 | resource "aws_vpc_endpoint" "secretsmanager" { 20 | vpc_id = aws_vpc.forms.id 21 | vpc_endpoint_type = "Interface" 22 | service_name = "com.amazonaws.${var.region}.secretsmanager" 23 | private_dns_enabled = true 24 | security_group_ids = [ 25 | aws_security_group.privatelink.id, 26 | ] 27 | subnet_ids = local.private_subnet_ids 28 | } -------------------------------------------------------------------------------- /aws/network/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_cidr_block" { 2 | description = "IP CIDR block of the VPC" 3 | type = string 4 | } 5 | 6 | variable "vpc_name" { 7 | description = "The name of the VPC" 8 | type = string 9 | } 10 | -------------------------------------------------------------------------------- /aws/network/vpc_flow_log.tf: -------------------------------------------------------------------------------- 1 | # 2 | # VPC Flow Logs: 3 | # Capture network traffic over the VPC 4 | # 5 | resource "aws_flow_log" "vpc_flow_logs" { 6 | count = var.env == "development" ? 0 : 1 7 | log_destination = "arn:aws:s3:::${var.cbs_satellite_bucket_name}/vpc_flow_logs/" 8 | log_destination_type = "s3" 9 | traffic_type = "ALL" 10 | vpc_id = aws_vpc.forms.id 11 | log_format = "$${vpc-id} $${version} $${account-id} $${interface-id} $${srcaddr} $${dstaddr} $${srcport} $${dstport} $${protocol} $${packets} $${bytes} $${start} $${end} $${action} $${log-status} $${subnet-id} $${instance-id}" 12 | } 13 | -------------------------------------------------------------------------------- /aws/pr_review/ecr.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Holds the Forms app images used by the Lambda preview service 3 | # 4 | resource "aws_ecr_repository" "pr_review_repository" { 5 | count = var.env == "staging" ? 1 : 0 6 | 7 | name = "pr_review" 8 | image_tag_mutability = "MUTABLE" 9 | 10 | image_scanning_configuration { 11 | scan_on_push = true 12 | } 13 | } 14 | 15 | resource "aws_ecr_lifecycle_policy" "pr_review_policy" { 16 | count = var.env == "staging" ? 1 : 0 17 | 18 | repository = aws_ecr_repository.pr_review_repository[0].name 19 | policy = jsonencode({ 20 | rules = [{ 21 | rulePriority = 1 22 | description = "Keep last 30 images" 23 | 24 | selection = { 25 | tagStatus = "tagged" 26 | tagPrefixList = ["v"] 27 | countType = "imageCountMoreThan" 28 | countNumber = 30 29 | } 30 | 31 | action = { 32 | type = "expire" 33 | } 34 | }] 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /aws/pr_review/iam.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "forms_lambda_parameter_store" { 2 | count = var.env == "staging" ? 1 : 0 3 | 4 | statement { 5 | actions = ["ssm:GetParameters"] 6 | effect = "Allow" 7 | resources = [ 8 | "arn:aws:ssm:ca-central-1:${var.account_id}:parameter/form-viewer/env" 9 | ] 10 | } 11 | } 12 | 13 | resource "aws_iam_policy" "forms_lambda_parameter_store" { 14 | count = var.env == "staging" ? 1 : 0 15 | 16 | name = "formsLambdaParameterStoreRetrieval" 17 | path = "/" 18 | policy = data.aws_iam_policy_document.forms_lambda_parameter_store[0].json 19 | } 20 | 21 | resource "aws_iam_role_policy_attachment" "forms_lambda_parameter_store" { 22 | count = var.env == "staging" ? 1 : 0 23 | 24 | role = var.forms_lambda_client_iam_role_name 25 | policy_arn = aws_iam_policy.forms_lambda_parameter_store[0].arn 26 | } 27 | -------------------------------------------------------------------------------- /aws/pr_review/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_id" { 2 | description = "VPC ID to attach the Lambda's security group to" 3 | type = string 4 | } 5 | 6 | variable "privatelink_security_group_id" { 7 | description = "Security group ID for the private link" 8 | type = string 9 | } 10 | 11 | variable "forms_database_security_group_id" { 12 | description = "Security group ID for the database" 13 | type = string 14 | } 15 | 16 | variable "forms_redis_security_group_id" { 17 | description = "Security group ID for the redis" 18 | type = string 19 | } 20 | 21 | variable "forms_lambda_client_iam_role_name" { 22 | description = "IAM role name for forms client Lambda" 23 | type = string 24 | } 25 | 26 | variable "idp_ecs_security_group_id" { 27 | description = "IdP ECS task security group ID" 28 | type = string 29 | } 30 | -------------------------------------------------------------------------------- /aws/rds/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "private_subnet_ids" { 2 | description = "The list of private subnet IDs to attach the RDS cluster to" 3 | type = list(string) 4 | } 5 | 6 | variable "rds_db_name" { 7 | description = "The name of the RDS database" 8 | type = string 9 | } 10 | 11 | variable "rds_connector_db_user" { 12 | description = "The username the RDS connector uses to connect to the database" 13 | type = string 14 | } 15 | 16 | variable "rds_connector_db_password" { 17 | description = "The password the RDS connector uses to connect to the database" 18 | type = string 19 | sensitive = true 20 | } 21 | 22 | variable "rds_db_user" { 23 | description = "The username of the RDS database" 24 | type = string 25 | } 26 | 27 | variable "rds_db_password" { 28 | description = "The password for the RDS database" 29 | type = string 30 | sensitive = true 31 | } 32 | 33 | variable "rds_db_subnet_group_name" { 34 | description = "The name of the RDS database subnet group" 35 | type = string 36 | } 37 | 38 | variable "rds_name" { 39 | description = "The name of the RDS cluster" 40 | type = string 41 | } 42 | 43 | variable "rds_security_group_id" { 44 | description = "The security group used by Redis" 45 | type = string 46 | } 47 | -------------------------------------------------------------------------------- /aws/rds/outputs.tf: -------------------------------------------------------------------------------- 1 | # The private subnets are required to match the cluster instance's availability zone 2 | # to its subnet ID. 3 | data "aws_subnet" "private" { 4 | for_each = toset(var.private_subnet_ids) 5 | id = each.value 6 | } 7 | 8 | output "database_secret_arn" { 9 | description = "value" 10 | value = aws_secretsmanager_secret_version.database_secret.arn 11 | } 12 | 13 | output "database_url_secret_arn" { 14 | description = "value" 15 | value = aws_secretsmanager_secret_version.database_url.arn 16 | } 17 | 18 | output "rds_cluster_arn" { 19 | description = "RDS cluster ARN" 20 | value = aws_rds_cluster.forms.arn 21 | } 22 | 23 | output "rds_cluster_identifier" { 24 | description = "RDS cluster identifier" 25 | value = aws_rds_cluster.forms.cluster_identifier 26 | } 27 | 28 | output "rds_cluster_instance_availability_zone" { 29 | description = "RDS cluster instance's availability zone" 30 | value = aws_rds_cluster_instance.forms.availability_zone 31 | } 32 | 33 | output "rds_cluster_instance_identifier" { 34 | description = "RDS cluster instance's identifier" 35 | value = aws_rds_cluster_instance.forms.identifier 36 | } 37 | 38 | output "rds_cluster_instance_subnet_id" { 39 | description = "RDS cluster instance's subnet ID, null if not found" 40 | value = try( 41 | [for subnet in data.aws_subnet.private : subnet.id if subnet.availability_zone == aws_rds_cluster_instance.forms.availability_zone][0], 42 | null 43 | ) 44 | } 45 | 46 | output "rds_connector_secret_arn" { 47 | description = "RDS connector secret ARN" 48 | value = aws_secretsmanager_secret.rds_connector.arn 49 | } 50 | 51 | output "rds_connector_secret_name" { 52 | description = "RDS connector secret name" 53 | value = aws_secretsmanager_secret.rds_connector.name 54 | } 55 | 56 | output "rds_db_name" { 57 | description = "Name of the database" 58 | value = var.rds_db_name 59 | } 60 | 61 | output "rds_cluster_reader_endpoint" { 62 | description = "RDS cluster endpoint" 63 | sensitive = true 64 | value = aws_rds_cluster.forms.reader_endpoint 65 | } 66 | -------------------------------------------------------------------------------- /aws/rds/secrets.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Database secrets 3 | # 4 | resource "aws_secretsmanager_secret" "database_url" { 5 | # checkov:skip=CKV2_AWS_57: Automatic secret rotation not required 6 | name = "server-database-url" 7 | recovery_window_in_days = 0 8 | } 9 | 10 | resource "aws_secretsmanager_secret_version" "database_url" { 11 | secret_id = aws_secretsmanager_secret.database_url.id 12 | secret_string = "postgres://${var.rds_db_user}:${var.rds_db_password}@${aws_rds_cluster.forms.endpoint}:5432/${var.rds_db_name}" 13 | } 14 | 15 | resource "aws_secretsmanager_secret" "database_secret" { 16 | # checkov:skip=CKV2_AWS_57: Automatic secret rotation not required 17 | name = "database-secret" 18 | recovery_window_in_days = 0 19 | } 20 | 21 | resource "aws_secretsmanager_secret_version" "database_secret" { 22 | depends_on = [aws_rds_cluster.forms] 23 | secret_id = aws_secretsmanager_secret.database_secret.id 24 | secret_string = "{\"dbInstanceIdentifier\": \"${var.rds_name}-cluster\",\"engine\": \"${aws_rds_cluster.forms.engine}\",\"host\": \"${aws_rds_cluster.forms.endpoint}\",\"port\": ${aws_rds_cluster.forms.port},\"resourceId\": \"${aws_rds_cluster.forms.cluster_resource_id}\",\"username\": \"${var.rds_db_user}\",\"password\": \"${var.rds_db_password}\"}" 25 | } 26 | 27 | resource "aws_secretsmanager_secret" "rds_connector" { 28 | # checkov:skip=CKV2_AWS_57: Automatic secret rotation not required 29 | name = "rds-connector" 30 | recovery_window_in_days = 0 31 | } 32 | 33 | resource "aws_secretsmanager_secret_version" "rds_connector" { 34 | depends_on = [aws_rds_cluster.forms] 35 | secret_id = aws_secretsmanager_secret.rds_connector.id 36 | secret_string = "{\"username\": \"${var.rds_connector_db_user}\",\"password\": \"${var.rds_connector_db_password}\"}" 37 | } 38 | -------------------------------------------------------------------------------- /aws/redis/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "private_subnet_ids" { 2 | description = "The list of private subnet IDs to attach Redis to" 3 | type = list(string) 4 | } 5 | 6 | variable "redis_security_group_id" { 7 | description = "The security group used by Redis" 8 | type = string 9 | } 10 | -------------------------------------------------------------------------------- /aws/redis/outputs.tf: -------------------------------------------------------------------------------- 1 | output "redis_port" { 2 | description = "The Redis port" 3 | value = aws_elasticache_replication_group.redis.port 4 | } 5 | 6 | output "redis_url" { 7 | description = "The Redis endpoint URL" 8 | value = aws_elasticache_replication_group.redis.primary_endpoint_address 9 | } 10 | -------------------------------------------------------------------------------- /aws/redis/redis.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ElastiCache (Redis) 3 | # Stores Form app feature flags. 4 | # 5 | 6 | resource "aws_elasticache_replication_group" "redis" { 7 | # checkov:skip=CKV_AWS_191: KMS encryption using customer managed key not required 8 | # checkov:skip=CKV2_AWS_50: Multi AZ is enabled in staging and production 9 | automatic_failover_enabled = var.env != "development" 10 | replication_group_id = "gcforms-redis-rep-group" 11 | description = "Redis cluster for GCForms" 12 | node_type = "cache.t2.micro" 13 | num_cache_clusters = var.env == "development" ? 1 : 2 14 | engine_version = "6.x" 15 | parameter_group_name = "default.redis6.x" 16 | port = 6379 17 | multi_az_enabled = var.env != "development" 18 | subnet_group_name = aws_elasticache_subnet_group.redis.name 19 | security_group_ids = [var.redis_security_group_id] 20 | } 21 | 22 | resource "aws_elasticache_subnet_group" "redis" { 23 | name = "redis-subnet-group" 24 | subnet_ids = var.private_subnet_ids 25 | } -------------------------------------------------------------------------------- /aws/s3/data_lake.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Holds ETL scripts 3 | # 4 | module "etl_bucket" { 5 | source = "github.com/cds-snc/terraform-modules//S3?ref=17994187b8628dc5decf74ead84768501378df4c" # ref for v10.0.0 6 | bucket_name = "cds-forms-data-etl-bucket-${local.env}" 7 | billing_tag_value = var.billing_tag_value 8 | force_destroy = var.env == "development" 9 | 10 | 11 | logging = { 12 | target_bucket = module.log_bucket.s3_bucket_id 13 | target_prefix = "dataetl/" 14 | } 15 | 16 | lifecycle_rule = [ 17 | local.lifecycle_remove_noncurrent_versions 18 | ] 19 | 20 | versioning = { 21 | enabled = true 22 | } 23 | } 24 | 25 | # 26 | # Holds exported data from ETL transformations 27 | # 28 | module "lake_bucket" { 29 | source = "github.com/cds-snc/terraform-modules//S3?ref=17994187b8628dc5decf74ead84768501378df4c" # ref for v10.0.0 30 | bucket_name = "cds-forms-data-lake-bucket-${local.env}" 31 | billing_tag_value = var.billing_tag_value 32 | force_destroy = var.env == "development" 33 | 34 | logging = { 35 | target_bucket = module.log_bucket.s3_bucket_id 36 | target_prefix = "datalake/" 37 | } 38 | 39 | lifecycle_rule = [ 40 | local.lifecycle_remove_noncurrent_versions, 41 | local.lifecycle_transition_storage 42 | ] 43 | 44 | versioning = { 45 | enabled = true 46 | } 47 | } 48 | 49 | # 50 | # Bucket access logs, stored for 30 days 51 | # 52 | module "log_bucket" { 53 | source = "github.com/cds-snc/terraform-modules//S3_log_bucket?ref=17994187b8628dc5decf74ead84768501378df4c" # ref for v10.0.0 54 | bucket_name = "cds-forms-data-lake-bucket-logs-${local.env}" 55 | versioning_status = "Enabled" 56 | force_destroy = var.env == "development" 57 | 58 | lifecycle_rule = [ 59 | local.lifecycle_expire_all, 60 | local.lifecycle_remove_noncurrent_versions 61 | ] 62 | 63 | billing_tag_value = var.billing_tag_value 64 | } -------------------------------------------------------------------------------- /aws/s3/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # Remove all objects after expiry 3 | lifecycle_expire_all = { 4 | id = "expire_all" 5 | enabled = true 6 | expiration = { 7 | days = "30" 8 | } 9 | } 10 | # Cleanup old versions and incomplete uploads 11 | lifecycle_remove_noncurrent_versions = { 12 | id = "remove_noncurrent_versions" 13 | enabled = true 14 | abort_incomplete_multipart_upload_days = "7" 15 | noncurrent_version_expiration = { 16 | days = "30" 17 | } 18 | } 19 | # Transition objects to cheaper storage classes over time 20 | lifecycle_transition_storage = { 21 | id = "transition_storage" 22 | enabled = true 23 | transition = [ 24 | { 25 | days = "90" 26 | storage_class = "STANDARD_IA" 27 | }, 28 | { 29 | days = "180" 30 | storage_class = "GLACIER" 31 | } 32 | ] 33 | } 34 | # Use account ID instead of environment name when in local development 35 | env = var.env == "development" ? var.account_id : var.env 36 | } -------------------------------------------------------------------------------- /aws/secrets/inputs.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "ecs_secret_token" { 3 | description = "Forms ECS JSON Web Token (JWT) secret used by Templates lambda" 4 | type = string 5 | sensitive = true 6 | } 7 | 8 | variable "recaptcha_secret" { 9 | description = "Secret Site Key for reCAPTCHA" 10 | type = string 11 | sensitive = true 12 | } 13 | 14 | variable "notify_callback_bearer_token" { 15 | description = "GC Notify callback bearer token which will be used as an authentication factor in GC Forms" 16 | type = string 17 | sensitive = true 18 | } 19 | 20 | variable "notify_api_key" { 21 | description = "The Notify API key used by the ECS task and Lambda" 22 | type = string 23 | sensitive = true 24 | } 25 | 26 | variable "freshdesk_api_key" { 27 | description = "The FreshDesk API key used by the ECS task and Lambda" 28 | type = string 29 | sensitive = true 30 | } 31 | 32 | variable "sentry_api_key" { 33 | description = "The Sentry API key used by the ECS task and Lambda" 34 | type = string 35 | sensitive = true 36 | } 37 | 38 | variable "zitadel_administration_key" { 39 | description = "The Zitadel administration key used by the ECS task and Lambda" 40 | type = string 41 | sensitive = true 42 | } 43 | 44 | variable "zitadel_application_key" { 45 | description = "The Zitadel application key used by the ECS task (API)" 46 | type = string 47 | sensitive = true 48 | } 49 | 50 | variable "hcaptcha_site_verify_key" { 51 | description = "The hCaptcha site verify key secret used for forms" 52 | type = string 53 | sensitive = true 54 | } 55 | -------------------------------------------------------------------------------- /aws/secrets/outputs.tf: -------------------------------------------------------------------------------- 1 | output "notify_api_key_secret_arn" { 2 | description = "ARN of notify_api_key secret" 3 | value = aws_secretsmanager_secret_version.notify_api_key.arn 4 | } 5 | 6 | output "freshdesk_api_key_secret_arn" { 7 | description = "ARN of freshdesk_api_key secret" 8 | value = aws_secretsmanager_secret.freshdesk_api_key.arn 9 | } 10 | 11 | output "sentry_api_key_secret_arn" { 12 | description = "ARN of sentry_api_key secret" 13 | value = aws_secretsmanager_secret.sentry_api_key.arn 14 | } 15 | 16 | output "token_secret_arn" { 17 | description = "ARN of tokensecret" 18 | value = aws_secretsmanager_secret.token_secret.arn 19 | } 20 | 21 | output "recaptcha_secret_arn" { 22 | description = "ARN of recaptcha_secret" 23 | value = aws_secretsmanager_secret.recaptcha_secret.arn 24 | } 25 | 26 | output "notify_callback_bearer_token_secret_arn" { 27 | description = "ARN of notify_callback_bearer_token_secret" 28 | value = aws_secretsmanager_secret.notify_callback_bearer_token.arn 29 | } 30 | 31 | output "zitadel_administration_key_secret_arn" { 32 | description = "ARN of zitadel_administration_key secret" 33 | value = aws_secretsmanager_secret_version.zitadel_administration_key.arn 34 | } 35 | 36 | output "zitadel_application_key_secret_arn" { 37 | description = "ARN of zitadel_application_key secret" 38 | value = aws_secretsmanager_secret_version.zitadel_application_key.arn 39 | } 40 | 41 | output "hcaptcha_site_verify_key_secret_arn" { 42 | description = "The hCaptcha site verify key secret used for forms" 43 | value = aws_secretsmanager_secret_version.hcaptcha_site_verify_key.arn 44 | } 45 | -------------------------------------------------------------------------------- /aws/sns/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "kms_key_cloudwatch_arn" { 2 | description = "Cloudwatch KMS key ARN" 3 | type = string 4 | } 5 | 6 | variable "kms_key_cloudwatch_us_east_arn" { 7 | description = "Cloudwatch KMS key ARN (US East)" 8 | type = string 9 | } -------------------------------------------------------------------------------- /aws/sns/outputs.tf: -------------------------------------------------------------------------------- 1 | output "sns_topic_alert_critical_arn" { 2 | description = "SNS topic ARN for critical alerts" 3 | value = aws_sns_topic.alert_critical.arn 4 | } 5 | 6 | output "sns_topic_alert_warning_arn" { 7 | description = "SNS topic ARN for warning alerts" 8 | value = aws_sns_topic.alert_warning.arn 9 | } 10 | 11 | output "sns_topic_alert_ok_arn" { 12 | description = "SNS topic ARN for ok alerts" 13 | value = aws_sns_topic.alert_ok.arn 14 | } 15 | 16 | output "sns_topic_alert_warning_us_east_arn" { 17 | description = "SNS topic ARN for warning alerts (US East)" 18 | value = aws_sns_topic.alert_warning_us_east.arn 19 | } 20 | 21 | output "sns_topic_alert_ok_us_east_arn" { 22 | description = "SNS topic ARN for ok alerts (US East)" 23 | value = aws_sns_topic.alert_ok_us_east.arn 24 | } -------------------------------------------------------------------------------- /aws/sns/sns.tf: -------------------------------------------------------------------------------- 1 | # 2 | # SNS topics 3 | # 4 | resource "aws_sns_topic" "alert_critical" { 5 | name = "alert-critical" 6 | kms_master_key_id = var.kms_key_cloudwatch_arn 7 | 8 | 9 | } 10 | 11 | resource "aws_sns_topic" "alert_warning" { 12 | name = "alert-warning" 13 | kms_master_key_id = var.kms_key_cloudwatch_arn 14 | 15 | 16 | } 17 | 18 | resource "aws_sns_topic" "alert_ok" { 19 | name = "alert-ok" 20 | kms_master_key_id = var.kms_key_cloudwatch_arn 21 | 22 | } 23 | 24 | resource "aws_sns_topic" "alert_warning_us_east" { 25 | provider = aws.us-east-1 26 | 27 | name = "alert-warning" 28 | kms_master_key_id = var.kms_key_cloudwatch_us_east_arn 29 | 30 | } 31 | 32 | resource "aws_sns_topic" "alert_ok_us_east" { 33 | provider = aws.us-east-1 34 | 35 | name = "alert-ok" 36 | kms_master_key_id = var.kms_key_cloudwatch_us_east_arn 37 | 38 | } -------------------------------------------------------------------------------- /aws/sqs/outputs.tf: -------------------------------------------------------------------------------- 1 | output "sqs_reliability_queue_id" { 2 | description = "SQS reliability queue ID" 3 | value = aws_sqs_queue.reliability_queue.id 4 | } 5 | 6 | output "sqs_reliability_dead_letter_queue_id" { 7 | description = "SQS Reliability dead letter queue URL" 8 | value = aws_sqs_queue.reliability_deadletter_queue.id 9 | } 10 | 11 | output "sqs_reliability_queue_arn" { 12 | description = "SQS reliability queue ARN" 13 | value = aws_sqs_queue.reliability_queue.arn 14 | } 15 | 16 | output "sqs_reprocess_submission_queue_arn" { 17 | description = "SQS reprocess submission queue ARN" 18 | value = aws_sqs_queue.reprocess_submission_queue.arn 19 | } 20 | 21 | output "sqs_reprocess_submission_queue_id" { 22 | description = "SQS reprocess submission queue URL" 23 | value = aws_sqs_queue.reprocess_submission_queue.id 24 | } 25 | 26 | output "sqs_reliability_deadletter_queue_arn" { 27 | description = "Reliability queue's dead-letter queue ARN" 28 | value = aws_sqs_queue.reliability_deadletter_queue.name 29 | } 30 | 31 | output "sqs_app_audit_log_queue_arn" { 32 | description = "SQS audit log queue ARN" 33 | value = aws_sqs_queue.audit_log_queue.arn 34 | } 35 | 36 | output "sqs_app_audit_log_queue_id" { 37 | description = "SQS audit log queue URL" 38 | value = aws_sqs_queue.audit_log_queue.id 39 | } 40 | 41 | output "sqs_api_audit_log_queue_arn" { 42 | description = "SQS API audit log queue ARN" 43 | value = aws_sqs_queue.api_audit_log_queue.arn 44 | } 45 | 46 | output "sqs_api_audit_log_queue_id" { 47 | description = "SQS API audit log queue URL" 48 | value = aws_sqs_queue.api_audit_log_queue.id 49 | } 50 | 51 | output "sqs_app_audit_log_deadletter_queue_arn" { 52 | description = "Audit Log queues dead-letter queue ARN" 53 | value = aws_sqs_queue.audit_log_deadletter_queue.arn 54 | } 55 | 56 | output "sqs_api_audit_log_deadletter_queue_arn" { 57 | description = "API Audit Log queues dead-letter queue ARN" 58 | value = aws_sqs_queue.api_audit_log_deadletter_queue.arn 59 | } 60 | -------------------------------------------------------------------------------- /aws/vpn/certificate.tf: -------------------------------------------------------------------------------- 1 | resource "aws_acm_certificate" "vpn" { 2 | private_key = file("./certificates/aws-development-server.key") 3 | certificate_body = file("./certificates/aws-development-server.crt") 4 | certificate_chain = file("./certificates/ca.crt") 5 | 6 | lifecycle { 7 | create_before_destroy = true 8 | } 9 | } -------------------------------------------------------------------------------- /aws/vpn/event.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_event_rule" "vpn_lambda_trigger" { 2 | name = "vpn-lambda-trigger" 3 | description = "Fires every 2 hours" 4 | schedule_expression = "rate(2 hours)" # Every 2 hours 5 | } 6 | 7 | resource "aws_cloudwatch_event_target" "vpn_lambda_trigger" { 8 | rule = aws_cloudwatch_event_rule.vpn_lambda_trigger.name 9 | arn = aws_lambda_function.vpn_lambda.arn 10 | } 11 | 12 | -------------------------------------------------------------------------------- /aws/vpn/inputs.tf: -------------------------------------------------------------------------------- 1 | variable "vpc_cidr_block" { 2 | description = "IP CIDR block of the VPC" 3 | type = string 4 | } 5 | 6 | variable "private_subnet_ids" { 7 | description = "The list of private subnet IDs to attach the RDS cluster to" 8 | type = list(string) 9 | } 10 | 11 | variable "ecs_security_group_id" { 12 | description = "The security group used by ECS" 13 | type = string 14 | } 15 | 16 | variable "vpc_id" { 17 | description = "The VPC ID" 18 | type = string 19 | } -------------------------------------------------------------------------------- /aws/vpn/lambda/code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vpn-scheduler", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rm -rf dist", 9 | "build": "tsc --project tsconfig.json", 10 | "build:dev": "tsc --project tsconfig.json --watch", 11 | "postbuild": "cp package.json dist/package.json && cp yarn.lock dist/yarn.lock && cd ./dist && yarn install --production", 12 | "start": "node dist/main.js" 13 | }, 14 | "devDependencies": { 15 | "@types/aws-lambda": "^8.10.126", 16 | "@types/node": "^20.9.0", 17 | "typescript": "^5.2.2" 18 | }, 19 | "dependencies": { 20 | "@aws-sdk/client-ec2": "^3.750.0" 21 | }, 22 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 23 | } 24 | -------------------------------------------------------------------------------- /aws/vpn/lambda/code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2023"], 4 | "module": "node16", 5 | "target": "es2022", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "moduleResolution": "node16", 11 | "typeRoots": ["./types", "./node_modules/@types"], 12 | "preserveConstEnums": true, 13 | "sourceMap": false, 14 | "isolatedModules": true, 15 | "outDir": "./dist", 16 | "baseUrl": "." 17 | }, 18 | "inlineSource": true, 19 | "sourceMap": true, 20 | "include": ["./**/*.ts"], 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /aws/vpn/vpn.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ec2_client_vpn_endpoint" "development" { 2 | description = "development-environment-connection" 3 | server_certificate_arn = aws_acm_certificate.vpn.arn 4 | client_cidr_block = cidrsubnet(var.vpc_cidr_block, 4, 7) 5 | split_tunnel = true 6 | vpc_id = var.vpc_id 7 | security_group_ids = [var.ecs_security_group_id] 8 | 9 | 10 | authentication_options { 11 | type = "certificate-authentication" 12 | root_certificate_chain_arn = aws_acm_certificate.vpn.arn 13 | } 14 | 15 | connection_log_options { 16 | enabled = false 17 | } 18 | } 19 | 20 | resource "aws_ec2_client_vpn_network_association" "development" { 21 | client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.development.id 22 | subnet_id = var.private_subnet_ids[0] 23 | } 24 | 25 | resource "aws_ec2_client_vpn_authorization_rule" "example" { 26 | client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.development.id 27 | target_network_cidr = var.vpc_cidr_block 28 | authorize_all_groups = true 29 | } -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: forms-terraform 5 | description: Infrastructure as Code for the GC Forms environment 6 | labels: 7 | license: MIT 8 | spec: 9 | type: website 10 | lifecycle: experimental 11 | owner: group:cds-snc/forms-dev-admin 12 | -------------------------------------------------------------------------------- /env/cloud/cognito/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//cognito" 3 | } 4 | 5 | dependencies { 6 | paths = ["../kms", "../secrets", "../ecr"] 7 | } 8 | 9 | dependency "kms" { 10 | config_path = "../kms" 11 | 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs_merge_strategy_with_state = "shallow" 14 | mock_outputs = { 15 | kms_key_cloudwatch_arn = null 16 | } 17 | } 18 | 19 | locals { 20 | aws_account_id = get_env("AWS_ACCOUNT_ID", "000000000000") 21 | } 22 | 23 | dependency "secrets" { 24 | config_path = "../secrets" 25 | mock_outputs_merge_strategy_with_state = "shallow" 26 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 27 | mock_outputs = { 28 | notify_api_key_secret_arn = "arn:aws:secretsmanager:ca-central-1:${local.aws_account_id}:secret:notify_api_key" 29 | } 30 | } 31 | 32 | dependency "ecr" { 33 | config_path = "../ecr" 34 | mock_outputs_merge_strategy_with_state = "shallow" 35 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 36 | mock_outputs = { 37 | ecr_repository_url_cognito_email_sender_lambda = "" 38 | ecr_repository_url_cognito_pre_sign_up_lambda = "" 39 | } 40 | } 41 | 42 | inputs = { 43 | kms_key_cloudwatch_arn = dependency.kms.outputs.kms_key_cloudwatch_arn 44 | 45 | notify_api_key_secret_arn = dependency.secrets.outputs.notify_api_key_secret_arn 46 | 47 | ecr_repository_url_cognito_email_sender_lambda = dependency.ecr.outputs.ecr_repository_url_cognito_email_sender_lambda 48 | ecr_repository_url_cognito_pre_sign_up_lambda = dependency.ecr.outputs.ecr_repository_url_cognito_pre_sign_up_lambda 49 | } 50 | 51 | include "root" { 52 | path = find_in_parent_folders("root.hcl") 53 | } -------------------------------------------------------------------------------- /env/cloud/dynamodb/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//dynamodb" 3 | } 4 | 5 | dependencies { 6 | paths = ["../kms"] 7 | } 8 | 9 | dependency "kms" { 10 | config_path = "../kms" 11 | mock_outputs_merge_strategy_with_state = "shallow" 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs = { 14 | kms_key_dynamodb_arn = null 15 | } 16 | } 17 | 18 | inputs = { 19 | kms_key_dynamodb_arn = dependency.kms.outputs.kms_key_dynamodb_arn 20 | } 21 | 22 | include "root" { 23 | path = find_in_parent_folders("root.hcl") 24 | } 25 | -------------------------------------------------------------------------------- /env/cloud/ecr/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//ecr" 3 | } 4 | 5 | include "root" { 6 | path = find_in_parent_folders("root.hcl") 7 | } 8 | 9 | locals { 10 | cds_org_id = get_env("CDS_ORG_ID", "o-1234567890") 11 | staging_account_id = get_env("STAGING_AWS_ACCOUNT_ID", "123456789012") 12 | 13 | } 14 | 15 | inputs = { 16 | cds_org_id = local.cds_org_id 17 | staging_account_id = local.staging_account_id 18 | aws_development_accounts = ["123456789012"] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /env/cloud/file_scanning/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//file_scanning" 3 | } 4 | 5 | dependencies { 6 | paths = ["../s3"] 7 | } 8 | 9 | dependency "s3" { 10 | config_path = "../s3" 11 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 12 | mock_outputs_merge_strategy_with_state = "shallow" 13 | mock_outputs = { 14 | vault_file_storage_id = "forms-staging-vault-file-storage" 15 | } 16 | } 17 | 18 | inputs = { 19 | vault_file_storage_id = dependency.s3.outputs.vault_file_storage_id 20 | } 21 | 22 | include "root" { 23 | path = find_in_parent_folders("root.hcl") 24 | } -------------------------------------------------------------------------------- /env/cloud/hosted_zone/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//hosted_zone" 3 | } 4 | 5 | include "root" { 6 | path = find_in_parent_folders("root.hcl") 7 | } 8 | -------------------------------------------------------------------------------- /env/cloud/kms/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//kms" 3 | } 4 | 5 | include "root" { 6 | path = find_in_parent_folders("root.hcl") 7 | } 8 | -------------------------------------------------------------------------------- /env/cloud/load_balancer/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//load_balancer" 3 | } 4 | 5 | dependencies { 6 | paths = ["../hosted_zone", "../network"] 7 | } 8 | 9 | locals { 10 | domain = jsondecode(get_env("APP_DOMAINS", "[\"localhost:3000\"]")) 11 | } 12 | 13 | 14 | dependency "hosted_zone" { 15 | config_path = "../hosted_zone" 16 | mock_outputs_merge_strategy_with_state = "shallow" 17 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 18 | mock_outputs = { 19 | hosted_zone_ids = formatlist("mocked_zone_id_%s", local.domain) 20 | } 21 | } 22 | 23 | dependency "network" { 24 | config_path = "../network" 25 | mock_outputs_merge_strategy_with_state = "shallow" 26 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 27 | mock_outputs = { 28 | alb_security_group_id = null 29 | public_subnet_ids = [""] 30 | vpc_id = null 31 | } 32 | } 33 | 34 | inputs = { 35 | hosted_zone_ids = dependency.hosted_zone.outputs.hosted_zone_ids 36 | alb_security_group_id = dependency.network.outputs.alb_security_group_id 37 | public_subnet_ids = dependency.network.outputs.public_subnet_ids 38 | vpc_id = dependency.network.outputs.vpc_id 39 | } 40 | 41 | include "root" { 42 | path = find_in_parent_folders("root.hcl") 43 | } 44 | -------------------------------------------------------------------------------- /env/cloud/load_testing/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//load_testing" 3 | } 4 | 5 | dependencies { 6 | paths = ["../ecr", "../lambdas"] 7 | } 8 | 9 | dependency "ecr" { 10 | config_path = "../ecr" 11 | 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs_merge_strategy_with_state = "shallow" 14 | mock_outputs = { 15 | ecr_repository_url_load_testing_lambda = "" 16 | } 17 | } 18 | 19 | dependency "lambdas" { 20 | config_path = "../lambdas" 21 | 22 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 23 | mock_outputs_merge_strategy_with_state = "shallow" 24 | mock_outputs = { 25 | lambda_submission_function_name = "Submission" 26 | } 27 | } 28 | 29 | inputs = { 30 | ecr_repository_url_load_testing_lambda = dependency.ecr.outputs.ecr_repository_url_load_testing_lambda 31 | lambda_submission_function_name = dependency.lambdas.outputs.lambda_submission_function_name 32 | } 33 | 34 | include "root" { 35 | path = find_in_parent_folders("root.hcl") 36 | } 37 | -------------------------------------------------------------------------------- /env/cloud/network/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//network" 3 | } 4 | 5 | locals { 6 | env = get_env("APP_ENV", "development") 7 | } 8 | 9 | 10 | inputs = { 11 | vpc_cidr_block = "172.16.0.0/16" 12 | vpc_name = "forms" 13 | } 14 | 15 | include "root" { 16 | path = find_in_parent_folders("root.hcl") 17 | } 18 | 19 | generate "network" { 20 | path = "network.tf" 21 | if_exists = "overwrite" 22 | contents = local.env == "development" ? file("../../../aws/network/development_env/network.tf") : file("../../../aws/network/network.tf") 23 | } 24 | 25 | generate "vpc_endpoints" { 26 | path = "vpc_endpoints.tf" 27 | if_exists = "overwrite" 28 | contents = local.env == "development" ? file("../../../aws/network/development_env/vpc_endpoints.tf") : file("../../../aws/network/vpc_endpoints.tf") 29 | } 30 | -------------------------------------------------------------------------------- /env/cloud/oidc_roles/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//oidc_roles" 3 | } 4 | 5 | include "root" { 6 | path = find_in_parent_folders("root.hcl") 7 | } -------------------------------------------------------------------------------- /env/cloud/pr_review/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//pr_review" 3 | } 4 | 5 | dependencies { 6 | paths = ["../network", "../lambdas"] 7 | } 8 | 9 | dependency "lambdas" { 10 | config_path = "../lambdas" 11 | 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs_merge_with_state = true 14 | mock_outputs_merge_strategy_with_state = "shallow" 15 | mock_outputs = { 16 | forms_lambda_client_iam_role_name = "forms-lambda-client" 17 | } 18 | } 19 | 20 | dependency "network" { 21 | config_path = "../network" 22 | 23 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 24 | mock_outputs_merge_with_state = true 25 | mock_outputs_merge_strategy_with_state = "shallow" 26 | mock_outputs = { 27 | vpc_id = null 28 | privatelink_security_group_id = "sg-1234567890" 29 | rds_security_group_id = "sg-1234567890" 30 | redis_security_group_id = "sg-1234567890" 31 | idp_ecs_security_group_id = "sg-ecs" 32 | } 33 | } 34 | 35 | inputs = { 36 | forms_lambda_client_iam_role_name = dependency.lambdas.outputs.forms_lambda_client_iam_role_name 37 | 38 | vpc_id = dependency.network.outputs.vpc_id 39 | privatelink_security_group_id = dependency.network.outputs.privatelink_security_group_id 40 | forms_database_security_group_id = dependency.network.outputs.rds_security_group_id 41 | forms_redis_security_group_id = dependency.network.outputs.redis_security_group_id 42 | idp_ecs_security_group_id = dependency.network.outputs.idp_ecs_security_group_id 43 | } 44 | 45 | include "root" { 46 | path = find_in_parent_folders("root.hcl") 47 | } -------------------------------------------------------------------------------- /env/cloud/rds/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//rds" 3 | } 4 | 5 | dependencies { 6 | paths = ["../network"] 7 | } 8 | 9 | dependency "network" { 10 | config_path = "../network" 11 | mock_outputs_merge_strategy_with_state = "shallow" 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs = { 14 | private_subnet_ids = [""] 15 | rds_security_group_id = null 16 | } 17 | } 18 | 19 | locals { 20 | env = get_env("APP_ENV", "development") 21 | db_pw = "chummy000" 22 | } 23 | 24 | inputs = { 25 | private_subnet_ids = dependency.network.outputs.private_subnet_ids 26 | rds_security_group_id = dependency.network.outputs.rds_security_group_id 27 | 28 | rds_connector_db_user = local.env == "development" ? "postgres" : "rds_connector_read" 29 | rds_db_user = "postgres" 30 | rds_db_name = "forms" 31 | rds_name = local.env == "staging" ? "forms-staging-db" : "forms-db" 32 | rds_db_subnet_group_name = local.env == "staging" ? "forms-staging-db" : "forms-db" 33 | 34 | # Overwritten in GitHub Actions by TFVARS 35 | rds_db_password = local.db_pw # RDS database password used for local setup 36 | rds_connector_db_password = local.db_pw 37 | 38 | } 39 | 40 | include "root" { 41 | path = find_in_parent_folders("root.hcl") 42 | } 43 | -------------------------------------------------------------------------------- /env/cloud/redis/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//redis" 3 | } 4 | 5 | dependencies { 6 | paths = ["../network"] 7 | } 8 | 9 | dependency "network" { 10 | config_path = "../network" 11 | mock_outputs_merge_strategy_with_state = "shallow" 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs = { 14 | private_subnet_ids = ["prv-1", "prv-2"] 15 | redis_security_group_id = "sg-1234567890" 16 | } 17 | } 18 | 19 | inputs = { 20 | private_subnet_ids = dependency.network.outputs.private_subnet_ids 21 | redis_security_group_id = dependency.network.outputs.redis_security_group_id 22 | } 23 | 24 | include "root" { 25 | path = find_in_parent_folders("root.hcl") 26 | } -------------------------------------------------------------------------------- /env/cloud/s3/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//s3" 3 | } 4 | 5 | include "root" { 6 | path = find_in_parent_folders("root.hcl") 7 | } 8 | -------------------------------------------------------------------------------- /env/cloud/secrets/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//secrets" 3 | } 4 | 5 | include "root" { 6 | path = find_in_parent_folders("root.hcl") 7 | } 8 | 9 | locals { 10 | ecs_secret_token = get_env("ECS_SECRET_TOKEN", "I_am_not_a_secret_token") 11 | recaptcha_secret = get_env("RECAPTCHA_SECRET", "I_am_not_a_secret_token") 12 | notify_callback_bearer_token = get_env("NOTIFY_CALLBACK_BEARER_TOKEN", "I_am_not_a_secret_token") 13 | notify_api_key = get_env("NOTIFY_API_KEY", "I_am_not_a_secret_token") 14 | freshdesk_api_key = get_env("FRESHDESK_API_KEY", "I_am_not_a_secret_token") 15 | sentry_api_key = get_env("SENTRY_API_KEY", "I_am_not_a_secret_token") 16 | zitadel_administration_key = get_env("ZITADEL_ADMINISTRATION_KEY", "I_am_not_a_secret_token") 17 | zitadel_application_key = get_env("ZITADEL_APPLICATION_KEY", "I_am_not_a_secret_token") 18 | rds_db_password = "chummy" 19 | hcaptcha_site_verify_key = get_env("HCAPTCHA_SITE_VERIFY_KEY", "I_am_not_a_secret_token") 20 | } 21 | 22 | inputs = { 23 | ecs_secret_token = local.ecs_secret_token 24 | recaptcha_secret = local.recaptcha_secret 25 | notify_callback_bearer_token = local.notify_callback_bearer_token 26 | notify_api_key = local.notify_api_key 27 | freshdesk_api_key = local.freshdesk_api_key 28 | zitadel_administration_key = local.zitadel_administration_key 29 | zitadel_application_key = local.zitadel_application_key 30 | hcaptcha_site_verify_key = local.hcaptcha_site_verify_key 31 | # Overwritten in GitHub Actions by TFVARS 32 | rds_db_password = local.rds_db_password 33 | sentry_api_key = local.sentry_api_key 34 | } -------------------------------------------------------------------------------- /env/cloud/sns/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//sns" 3 | } 4 | 5 | dependencies { 6 | paths = ["../kms"] 7 | } 8 | 9 | dependency "kms" { 10 | config_path = "../kms" 11 | 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs_merge_strategy_with_state = "shallow" 14 | mock_outputs = { 15 | kms_key_cloudwatch_arn = null 16 | kms_key_cloudwatch_us_east_arn = null 17 | } 18 | } 19 | 20 | inputs = { 21 | kms_key_cloudwatch_arn = dependency.kms.outputs.kms_key_cloudwatch_arn 22 | kms_key_cloudwatch_us_east_arn = dependency.kms.outputs.kms_key_cloudwatch_us_east_arn 23 | } 24 | 25 | include "root" { 26 | path = find_in_parent_folders("root.hcl") 27 | } -------------------------------------------------------------------------------- /env/cloud/sqs/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//sqs" 3 | } 4 | 5 | include "root" { 6 | path = find_in_parent_folders("root.hcl") 7 | } 8 | -------------------------------------------------------------------------------- /env/cloud/vpn/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../../aws//vpn" 3 | } 4 | 5 | dependencies { 6 | paths = ["../network"] 7 | } 8 | 9 | dependency "network" { 10 | config_path = "../network" 11 | mock_outputs_merge_strategy_with_state = "shallow" 12 | mock_outputs_allowed_terraform_commands = ["init", "fmt", "validate", "plan", "show"] 13 | mock_outputs = { 14 | private_subnet_ids = [""] 15 | ecs_security_group_id = null 16 | vpc_id = null 17 | } 18 | } 19 | 20 | inputs = { 21 | private_subnet_ids = dependency.network.outputs.private_subnet_ids 22 | ecs_security_group_id = dependency.network.outputs.ecs_security_group_id 23 | vpc_id = dependency.network.outputs.vpc_id 24 | # non dynamic 25 | vpc_cidr_block = "172.16.0.0/16" 26 | } 27 | 28 | include "root" { 29 | path = find_in_parent_folders("root.hcl") 30 | } -------------------------------------------------------------------------------- /env/common/common_variables.tf: -------------------------------------------------------------------------------- 1 | variable "account_id" { 2 | description = "The account ID to create resources in" 3 | type = string 4 | } 5 | 6 | variable "cbs_satellite_bucket_name" { 7 | description = "(Required) Name of the Cloud Based Sensor S3 satellite bucket" 8 | type = string 9 | } 10 | 11 | variable "billing_tag_key" { 12 | description = "The default tagging key" 13 | type = string 14 | } 15 | 16 | variable "billing_tag_value" { 17 | description = "The default tagging value" 18 | type = string 19 | } 20 | 21 | variable "domain_api" { 22 | description = "The API's domain" 23 | type = string 24 | } 25 | 26 | variable "domain_idp" { 27 | description = "The identity provider domain" 28 | type = string 29 | } 30 | 31 | variable "domains" { 32 | description = "The application domains" 33 | type = list(string) 34 | } 35 | 36 | variable "env" { 37 | description = "The current running environment" 38 | type = string 39 | } 40 | 41 | variable "region" { 42 | description = "The current AWS region" 43 | type = string 44 | } 45 | -------------------------------------------------------------------------------- /env/common/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "1.11.2" 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "5.84.0" 7 | } 8 | random = { 9 | source = "hashicorp/random" 10 | version = "=3.6.0" 11 | } 12 | } 13 | } 14 | 15 | provider "aws" { 16 | region = var.region 17 | allowed_account_ids = [var.account_id] 18 | default_tags { 19 | tags = { 20 | (var.billing_tag_key) = var.billing_tag_value 21 | Terraform = true 22 | } 23 | } 24 | } 25 | 26 | provider "aws" { 27 | alias = "us-east-1" 28 | region = "us-east-1" 29 | allowed_account_ids = [var.account_id] 30 | default_tags { 31 | tags = { 32 | (var.billing_tag_key) = var.billing_tag_value 33 | Terraform = true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /env/root.hcl: -------------------------------------------------------------------------------- 1 | locals { 2 | account_id = get_env("AWS_ACCOUNT_ID", "000000000000") 3 | env = get_env("APP_ENV", "development") 4 | domain_api = get_env("API_DOMAIN", "localhost:3001") 5 | domain_idp = get_env("IDP_DOMAIN", "localhost:8080") 6 | domains = get_env("APP_DOMAINS", "[\"localhost:3000\"]") 7 | } 8 | 9 | inputs = { 10 | account_id = "${local.account_id}" 11 | billing_tag_key = "CostCentre" 12 | billing_tag_value = "forms-platform-${local.env}" 13 | domain_api = local.domain_api 14 | domain_idp = local.domain_idp 15 | domains = local.domains 16 | env = "${local.env}" 17 | region = "ca-central-1" 18 | cbs_satellite_bucket_name = "cbs-satellite-${local.account_id}" 19 | } 20 | 21 | generate "backend_remote_state" { 22 | path = "backend.tf" 23 | if_exists = "overwrite" 24 | contents = < { 14 | return new Promise((resolve) => setTimeout(resolve, durationInSeconds * 1000)); 15 | } 16 | -------------------------------------------------------------------------------- /lambda-code/api-end-to-end-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from "aws-lambda"; 2 | import { IdpClient } from "@lib/idpClient.js"; 3 | import { ApiClient } from "@lib/apiClient.js"; 4 | import { SubmissionGenerator } from "@lib/submissionGenerator.js"; 5 | import { CommonUseCaseTest } from "@tests/commonUseCaseTest.js"; 6 | import { RateLimiterTest } from "@tests/rateLimiterTest.js"; 7 | 8 | const IDP_TRUSTED_DOMAIN: string = process.env.IDP_TRUSTED_DOMAIN ?? ""; 9 | const IDP_URL: string = process.env.IDP_URL ?? ""; 10 | const IDP_PROJECT_IDENTIFIER: string = process.env.IDP_PROJECT_IDENTIFIER ?? ""; 11 | const API_URL: string = process.env.API_URL ?? ""; 12 | const FORM_ID: string = process.env.FORM_ID ?? ""; 13 | const API_PRIVATE_KEY: string = process.env.API_PRIVATE_KEY ?? ""; 14 | 15 | export const handler: Handler = async () => { 16 | try { 17 | const submissionGenerator = new SubmissionGenerator(FORM_ID); 18 | 19 | const privateApiKey = JSON.parse(API_PRIVATE_KEY) as PrivateApiKey; 20 | 21 | const idpClient = new IdpClient( 22 | IDP_TRUSTED_DOMAIN, 23 | IDP_URL, 24 | IDP_PROJECT_IDENTIFIER, 25 | privateApiKey 26 | ); 27 | 28 | const apiAccessToken = await idpClient.generateAccessToken(); 29 | 30 | const apiClient = new ApiClient(FORM_ID, API_URL, apiAccessToken); 31 | 32 | const tests: ApiTest[] = [ 33 | new CommonUseCaseTest(submissionGenerator, apiClient, privateApiKey), 34 | new RateLimiterTest(apiClient), 35 | ]; 36 | 37 | for (const test of tests) { 38 | console.info(`>>> Running ${test.name}...`); 39 | 40 | await test.run(); 41 | 42 | console.info(`<<< ${test.name} completed!`); 43 | } 44 | } catch (error) { 45 | console.error( 46 | JSON.stringify({ 47 | level: "error", 48 | msg: "Failed to run API end to end test", 49 | error: (error as Error).message, 50 | }) 51 | ); 52 | 53 | throw error; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /lambda-code/api-end-to-end-test/src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | type PrivateApiKey = { 2 | keyId: string; 3 | key: string; 4 | userId: string; 5 | formId: string; 6 | }; 7 | 8 | type NewFormSubmission = { 9 | name: string; 10 | createdAt: number; 11 | }; 12 | 13 | type EncryptedFormSubmission = { 14 | encryptedResponses: string; 15 | encryptedKey: string; 16 | encryptedNonce: string; 17 | encryptedAuthTag: string; 18 | }; 19 | 20 | type FormSubmission = { 21 | confirmationCode: string; 22 | answers: string; 23 | checksum: string; 24 | }; 25 | 26 | type RateLimitStatus = { 27 | limit: number; 28 | remanining: number; 29 | reset: Date; 30 | retryAfter?: number; 31 | }; 32 | 33 | type ApiResponse = { 34 | status: number; 35 | rateLimitStatus: RateLimitStatus; 36 | payload: T; 37 | }; 38 | -------------------------------------------------------------------------------- /lambda-code/api-end-to-end-test/src/types/test.d.ts: -------------------------------------------------------------------------------- 1 | interface ApiTest { 2 | name: string; 3 | run(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /lambda-code/api-end-to-end-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@lib/*": ["src/lib/*"], 11 | "@tests/*": ["src/tests/*"] 12 | }, 13 | "outDir": "build" 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /lambda-code/audit-logs-archiver/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/audit-logs-archiver/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/audit-logs-archiver/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/audit-logs-archiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/audit-logs-archiver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audit-logs-archiver", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-dynamodb": "3.799.0", 13 | "@aws-sdk/client-s3": "3.799.0", 14 | "@aws-sdk/lib-dynamodb": "3.799.0" 15 | }, 16 | "devDependencies": { 17 | "@types/aws-lambda": "^8.10.149", 18 | "@types/node": "^22.15.3", 19 | "tsc-alias": "^1.8.15", 20 | "typescript": "^5.8.3" 21 | }, 22 | "packageManager": "yarn@4.9.1" 23 | } 24 | -------------------------------------------------------------------------------- /lambda-code/audit-logs-archiver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "outDir": "build" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /lambda-code/audit-logs/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/audit-logs/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/audit-logs/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/audit-logs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/audit-logs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audit-logs", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-dynamodb": "3.799.0", 13 | "@aws-sdk/lib-dynamodb": "3.799.0" 14 | }, 15 | "devDependencies": { 16 | "@types/aws-lambda": "^8.10.149", 17 | "@types/node": "^22.15.3", 18 | "tsc-alias": "^1.8.15", 19 | "typescript": "^5.8.3" 20 | }, 21 | "packageManager": "yarn@4.9.1" 22 | } 23 | -------------------------------------------------------------------------------- /lambda-code/audit-logs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "outDir": "build" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /lambda-code/cognito-email-sender/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/cognito-email-sender/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/cognito-email-sender/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/cognito-email-sender/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/cognito-email-sender/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cognito-email-sender", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-crypto/client-node": "4.2.1", 13 | "@gcforms/connectors": "^2.0.2" 14 | }, 15 | "devDependencies": { 16 | "@types/aws-lambda": "^8.10.149", 17 | "@types/node": "^22.15.3", 18 | "tsc-alias": "^1.8.15", 19 | "typescript": "^5.8.3" 20 | }, 21 | "packageManager": "yarn@4.9.1" 22 | } 23 | -------------------------------------------------------------------------------- /lambda-code/cognito-email-sender/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "outDir": "build" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /lambda-code/cognito-pre-sign-up/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/cognito-pre-sign-up/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/cognito-pre-sign-up/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/cognito-pre-sign-up/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/cognito-pre-sign-up/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cognito-pre-sign-up", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "devDependencies": { 12 | "@types/aws-lambda": "^8.10.149", 13 | "@types/node": "^22.15.3", 14 | "tsc-alias": "^1.8.15", 15 | "typescript": "^5.8.3" 16 | }, 17 | "packageManager": "yarn@4.9.1" 18 | } 19 | -------------------------------------------------------------------------------- /lambda-code/cognito-pre-sign-up/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, PreSignUpEmailTriggerEvent } from "aws-lambda"; 2 | 3 | export const handler: Handler = async (event: PreSignUpEmailTriggerEvent) => { 4 | // Confirm the user 5 | event.response.autoConfirmUser = true; 6 | 7 | // Set the email as verified if it is in the request 8 | if (event.request.userAttributes.hasOwnProperty("email")) { 9 | event.response.autoVerifyEmail = true; 10 | } 11 | 12 | return event; 13 | }; 14 | -------------------------------------------------------------------------------- /lambda-code/cognito-pre-sign-up/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "outDir": "build" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /lambda-code/deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Lambda helper script to install or delete the dependencies. 4 | # Use: deps.sh [install|delete] 5 | # 6 | set -euo pipefail 7 | IFS=$'\n\t' 8 | 9 | ACTION=$1 10 | SCRIPT_DIR="$(dirname "$(realpath "$0")")" 11 | 12 | # Loop over the Lambda directories 13 | for LAMBDA_DIR in "$SCRIPT_DIR"/*/; do 14 | if test -f "$LAMBDA_DIR/package.json"; then 15 | echo "⚡ $LAMBDA_DIR $ACTION" 16 | if [ "$ACTION" = "delete" ]; then 17 | rm -rf "$LAMBDA_DIR/node_modules" 18 | rm -rf "$LAMBDA_DIR/build" || true 19 | else 20 | (cd "$LAMBDA_DIR" && yarn install) 21 | if test -f "$LAMBDA_DIR/tsconfig.json"; then 22 | echo "⚡ $LAMBDA_DIR Building TypeScript" 23 | (cd "$LAMBDA_DIR" && yarn build) 24 | fi 25 | fi 26 | fi 27 | done 28 | -------------------------------------------------------------------------------- /lambda-code/form-archiver/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/form-archiver/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/form-archiver/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/form-archiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/form-archiver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "form-archiver", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-rds-data": "3.799.0" 13 | }, 14 | "devDependencies": { 15 | "@types/aws-lambda": "^8.10.149", 16 | "@types/node": "^22.15.3", 17 | "tsc-alias": "^1.8.15", 18 | "typescript": "^5.8.3" 19 | }, 20 | "packageManager": "yarn@4.9.1" 21 | } 22 | -------------------------------------------------------------------------------- /lambda-code/form-archiver/src/lib/templates.ts: -------------------------------------------------------------------------------- 1 | import { RDSDataClient, ExecuteStatementCommand } from "@aws-sdk/client-rds-data"; 2 | 3 | /** 4 | * Delete all form templates that have been marked as archived (has an TTL value that is not null) 5 | */ 6 | export const deleteFormTemplatesMarkedAsArchived = async () => { 7 | try { 8 | const rdsDataClient = new RDSDataClient({ region: process.env.REGION }); 9 | 10 | const executeStatementCommand = new ExecuteStatementCommand({ 11 | database: process.env.DB_NAME, 12 | resourceArn: process.env.DB_ARN, 13 | secretArn: process.env.DB_SECRET, 14 | sql: `DELETE FROM "Template" WHERE ttl IS NOT NULL AND ttl < CURRENT_TIMESTAMP`, 15 | includeResultMetadata: false, // set to true if we want metadata like column names 16 | }); 17 | 18 | await rdsDataClient.send(executeStatementCommand); 19 | } catch (error) { 20 | // Warn Message will be sent to slack 21 | console.warn( 22 | JSON.stringify({ 23 | level: "warn", 24 | msg: `Failed to delete form templates marked as archived.`, 25 | error: (error as Error).message, 26 | }) 27 | ); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lambda-code/form-archiver/src/main.ts: -------------------------------------------------------------------------------- 1 | import { deleteFormTemplatesMarkedAsArchived } from "@lib/templates.js"; 2 | import { Handler } from "aws-lambda"; 3 | 4 | export const handler: Handler = async () => { 5 | try { 6 | await deleteFormTemplatesMarkedAsArchived(); 7 | 8 | console.log( 9 | JSON.stringify({ 10 | level: "info", 11 | status: "success", 12 | msg: "Form Archiver ran successfully.", 13 | }) 14 | ); 15 | 16 | return { 17 | statusCode: "SUCCESS", 18 | }; 19 | } catch (error) { 20 | // Error Message will be sent to slack 21 | console.error( 22 | JSON.stringify({ 23 | level: "error", 24 | status: "failed", 25 | msg: "Failed to run Form Templates Archiver.", 26 | error: (error as Error).message, 27 | }) 28 | ); 29 | 30 | return { 31 | statusCode: "ERROR", 32 | error: (error as Error).message, 33 | }; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lambda-code/form-archiver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@lib/*": ["src/lib/*"] 11 | }, 12 | "outDir": "build" 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /lambda-code/load-testing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazon/aws-lambda-python:3.12@sha256:37b95206c4c78331f6d5cb0e8389ef573f39cfea01f73c530f28f3ac6f6493c7 2 | 3 | COPY requirements.txt . 4 | RUN pip3 install -r requirements.txt 5 | 6 | COPY main.py . 7 | COPY tests ./tests 8 | 9 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/load-testing/Makefile: -------------------------------------------------------------------------------- 1 | fmt: 2 | black . 3 | 4 | install: 5 | pip3 install --user -r requirements.txt 6 | 7 | locust: 8 | locust -f tests/locust_test_file.py --host=https://forms-staging.cdssandbox.xyz 9 | 10 | .PHONY: \ 11 | fmt \ 12 | install \ 13 | locust 14 | -------------------------------------------------------------------------------- /lambda-code/load-testing/README.md: -------------------------------------------------------------------------------- 1 | # Load testing 2 | Locust load tests that can be run in a Lambda function or locally. 3 | 4 | ## Lambda 5 | Invoke the function using an event that looks like so: 6 | ```json 7 | { 8 | "locustfile": "./tests/locust_test_file.py", 9 | "host": "https://forms-staging.cdssandbox.xyz", 10 | "num_users": "5", 11 | "spawn_rate": "1", 12 | "run_time": "5m" 13 | } 14 | ``` 15 | 16 | ## Locally 17 | You will need AWS access credentials for the target environment, along with the following environment variables set: 18 | ```sh 19 | FORM_ID # Form ID to use for load testing 20 | FORM_PRIVATE_KEY # JSON private key for the form (must be from the `FORM_ID` form) 21 | ZITADEL_APP_PRIVATE_KEY # JSON private key for the Zitadel application that is used for access token introspection 22 | ``` 23 | Once the variables are set, you can start the tests like so: 24 | ```sh 25 | make install 26 | make locust 27 | ``` -------------------------------------------------------------------------------- /lambda-code/load-testing/main.py: -------------------------------------------------------------------------------- 1 | import invokust 2 | import logging 3 | import os 4 | import boto3 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | 8 | ssm_client = boto3.client("ssm") 9 | 10 | 11 | def get_ssm_parameters(client, parameter_names): 12 | response = client.get_parameters(Names=parameter_names, WithDecryption=True) 13 | return {param["Name"]: param["Value"] for param in response["Parameters"]} 14 | 15 | 16 | # Load required environment variables from AWS SSM 17 | params = get_ssm_parameters( 18 | ssm_client, 19 | [ 20 | "/load-testing/form-id", 21 | "/load-testing/form-private-key", 22 | "/load-testing/zitadel-app-private-key", 23 | ], 24 | ) 25 | os.environ["FORM_ID"] = params["/load-testing/form-id"] 26 | os.environ["FORM_PRIVATE_KEY"] = params["/load-testing/form-private-key"] 27 | os.environ["ZITADEL_APP_PRIVATE_KEY"] = params["/load-testing/zitadel-app-private-key"] 28 | 29 | def handler(event=None, context=None): 30 | 31 | # Check for required environment variables 32 | required_env_vars = [ 33 | "FORM_ID", 34 | "FORM_PRIVATE_KEY", 35 | "ZITADEL_APP_PRIVATE_KEY", 36 | ] 37 | for env_var in required_env_vars: 38 | if env_var not in os.environ: 39 | raise ValueError(f"Missing required environment variable: {env_var}") 40 | 41 | try: 42 | settings = ( 43 | invokust.create_settings(**event) 44 | if event 45 | else invokust.create_settings(from_environment=True) 46 | ) 47 | loadtest = invokust.LocustLoadTest(settings) 48 | loadtest.run() 49 | except Exception as e: 50 | logging.error("Exception running locust tests {0}".format(repr(e))) 51 | else: 52 | return loadtest.stats() 53 | -------------------------------------------------------------------------------- /lambda-code/load-testing/requirements.txt: -------------------------------------------------------------------------------- 1 | black==24.10.0 2 | boto3==1.35.39 3 | cryptography==43.0.1 4 | httpx==0.27.2 5 | invokust==0.77 6 | locust==2.31.7 7 | PyJWT==2.9.0 -------------------------------------------------------------------------------- /lambda-code/load-testing/tests/behaviours/idp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the IdP's access token generation and introspection endpoints. 3 | """ 4 | 5 | from locust import HttpUser, task 6 | from utils.jwt_generator import JwtGenerator 7 | from utils.task_set import SequentialTaskSetWithFailure 8 | 9 | 10 | class AccessTokenBehaviour(SequentialTaskSetWithFailure): 11 | def __init__(self, parent: HttpUser) -> None: 12 | super().__init__(parent) 13 | self.jwt_app = None 14 | self.jwt_form = None 15 | self.access_token = None 16 | 17 | def on_start(self) -> None: 18 | self.jwt_app = JwtGenerator.generate(self.idp_url, self.zitadel_app_private_key) 19 | self.jwt_form = JwtGenerator.generate(self.idp_url, self.form_private_key) 20 | 21 | @task 22 | def request_access_token(self) -> None: 23 | data = { 24 | "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", 25 | "assertion": self.jwt_form, 26 | "scope": f"openid profile urn:zitadel:iam:org:project:id:{self.idp_project_id}:aud", 27 | } 28 | response = self.request_with_failure_check( 29 | "post", f"{self.idp_url}/oauth/v2/token", 200, data=data 30 | ) 31 | self.access_token = response["access_token"] 32 | 33 | @task 34 | def introspect_access_token(self) -> None: 35 | data = { 36 | "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 37 | "client_assertion": self.jwt_app, 38 | "token": self.access_token, 39 | } 40 | self.request_with_failure_check( 41 | "post", f"{self.idp_url}/oauth/v2/introspect", 200, data=data 42 | ) 43 | -------------------------------------------------------------------------------- /lambda-code/load-testing/tests/locust_test_file.py: -------------------------------------------------------------------------------- 1 | from locust import HttpUser, between 2 | 3 | from behaviours.api import RetrieveResponseBehaviour 4 | from behaviours.idp import AccessTokenBehaviour 5 | from behaviours.submit import FormSubmitBehaviour 6 | 7 | 8 | class ApiUser(HttpUser): 9 | tasks = [RetrieveResponseBehaviour] 10 | wait_time = between(1, 5) 11 | weight = 6 12 | 13 | 14 | class FormSubmitUser(HttpUser): 15 | tasks = [FormSubmitBehaviour] 16 | wait_time = between(1, 5) 17 | weight = 2 18 | 19 | 20 | class IdpUser(HttpUser): 21 | tasks = [AccessTokenBehaviour] 22 | wait_time = between(1, 5) 23 | weight = 1 24 | -------------------------------------------------------------------------------- /lambda-code/load-testing/tests/utils/data_structures.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | 4 | 5 | @dataclass 6 | class PrivateApiKey: 7 | key_id: str 8 | key: str 9 | user_or_client_id: str 10 | 11 | @staticmethod 12 | def from_json(json_object: dict) -> "PrivateApiKey": 13 | return PrivateApiKey( 14 | key_id=json_object["keyId"], 15 | key=json_object["key"], 16 | user_or_client_id=json_object.get("userId") or json_object.get("clientId"), 17 | ) 18 | 19 | 20 | @dataclass 21 | class EncryptedFormSubmission: 22 | encrypted_responses: str 23 | encrypted_key: str 24 | encrypted_nonce: str 25 | encrypted_auth_tag: str 26 | 27 | @staticmethod 28 | def from_json(json_object: dict) -> "EncryptedFormSubmission": 29 | return EncryptedFormSubmission( 30 | encrypted_responses=json_object["encryptedResponses"], 31 | encrypted_key=json_object["encryptedKey"], 32 | encrypted_nonce=json_object["encryptedNonce"], 33 | encrypted_auth_tag=json_object["encryptedAuthTag"], 34 | ) 35 | -------------------------------------------------------------------------------- /lambda-code/load-testing/tests/utils/jwt_generator.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import time 3 | 4 | from dataclasses import dataclass 5 | from cryptography.hazmat.backends import default_backend 6 | from cryptography.hazmat.primitives import serialization 7 | from utils.data_structures import PrivateApiKey 8 | 9 | 10 | class JwtGenerator: 11 | 12 | @staticmethod 13 | def generate( 14 | identity_provider_url: str, 15 | private_api_key: PrivateApiKey, 16 | ) -> str: 17 | try: 18 | current_time = int(time.time()) 19 | private_key = serialization.load_pem_private_key( 20 | private_api_key.key.encode(), password=None, backend=default_backend() 21 | ) 22 | 23 | headers = {"kid": private_api_key.key_id, "alg": "RS256"} 24 | 25 | claims = { 26 | "iat": current_time, 27 | "iss": private_api_key.user_or_client_id, 28 | "sub": private_api_key.user_or_client_id, 29 | "aud": identity_provider_url, 30 | "exp": current_time + 3600, 31 | } 32 | 33 | jwt_signed_token = jwt.encode( 34 | claims, private_key, algorithm="RS256", headers=headers 35 | ) 36 | 37 | return jwt_signed_token 38 | 39 | except Exception as exception: 40 | raise Exception("Failed to generate signed JWT") from exception 41 | -------------------------------------------------------------------------------- /lambda-code/load-testing/tests/utils/task_set.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from urllib.parse import urlparse 5 | from locust import SequentialTaskSet 6 | from typing import Any, Dict 7 | 8 | from utils.data_structures import PrivateApiKey 9 | 10 | 11 | class SequentialTaskSetWithFailure(SequentialTaskSet): 12 | 13 | def __init__(self, parent) -> None: 14 | super().__init__(parent) 15 | parsed_url = urlparse(parent.host) 16 | self.api_url = f"{parsed_url.scheme}://api.{parsed_url.netloc}" 17 | self.idp_url = f"{parsed_url.scheme}://auth.{parsed_url.netloc}" 18 | self.idp_project_id = os.getenv("IDP_PROJECT_ID", "275372254274006635") 19 | self.form_private_key = PrivateApiKey.from_json( 20 | json.loads(os.getenv("FORM_PRIVATE_KEY").replace('\n', '\\n')) 21 | ) 22 | self.zitadel_app_private_key = PrivateApiKey.from_json( 23 | json.loads(os.getenv("ZITADEL_APP_PRIVATE_KEY").replace('\n', '\\n')) 24 | ) 25 | 26 | def request_with_failure_check( 27 | self, method: str, url: str, status_code: int, **kwargs: Dict[str, Any] 28 | ) -> dict: 29 | kwargs["catch_response"] = True 30 | with self.client.request(method, url, **kwargs) as response: 31 | if response.status_code != status_code: 32 | response.failure( 33 | f"Request failed: {response.status_code} {response.text}" 34 | ) 35 | raise ValueError( 36 | f"Request failed: {response.status_code} {response.text}" 37 | ) 38 | return ( 39 | response.json() 40 | if "application/json" in response.headers.get("Content-Type", "") 41 | else response.text 42 | ) 43 | -------------------------------------------------------------------------------- /lambda-code/nagware/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/nagware/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/nagware/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/nagware/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/nagware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nagware", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-dynamodb": "3.799.0", 13 | "@aws-sdk/lib-dynamodb": "3.799.0", 14 | "@gcforms/connectors": "^2.0.2", 15 | "redis": "^4.7.0" 16 | }, 17 | "devDependencies": { 18 | "@types/aws-lambda": "^8.10.149", 19 | "@types/node": "^22.15.3", 20 | "tsc-alias": "^1.8.15", 21 | "typescript": "^5.8.3" 22 | }, 23 | "packageManager": "yarn@4.9.1" 24 | } 25 | -------------------------------------------------------------------------------- /lambda-code/nagware/src/lib/overdueResponseCache.ts: -------------------------------------------------------------------------------- 1 | import { RedisConnector } from "./redisConnector.js"; 2 | 3 | /** 4 | * Handles caching of overdue form responses. The cache expiry should be at least 5 | * as long as the interval between Nagware function runs. 6 | * 7 | * This cache is used by the app to reduce the number of DynamoDB calls required 8 | * when loading the user's `/forms` page. 9 | */ 10 | 11 | const OVERDUE_CACHE_EXPIRE_SECONDS = 86400; // 1 day 12 | const OVERDUE_CACHE_KEY = "overdue:responses:template-ids"; 13 | 14 | export async function setOverdueResponseCache( 15 | formResponses: { formID: string; createdAt: number }[] 16 | ): Promise { 17 | const formIDs = formResponses.map((formResponse) => formResponse.formID); 18 | const redisConnector = await RedisConnector.getInstance(); 19 | await redisConnector.client.set(OVERDUE_CACHE_KEY, JSON.stringify(formIDs), { 20 | EX: OVERDUE_CACHE_EXPIRE_SECONDS, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /lambda-code/nagware/src/lib/redisConnector.ts: -------------------------------------------------------------------------------- 1 | import { type RedisClientType, createClient } from "redis"; 2 | 3 | const REDIS_URL = process.env.REDIS_URL ?? "redis://localhost:6379"; 4 | 5 | export class RedisConnector { 6 | private static instance: RedisConnector | undefined = undefined; 7 | private static RETRY_MAX = 10; 8 | private static RETRY_DELAY_STEP = 500; // milliseconds 9 | 10 | public client: RedisClientType; 11 | 12 | private constructor() { 13 | this.client = createClient({ 14 | url: REDIS_URL, 15 | socket: { 16 | // Reconnect strategy with exponential backoff 17 | reconnectStrategy: (retries: number): number | Error => 18 | retries < RedisConnector.RETRY_MAX 19 | ? RedisConnector.RETRY_DELAY_STEP * retries 20 | : new Error("Failed to connect to Redis"), 21 | }, 22 | }); 23 | this.client 24 | .on("error", (err: Error) => console.error("Redis client error:", err)) 25 | .on("ready", () => console.log("Redis client ready!")) 26 | .on("reconnecting", () => console.log("Redis client reconnecting...")); 27 | } 28 | 29 | /** 30 | * Uses the singleton promise pattern to initialize the RedisConnector class instance. 31 | * This ensures that only one connection is attempted to the Redis server when the 32 | * instance is first initialized, even if multiple concurrent calls are made. 33 | * @returns {Promise} 34 | */ 35 | public static async getInstance(): Promise { 36 | if (RedisConnector.instance === undefined) { 37 | RedisConnector.instance = new RedisConnector(); 38 | await RedisConnector.instance.client.connect(); 39 | } 40 | return RedisConnector.instance; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lambda-code/nagware/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@lib/*": ["src/lib/*"] 11 | }, 12 | "outDir": "build" 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /lambda-code/notify-slack/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/notify-slack/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/notify-slack/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/notify-slack/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/notify-slack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notify-slack", 3 | "description": "Lambda that notifies Slack and triggers Opsgenie alert", 4 | "version": "1.0.0", 5 | "main": "main.js", 6 | "type": "module", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 10 | "build:dev": "tsc --project tsconfig.json --watch", 11 | "test": "vitest --coverage" 12 | }, 13 | "devDependencies": { 14 | "@types/aws-lambda": "^8.10.149", 15 | "@types/node": "^22.15.3", 16 | "@vitest/coverage-v8": "^1.6.0", 17 | "tsc-alias": "^1.8.15", 18 | "typescript": "^5.8.3", 19 | "vitest": "^1.6.0" 20 | }, 21 | "packageManager": "yarn@4.9.1" 22 | } 23 | -------------------------------------------------------------------------------- /lambda-code/notify-slack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "outDir": "build" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /lambda-code/prisma-migration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 2 | 3 | WORKDIR /var/task 4 | 5 | # install unzip 6 | RUN dnf install -y unzip groff less 7 | 8 | # install new aws v2 cli 9 | RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 10 | RUN unzip awscliv2.zip && ./aws/install && rm awscliv2.zip 11 | 12 | # Install Yarn 13 | RUN npm install -g yarn 14 | RUN export PATH="$(yarn global bin):$PATH" 15 | 16 | # Install global dependencies 17 | RUN yarn global add prisma@6 tsx@4 18 | 19 | # Install Prisma Seeding dependencies to current directory 20 | RUN yarn add @prisma/client@6 prisma@6 ts-command-line-args@2 typescript@5 21 | 22 | # Download correct Prisma engines for runtime 23 | COPY base_schema.prisma ${LAMBDA_TASK_ROOT} 24 | RUN prisma generate --schema=base_schema.prisma --allow-no-models 25 | RUN rm -f base_schema.prisma 26 | 27 | # Copy custom runtime bootstrap 28 | COPY bootstrap ${LAMBDA_RUNTIME_DIR} 29 | # Copy function code 30 | COPY prisma.sh ${LAMBDA_TASK_ROOT} 31 | 32 | RUN chmod +x ${LAMBDA_TASK_ROOT}/prisma.sh ${LAMBDA_RUNTIME_DIR}/bootstrap 33 | 34 | 35 | 36 | 37 | # Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) 38 | CMD [ "prisma.handler" ] -------------------------------------------------------------------------------- /lambda-code/prisma-migration/base_schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = "postgres://postgres:chummy@localhost:5432/forms" 8 | } 9 | -------------------------------------------------------------------------------- /lambda-code/prisma-migration/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -euo pipefail 4 | 5 | # Initialization - load function handler 6 | source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh" 7 | 8 | # Processing 9 | while true; do 10 | HEADERS="$(mktemp)" 11 | # Get an event. The HTTP request will block until one is received 12 | EVENT_DATA=$(curl -sS -LD "$HEADERS" "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next") 13 | 14 | # Extract request ID by scraping response headers received above 15 | REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2) 16 | 17 | # Run the handler function from the script 18 | RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA") 19 | 20 | # Send the response 21 | curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE" 22 | done 23 | -------------------------------------------------------------------------------- /lambda-code/prisma-migration/prisma.sh: -------------------------------------------------------------------------------- 1 | function handler() { 2 | # needed to ensure that the logs are printed to stderr and show up in the AWS console 3 | exec 1>&2 4 | 5 | # Event data can be used in the future to trigger different actions 6 | EVENT_DATA=$1 7 | echo "Echoing event request: '${EVENT_DATA}'" 8 | 9 | DATABASE_URL_VALUE=$(aws secretsmanager get-secret-value --secret-id $DB_URL_SECRET_ARN --query "SecretString" --output text) 10 | export DATABASE_URL="${DATABASE_URL_VALUE}?connect_timeout=30&pool_timeout=30" 11 | 12 | echo "Syncing S3 bucket to /tmp/prisma/" 13 | aws s3 sync s3://${PRISMA_S3_BUCKET_NAME}/ /tmp/prisma 14 | 15 | cp -a node_modules /tmp/node_modules 16 | cp -a package.json /tmp/package.json 17 | 18 | # Move to the prisma directory where the filesystem is writeable 19 | cd /tmp 20 | 21 | echo "Running Prisma migration" 22 | prisma migrate deploy 23 | 24 | echo "Running Prisma generate" 25 | prisma generate 26 | 27 | echo "Running Prisma seeding script" 28 | tsx ./prisma/seeds/seed_cli.ts --environment=production 29 | } 30 | -------------------------------------------------------------------------------- /lambda-code/reliability-dlq-consumer/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/reliability-dlq-consumer/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/reliability-dlq-consumer/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/reliability-dlq-consumer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/reliability-dlq-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reliability-dlq-consumer", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-sqs": "3.799.0" 13 | }, 14 | "devDependencies": { 15 | "@types/aws-lambda": "^8.10.149", 16 | "@types/node": "^22.15.3", 17 | "tsc-alias": "^1.8.15", 18 | "typescript": "^5.8.3" 19 | }, 20 | "packageManager": "yarn@4.9.1" 21 | } 22 | -------------------------------------------------------------------------------- /lambda-code/reliability-dlq-consumer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "outDir": "build" 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /lambda-code/reliability/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/reliability/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/reliability/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/reliability/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/reliability/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reliability", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-dynamodb": "3.799.0", 13 | "@aws-sdk/client-lambda": "3.799.0", 14 | "@aws-sdk/client-s3": "3.799.0", 15 | "@aws-sdk/client-sqs": "3.799.0", 16 | "@aws-sdk/lib-dynamodb": "3.799.0", 17 | "@gcforms/connectors": "^2.0.2", 18 | "json2md": "^2.0.3", 19 | "uuid": "^11.1.0" 20 | }, 21 | "devDependencies": { 22 | "@types/aws-lambda": "^8.10.149", 23 | "@types/json2md": "^1.5.4", 24 | "@types/node": "^22.15.3", 25 | "@types/uuid": "^10.0.0", 26 | "tsc-alias": "^1.8.15", 27 | "typescript": "^5.8.3" 28 | }, 29 | "packageManager": "yarn@4.9.1" 30 | } 31 | -------------------------------------------------------------------------------- /lambda-code/reliability/src/lib/markdown.ts: -------------------------------------------------------------------------------- 1 | import json2md from "json2md"; 2 | import { extractFormData } from "./dataLayer.js"; 3 | import { FormSubmission } from "./types.js"; 4 | 5 | export default ( 6 | formSubmission: FormSubmission, 7 | submissionID: string, 8 | language: string, 9 | createdAt: string 10 | ) => { 11 | const title = `${ 12 | language === "fr" 13 | ? formSubmission.deliveryOption.emailSubjectFr 14 | ? formSubmission.deliveryOption.emailSubjectFr 15 | : formSubmission.form.titleFr 16 | : formSubmission.deliveryOption.emailSubjectEn 17 | ? formSubmission.deliveryOption.emailSubjectEn 18 | : formSubmission.form.titleEn 19 | }`; 20 | 21 | const stringifiedData = extractFormData(formSubmission, language); 22 | const mdBody = stringifiedData.map((item) => { 23 | return { p: item }; 24 | }); 25 | 26 | let emailMarkdown = json2md([{ h1: title }, mdBody]); 27 | const isoCreatedAtString = new Date(parseInt(createdAt)).toISOString(); 28 | 29 | // Using language attribute tags https://notification.canada.ca/format 30 | // This is done so screen readers can read in the correct voice 31 | if (language === "fr") { 32 | emailMarkdown = `[[fr]]\n${emailMarkdown}\nDate de soumission : ${isoCreatedAtString}\nID : ${submissionID}\n[[/fr]]`; 33 | } else { 34 | emailMarkdown = `[[en]]\n${emailMarkdown}\nSubmission Date: ${isoCreatedAtString}\nID: ${submissionID}\n[[/en]]`; 35 | } 36 | 37 | return emailMarkdown; 38 | }; 39 | -------------------------------------------------------------------------------- /lambda-code/reliability/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@lib/*": ["src/lib/*"] 11 | }, 12 | "outDir": "build" 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /lambda-code/response-archiver/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/response-archiver/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/response-archiver/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/response-archiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/response-archiver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "response-archiver", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-dynamodb": "3.799.0", 13 | "@aws-sdk/client-s3": "3.799.0", 14 | "@aws-sdk/lib-dynamodb": "3.799.0" 15 | }, 16 | "devDependencies": { 17 | "@types/aws-lambda": "^8.10.149", 18 | "@types/node": "^22.15.3", 19 | "tsc-alias": "^1.8.15", 20 | "typescript": "^5.8.3" 21 | }, 22 | "packageManager": "yarn@4.9.1" 23 | } 24 | -------------------------------------------------------------------------------- /lambda-code/response-archiver/src/lib/fileAttachments.ts: -------------------------------------------------------------------------------- 1 | export interface FileAttachement { 2 | name: string; 3 | path: string; 4 | } 5 | 6 | /** 7 | * Given a form submission JSON string, returns an array of file attachments objects that 8 | * includes the file name, S3 object URL. 9 | */ 10 | export function getFileAttachments(formSubmission: string): FileAttachement[] { 11 | const attachmentPrefix = "form_attachments/"; 12 | const attachmentRegex = 13 | /^form_attachments\/\d{4}-\d{2}-\d{2}\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/.+$/; 14 | 15 | // Check file attachments exist by checking the form submission for the 16 | // file attachment prefix that's part of the uploaded file's S3 object key 17 | if (formSubmission.indexOf(attachmentPrefix) > -1) { 18 | // Be more explicit in the attachment check to avoid false positives 19 | const jsonObj = JSON.parse(formSubmission); 20 | return Object.values(jsonObj) 21 | .filter((value) => attachmentRegex.test(value)) 22 | .map((value) => ({ name: getFileNameFromPath(value), path: value })); 23 | } 24 | 25 | return []; 26 | } 27 | 28 | function getFileNameFromPath(filePath: string): string { 29 | return filePath.substring(filePath.lastIndexOf("/") + 1); 30 | } 31 | -------------------------------------------------------------------------------- /lambda-code/response-archiver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@lib/*": ["src/lib/*"] 11 | }, 12 | "outDir": "build" 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /lambda-code/submission/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/submission/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/submission/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/submission/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "submission", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "dependencies": { 12 | "@aws-sdk/client-dynamodb": "3.799.0", 13 | "@aws-sdk/client-sqs": "3.799.0", 14 | "@aws-sdk/lib-dynamodb": "3.799.0", 15 | "uuid": "^11.1.0" 16 | }, 17 | "devDependencies": { 18 | "@types/aws-lambda": "^8.10.149", 19 | "@types/node": "^22.15.3", 20 | "@types/uuid": "^10.0.0", 21 | "tsc-alias": "^1.8.15", 22 | "typescript": "^5.8.3" 23 | }, 24 | "packageManager": "yarn@4.9.1" 25 | } 26 | -------------------------------------------------------------------------------- /lambda-code/submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@lib/*": ["src/lib/*"], 11 | "@tests/*": ["src/tests/*"] 12 | }, 13 | "outDir": "build" 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /lambda-code/vault-integrity/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/lambda-code/vault-integrity/.yarn/install-state.gz -------------------------------------------------------------------------------- /lambda-code/vault-integrity/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /lambda-code/vault-integrity/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 as build 2 | 3 | WORKDIR /usr/app 4 | 5 | RUN npm install -g yarn 6 | RUN yarn set version stable 7 | 8 | COPY .yarnrc.yml package.json yarn.lock ./ 9 | 10 | RUN yarn install --frozen-lockfile 11 | 12 | COPY src ./src 13 | COPY tsconfig.json ./ 14 | 15 | RUN yarn build 16 | 17 | FROM public.ecr.aws/lambda/nodejs:22 18 | 19 | WORKDIR ${LAMBDA_TASK_ROOT} 20 | 21 | COPY package.json ./ 22 | COPY --from=build /usr/app/node_modules ./node_modules 23 | COPY --from=build /usr/app/build ./ 24 | 25 | CMD ["main.handler"] -------------------------------------------------------------------------------- /lambda-code/vault-integrity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vault-integrity", 3 | "version": "1.0.0", 4 | "main": "main.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rm -rf build && tsc -p tsconfig.json && tsc-alias -p tsconfig.json", 9 | "build:dev": "tsc --project tsconfig.json --watch" 10 | }, 11 | "devDependencies": { 12 | "@types/aws-lambda": "^8.10.149", 13 | "@types/node": "^22.15.3", 14 | "tsc-alias": "^1.8.15", 15 | "typescript": "^5.8.3" 16 | }, 17 | "packageManager": "yarn@4.9.1" 18 | } 19 | -------------------------------------------------------------------------------- /lambda-code/vault-integrity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "target": "ESNext", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@lib/*": ["src/lib/*"], 11 | "@tests/*": ["src/tests/*"] 12 | }, 13 | "outDir": "build" 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /local_dev_files/certificates/client-config.ovpn: -------------------------------------------------------------------------------- 1 | client 2 | dev tun 3 | proto udp 4 | remote-random-hostname 5 | resolv-retry infinite 6 | nobind 7 | remote-cert-tls server 8 | cipher AES-256-GCM 9 | verb 3 10 | 11 | reneg-sec 0 12 | 13 | verify-x509-name aws-development-server name -------------------------------------------------------------------------------- /local_dev_files/clean_terragrunt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on any error 4 | set -e 5 | 6 | # Text colors 7 | greenColor='\033[0;32m' 8 | yellowColor='\033[0;33m' 9 | redColor='\033[0;31m' 10 | reset='\033[0m' # No Color 11 | 12 | basedir=$(pwd) 13 | 14 | printf "${yellowColor}=> Clearing local files terragrunt cache and lock files...${reset}\n" 15 | for dir in $basedir/env/cloud/*/; do 16 | rm -rf "${dir}.terragrunt-cache" 17 | rm -f "${dir}.terraform.lock.hcl" 18 | done 19 | 20 | printf "${greenColor}=> Local files cleared.${reset}\n" 21 | 22 | printf "${yellowColor}=> Clearing local terraform state files and backups...${reset}\n" 23 | for file in $basedir/env/cloud/*/*.tfstate*; do 24 | rm -f $file 25 | done 26 | printf "${greenColor}=> Local terraform state files and backups cleared.${reset}\n" 27 | -------------------------------------------------------------------------------- /local_dev_files/create_vpn_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Text colors 4 | greenColor='\033[0;32m' 5 | yellowColor='\033[0;33m' 6 | redColor='\033[0;31m' 7 | reset='\033[0m' # No Color 8 | 9 | basedir=$(pwd) 10 | 11 | easyrsa_dir="$basedir/local_dev_files/easyrsa" 12 | 13 | if [ ! -d "$easyrsa_dir" ]; then 14 | printf "${greenColor}Easy RSA not yet installed.${reset}\n" 15 | printf "${greenColor}Installing Easy RSA...${reset}\n" 16 | git clone https://github.com/OpenVPN/easy-rsa.git $easyrsa_dir 17 | else 18 | printf "${greenColor}Easy RSA already installed.${reset}\n" 19 | fi 20 | 21 | cd $easyrsa_dir/easyrsa3 22 | 23 | if [ ! -d "$easyrsa_dir/easyrsa3/pki" ]; then 24 | printf "${greenColor}Easy RSA not yet initialized.${reset}\n" 25 | ./easyrsa init-pki 26 | fi 27 | 28 | printf "${greenColor}Easy RSA Initialized...${reset}\n" 29 | printf "${greenColor}Generating VPN certificates...${reset}\n" 30 | 31 | ./easyrsa --batch --req-cn=aws-development-ca build-ca nopass 32 | ./easyrsa --san=DNS:server --batch --req-cn=aws-development-server build-server-full aws-development-server nopass 33 | ./easyrsa --batch build-client-full client.development.aws nopass 34 | 35 | if [ ! -d "$basedir/aws/vpn/certificates" ]; then 36 | mkdir $basedir/aws/vpn/certificates 37 | fi 38 | 39 | # Copy the server and client certificates to the local_dev_files directory 40 | cp $easyrsa_dir/easyrsa3/pki/ca.crt $basedir/local_dev_files/certificates/ 41 | cp $easyrsa_dir/easyrsa3/pki/ca.crt $basedir/aws/vpn/certificates 42 | cp $easyrsa_dir/easyrsa3/pki/issued/aws-development-server.crt $basedir/aws/vpn/certificates 43 | cp $easyrsa_dir/easyrsa3/pki/private/aws-development-server.key $basedir/aws/vpn/certificates 44 | cp $easyrsa_dir/easyrsa3/pki/issued/client.development.aws.crt $basedir/local_dev_files/certificates 45 | cp $easyrsa_dir/easyrsa3/pki/private/client.development.aws.key $basedir/local_dev_files/certificates 46 | printf "${greenColor}VPN certificates initialized.${reset}\n" 47 | -------------------------------------------------------------------------------- /local_dev_files/sso_login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROFILE=${1:-development} 4 | 5 | if aws --profile "$PROFILE" sts get-caller-identity >/dev/null 2>&1; then 6 | echo "Valid session" 7 | else 8 | echo "Invalid session: logging using SSO" 9 | aws sso login --profile "$PROFILE" 10 | fi 11 | -------------------------------------------------------------------------------- /local_dev_files/tf/blank.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/local_dev_files/tf/blank.tf -------------------------------------------------------------------------------- /readme_images/GSI_Vault_StatusCreatedAt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/readme_images/GSI_Vault_StatusCreatedAt.png -------------------------------------------------------------------------------- /readme_images/Vault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cds-snc/forms-terraform/dac07b17f9aed7fa3b14b1f1a548ee9119393b26/readme_images/Vault.png -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "changelog-path": "CHANGELOG.md", 5 | "release-type": "simple", 6 | "bump-minor-pre-major": false, 7 | "bump-patch-for-minor-pre-major": false, 8 | "draft": false, 9 | "prerelease": false, 10 | "pull-request-title-pattern": "chore: GCForms release v${version}" 11 | } 12 | }, 13 | "changelog-sections": [ 14 | { "type": "feat", "section": "Features" }, 15 | { "type": "feature", "section": "Features" }, 16 | { "type": "fix", "section": "Bug Fixes" }, 17 | { "type": "perf", "section": "Performance Improvements" }, 18 | { "type": "revert", "section": "Reverts", "hidden": true }, 19 | { "type": "docs", "section": "Documentation" }, 20 | { "type": "style", "section": "Styles" }, 21 | { "type": "chore", "section": "Miscellaneous Chores" }, 22 | { "type": "refactor", "section": "Code Refactoring" }, 23 | { "type": "test", "section": "Tests", "hidden": true }, 24 | { "type": "build", "section": "Build System", "hidden": true }, 25 | { "type": "ci", "section": "Continuous Integration", "hidden": true } 26 | ], 27 | "include-component-in-tag": false, 28 | "include-v-in-tag": true, 29 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 30 | } 31 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>cds-snc/renovate-config" 5 | ], 6 | "packageRules": [ 7 | { 8 | "description": "Group all non-major GitHub actions", 9 | "matchManagers": [ 10 | "github-actions" 11 | ], 12 | "matchUpdateTypes": [ 13 | "minor", 14 | "patch", 15 | "pin", 16 | "pinDigest", 17 | "digest", 18 | "bump" 19 | ], 20 | "groupName": "all non-major github action dependencies", 21 | "groupSlug": "all-non-major-github-action" 22 | }, 23 | { 24 | "description": "Group all non-major Docker images", 25 | "matchDatasources": [ 26 | "docker" 27 | ], 28 | "matchUpdateTypes": [ 29 | "minor", 30 | "patch", 31 | "pin", 32 | "pinDigest", 33 | "digest", 34 | "bump" 35 | ], 36 | "groupName": "all non-major docker images", 37 | "groupSlug": "all-non-major-docker-images" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 3.35.0 2 | --------------------------------------------------------------------------------